小言_互联网的博客

多线程知识点总结01

436人阅读  评论(0)

目录

一:认识多线程

1. 程序、进程、线程

2. 为什么要使用线程

3. 多线程的原理

4. 多线程的优缺点

二:基本使用及特性

1. 创建线程的方式:

2. 线程的生命周期

3. 线程的优先级

4. 守护线程、线程调度

注意事项:

三、线程同步与锁

1. synchronized关键字

2. 同步方法与同步代码块

3. 静态同步方法与synchronized(class)代码块

4. 锁重入

5. 其他概念:

6. 锁对象的改变

四、线程间通信

1. wait线程等待

2. notify线程唤醒

3. 共性注意问题


一:认识多线程

1. 程序、进程、线程

程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程

进程是一个具有独立功能的程序的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。

线程是进程的基本执行单元,是进程中独立运行的子任务。一个进程的所有任务都在线程中执行(每一个进程至少有一条线程)

2. 为什么要使用线程

进程作为CPU分配资源的基本单位,通常在一个进程中可以包含若干个线程,这些线程可以利用进程所拥有的公共资源。

线程作为独立运行和调度的基本单位。对它的调度所付出的开销会小得多,能更高效的提高系统内多个程序间并发执行的效率。

线程中任务的执行是串行的,多个任务只能按照顺序执行。在同一时间内,1个线程只能执行1个任务。

一个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务。多线程技术可以提高程序的执行效率。

3. 多线程的原理

同一时间内,一个CPU只能处理1条线程,只有1条线程在工作(执行);多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。(当然,如果你的机器是多核,比如双CPU,那么此时就真的是多线程在运行)

如果线程非常多,CPU会在N多线程之间调度,消耗大量的CPU资源;每条线程被调度执行的频次会较低(线程的执行效率减低)

4. 多线程的优缺点

多线程出现的原因:为了解决负载均衡问题,充分利用CPU资源。为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰。为了处理大量的IO操作时或处理的情况需要花费大量的时间等等,比如:读写文件、视频图像的采集、处理、显示、保存等。

优点:能适当提高程序的执行效率,解决一些特定场景需求;能适当提高资源利用率(CPU、内存利用率)避免无用的等待。

缺点:大量的线程,会占用大量的内存空间,降低程序的性能;线程越多,CPU在调度线程上的开销就越大;程序设计更加复杂,例如线程之间的通信、多线程的数据共享,需要防止死锁的产生。

二:基本使用及特性

1. 创建线程的方式:

1.1 继承Thread类(Thread类自身实现了Runnable接口)

继承Thread后需要覆盖run()来实现自己的业务逻辑。调用start()方法以表示就绪,等待CPU的调度。

由于java不支持多继承,假如一个类已经继承其他父类,此时就只能使用方式二

1.2 实现Runnable接口

实现Runnable接口需要实现run方法,在run方法里面实现自己的业务逻辑。

实现Runnable接口的类,需要将此类的对象传递给Thread,调用Thread类的start()方法以使线程就绪。

Thread类的具体的API可自行查阅文档,暂不记录,可参考另一笔记。

2. 线程的生命周期

https://www.cnblogs.com/sunddenly/p/4106562.html

3. 线程的优先级

线程的优先级是为了在多线程环境中便于系统对线程的调度,CPU尽量将执行资源让给优先级高的线程,优先级高的线程将有更大概率优先执行。线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。

线程创建时,子继承父的优先级,比如B启动A线程,则B线程与A线程优先级一致。线程创建后,可通过调用setPriority()方法改变优先级。线程的优先级是1-10之间的正整数,默认值是5。

4. 守护线程、线程调度

https://www.cnblogs.com/sunddenly/p/4106905.html

注意事项:

1. 调用start()方法会通知线程规划器,此线程已启动,准备就绪,但此时仍未真正运行。run()方法里面是对具体业务逻辑的实现,等待CPU调用线程对象的run()方法后,该线程才算真正运行

2. 线程启动具有随机性。线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用某个线程对象的run()方法。线程执行start()方法的顺序,并不代表线程启动的顺序。

三、线程同步与锁

同步是为了防止在多线程环境下,对共享资源的读写访问出现错乱。

1. synchronized关键字

synchronized可以在任意对象及方法上加锁,而加锁的这段代码称为“临界区”或“互斥区”。当一个线程想执行同步方法里的代码时,线程首先尝试去拿这把锁,如果拿到则执行,拿不到,则会不断尝试去拿,直到能拿到为止,而且是有多个线程同时去争抢这把锁。

关键字synchronized取得的锁都是对象锁,用synchronized修饰方法,锁是当前方法调用的调用对象;用synchronized修饰语句块,锁是指定的某个对象。

2. 同步方法与同步代码块

当A对象的同步方法a()被某个线程访问时,其他线程无法对A对象的其他任何同步方法进行访问,但是可以异步的正常访问该对象的任意非同步方法。

同步方法在某些场景下是有弊端的,比如同步的方法耗时很长,其他线程就需要等待很久。而如果该方法中需要同步的,存在线程安全问题的仅仅是小部分代码,耗时的操作比如IO读写并不存在线程安全问题不需要同步,这时就可以选择使用同步代码块。

当一个线程访问某个对象的同步代码块时,其他线程可以任意访问该对象的非同步代码块,而且若两个同步代码块的锁不一致,即可异步访问。使用同步代码块要明确存在线程安全问题的代码,因为其他非同步块的代码是可以异步执行的。

同步代码块synchronized(this)取得的锁对象其实是当前对象。使用synchronized(非this)代码块可以指定其他对象作为锁,这样可以避免与同步方法或者synchronized(this)进行争抢锁,可大大提高运行效率。但是也需要开发人员更加细心避免线程安全问题,合理准确地选择锁对象也是十分重要的。

synchronized(x){}是将x对象本身作为“对象监视器”,所以,当多个线程同时执行synchronized(x){}同步代码块中的方法时呈同步效果。当线程A执行synchronized(x){}同步代码块,线程B执行x对象的synchronized方法或synchronized(this)时,呈同步效果。

3. 静态同步方法与synchronized(class)代码块

两者的功能其实是一致的,都属于Class锁,锁的是当前java文件对应的Class类。Class锁可以对类的所有实例起作用,即A类的不同实例对象a1,a2分别在两个线程中调用A中定义的静态同步方法,仍然会呈现同步效果。

4. 锁重入

当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。即:线程A获得了某个对象的锁,当这个对象锁还未释放时,线程内部想要再次获取这个对象锁的时候还是可以获取的。如果不支持锁重入,就会造成死锁。另外,子类是完全可以通过该机制调用父类的同步方法的。

5. 其他概念:

5.1 非线程安全:主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值更改,值不同步的情况,从而影响程序执行流程。只有共享资源才需同步化,方法内的局部变量根本没有同步的必要。

5.2 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

5.3 同步不能继承,父类的方法A被synchronized修饰,子类继承后,重写的A方法同样需要加synchronized才能达到同步效果

5.4 在JVM中具有String常量池缓存的功能,两个字面量相同的String,如果不是通过new的方式产生,而是synchronized("A")这种方式,那么第二个"A"会从常量池中获取,实际上为同一个对象。所以synchronized代码块通常都不使用String作为锁对象。可以采用这种方式synchronized(new Object())实例化一个Object对象,但是它不做接取,所以并不放入缓存中。

6. 锁对象的改变

先说结论:线程争抢的同步锁是锁对象,而不是其引用,一旦线程开始争抢同步锁,即确定了锁的唯一性,即使锁对象的引用指向了其他对象,或者该对象本身的属性被改变,也不会改变线程争抢的锁对象目标

比如说,线程A和线程B的同步代码块中锁对象都是某个公共变量Object obj = new Object (),即synchronized(obj); A在获得obj对象后,运行过程中将引用obj指向了另一个对象,比如对obj重新赋值,那么分两种情况,一是B线程在赋值操作完成前开始争抢锁对象,那么B和A依然是同步的,争抢的是同一个对象,需要等待A完成。二是B线程在赋值操作完成后开始争抢锁对象,那么B此时和A就不再是争抢同一个锁对象,两者呈异步状态运行(不贴代码了,有点长)

四、线程间通信

1. wait线程等待

wailt()方法的作用是使当前线程等待,调用wait()方法的线程会进入阻塞状态。

调用wait()方法后,当前线程会在wait()所在代码行处停止执行,直到接到通知或者被中断为止。

执行完wait()方法后,当前线程会释放锁。在释放锁对象后,如果没有该对象调用notify语句来唤醒此线程,即使该锁对象已经空闲,此线程仍然处在阻塞状态,无法执行。

线程被nofity唤醒后,进入可运行状态,它会试图重新获得(并不一定能获得)临界区的控制权(也就是锁),并继续执行wait()之后的代码,直到执行完synchronized 代码块的代码或是中途再次遇到wait() ,才会再次释放锁。也就是说,执行notify语句后,锁是不会立刻被释放的。

wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒,不至于一直处在等待过程

2. notify线程唤醒

notify()该方法唤醒在此对象监视器上等待的单个线程。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现,并使它等待获取该对象的对象锁(这意味着,即使收到了通知,wait的线程也不会马上获取对象锁,必须等待notify()方法的线程执行完毕释放锁,才可以。notifyAll() 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法

线程被nofity唤醒后,进入可运行状态,假若它获得了CPU的调度,它会试图重新获得(并不一定能获得)临界区的控制权(也就是锁,此时再次处于阻塞状态,但阻塞原因不再是因为wait,而是锁),获得锁之后,会继续执行wait()之后的代码,直到执行完synchronized 代码块的代码或是中途再次遇到wait() ,才会再次释放锁。也就是说,执行notify语句后,锁是不会立刻被释放的。

notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑;显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while进行等待,而不适合使用if,直到满足条件才继续往下执行。如下代码,如果使用if,线程被唤醒且得到执行后,即使此时now依然<seed,线程会直接退出if语句块去执行doOtherThing(),显然这不是预期结果。所以,必须使用while 循环阻塞。

public class K {
    //状态锁
    private Object lock;
    //条件变量
    private int now,need;
    public void produce(int num){
        //同步
        synchronized (lock){
           //当前有的不满足需要,进行等待,直到满足条件
            while(now < need){
                try {
                    //等待阻塞
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("我被唤醒了!");
            }
           // 做其他的事情
           doOtherThing();
        }
    }
}

3. 共性注意问题

wait() 和 notify() 方法必须在同步块内进行调用,并且在当前对象的同步块中,调用者为锁对象,否则会抛出IllegalMonitorStateException 线程持有的监视器和调用的wait方法的监视器不是一个对象。即a.wait()必须在锁对象为a的同步块中进行调用

每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒后,才会进入就绪队列,等待CPU的调度。反之,一个线程被wait后,就会进入阻塞队列,等待被唤醒。

在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。

这些方法都定义在Object类中(锁对象可以是任意对象,所以这些方法必须定义在超级父类Object中),被final修饰,不可重写

wait(long) 方法,该方法参数是毫秒,如果线程等待了指定的毫秒数()没被唤醒,就会尝试自动唤醒该线程(假如锁对象仍在被其他线程占用,则继续等待并不断尝试,直至锁对象空闲,成功唤醒本线程,所以线程会在指定毫秒数之后的某个不确定时间被唤醒,当然,前提是可以被唤醒)

public final native void notify();

public final native void notifyAll();
 
public final void wait() throws InterruptedException {
    wait(0);
}

public final native void wait(long timeout) throws InterruptedException;

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos > 0) {
        timeout++;
    }

    wait(timeout);
}

线程间通信要注意两个问题:在多生产者多消费者模式下,1.使用while循环阻塞条件,避免过早通知  2. 使用notifyAll 避免假死

 

参考文章https://www.cnblogs.com/stateis0/p/9061611.html

更深一步的探究需要对JVM虚拟机进行学习,暂时停在这里。


转载:https://blog.csdn.net/java_lifeng/article/details/100561258
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场