并发编程线程基础——超详细

530 阅读22分钟

前言

本篇内容较多,近8千字,可以依据目录按照需要进行阅读。

看完这篇文章,如果想了解并发编程的其他基础知识,可以看这篇:

并发编程的其他基础知识——小白入门篇: juejin.cn/post/696659…

一、线程创建的三种方式

Java中有三中线程创建方式,分别是实现Runnable接口的run方法,继承Thread类并重写run的方法,使用FutureTask方式。

继承Thread类并重写run的方法

当创建完thread对象后该线程并没有被启动执行,知道调用了start方法后才真正启动了线程。

其实调用start方法后线程并没有马上执行,而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU之外的其他资源,等待获取CPU资源后才会真正处于运行状态。一旦run方法执行完毕,该线程就处于终止状态。

优点:

  1. 在run方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法。
  2. 方便传参,可以在子类里面添加成员变量,通过set方法设置参数或通过构造函数进行传递。

缺点:

  1. Java不支持多继承,如果继承了Thread类,那么就不能再基础其他类。
  2. 任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runable则没有这个限制。

代码实现:

public class ThreadTest {

    //继承Thread类并重写run方法
    public static class MyThread extends Thread{

        @Override
        public void run(){
            System.out.println("I am a child thread");
        }

    }

    public static void main(String[] args){
        //创建线程
        MyThread thread = new MyThread();

        //启动线程
        thread.start();
    }
}

实现Runnable接口的run方法

缺点: 如果要传递参数,只能使用主线程里面被声明为final的变量。

代码实现

public class ThreadTest {

    public static class RunableTask implements Runnable{

        @Override
        public void run(){
            System.out.println("I am a child thread");
        }
    }

    //中断异常
    public static void main(String[] args) throws InterruptedException{
        RunableTask task = new RunableTask();
        new Thread(task).start();
        new Thread(task).start();
    }
}

使用FutureTask方式

上面介绍的两种方式都有一个缺点,就是任务没有返回值。

代码实现

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest {

    //创建任务类,类似Runable
    public static class CallerTask implements Callable<String> {

        @Override
        public String call() throws Exception{
            return "hello";
        }
    }

    public static void main(String[] args) throws InterruptedException{
        //创建异步任务
        FutureTask<String> futureTask = new FutureTask<>(new CallerTask());

        //启动线程
        new Thread(futureTask).start();
        try{
            //等待任务执行完毕,并返回结果
            String result = futureTask.get();
            System.out.println(result);
        }catch(ExecutionException e){
            e.printStackTrace();
        }
    }
}

二、线程通知和等待

Java中的Object类是所有类的父类,鉴于继承机制,Java把所有类都需要的方法放到了Object类里面,其中就包含了本节要讲的通知和等待系列函数———wait()、notify()、notifyAll()

1. wait函数

当一个线程调用一个共享变量的wait()方法时,该掉员工线程会被阻塞挂起,直到发生下面几件事之一才返回: (1)其他线程调用了该共享对象的notify()或者notifyAll()方法;

(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

注意:如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法是调用线程会抛出IllegalMonitorStateException异常。

线程获取共享变量的监视器锁的方法:

(1)执行synchronized同步代码块是,使用该共享变量作为参数。

synchronized (共享变量){
    //doSomething
}

(2)调用该共享变量的方法,并且该方法使用了synchronized修饰

synchronized void add(int a, int b){
    //doSomething
}
虚假唤醒

如果一个线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,就从挂起状态变为可以运行状态(也就是被唤醒)。

虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停的去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足唤醒该进程的条件。

synchronized (obj){
    while(条件不满足){
        obj.wait();
    }
}
wait方法只会释放当前共享变量上的锁

当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。

举例

public class ThreadTest {

    //创建资源
    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();

    public static void main(String[] args) throws InterruptedException{

        //创建线程threadA
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    //获取resourceA共享资源的监视器锁
                    synchronized(resourceA){
                        System.out.println("threadA get resourceA lock");

                        synchronized(resourceB){
                            System.out.println("threadA get resourceB lock");

                            //线程A阻塞,并释放(release)获取到的resourceA的锁
                            System.out.println("threadA release resourceA lock");
                            resourceA.wait();
                        }
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        //创建线程threadB
        Thread threadB = new Thread(new Runnable() {

            @Override
            public void run(){
                try{
                    //休眠1秒
                    Thread.sleep(1000);

                    //获取resourceA共享资源的监视器锁
                    synchronized(resourceA){
                        System.out.println("threadB get resourceA lock");

                        System.out.println("threadB try get resouceB lock...");

                        //获取resourceB共享资源的监视器锁
                        synchronized(resourceB){
                            System.out.println("threadB get resourceB lock");

                            //线程B阻塞,并释放获取到的resourceA 的锁
                            System.out.println("threadB release resourceA lock");
                            resourceA.wait();
                        }
                    }
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        });
        
        //启动线程
        threadA.start();
        threadB.start();
        
        //等待两个线程结束
        threadA.join();
        threadB.join();
        
        System.out.println("main over");
    }
}

运行结果:

image.png

结果分析: , 在代码中,在main函数里面启动了线程A和线程B,为了让线程A先获取到锁,这里让线程B先休眠1s,线程A先获取共享变量resourceA 和 共享变量resourceB 上的锁,然后调用了resourceA 的wait()方法阻塞自己,阻塞自己后线程A释放掉获取的 resourceA 上的锁。

线程B休眠结束后会首先尝试获取resourceA 上的锁,如果当时线程A还没有调用 wait()方法释放该锁,那么线程B会被阻塞,当线程A释放了resourceA 上的锁后,线程B就会获取到resourceA 上的锁,然后尝试获取resourceB 上的锁。由于线程A调用的是resourceA 上的wait()方法,所以线程A挂起自己后并没有释放获取到的resourceB 上的锁,所以线程B尝试获取resourceB 上的锁时会被阻塞。

这就证明了:当线程调用共享对象的wait()方法时,当前线程自会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。

中断阻塞挂起的线程会抛出异常

当一个线程调用共享对象的wait()方法被阻塞挂起后,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常并返回。

示例

public class ThreadTest {

    static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException{

        //创建线程
        Thread threadA = new Thread(new Runnable(){
            public void run(){
                try{
                    System.out.println("---begin---");

                    //阻塞当前线程
                    synchronized(obj){
                        obj.wait();
                    }
                    System.out.println("---end---");
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        });

        threadA.start();

        Thread.sleep(1000);

        System.out.println("---begin interrupt threadA---");
        threadA.interrupt();                                    //主线程中断了线程threadA
        System.out.println("---end interrupt threadA---");
    }
}

运行结果:

image.png

代码分析:

在如上代码中,threadA 调用共享对象obj的wait()方法后阻塞挂起了自己,然后主线程在休眠1s后中断了threadA线程,中断后threadA 在obj.wait()处抛出java.lang.InterruptedException异常而返回并终止。

2. wait(long timeout) 函数

与wait()方法相比多了一个超时参数,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify() 或者 notifyAll() 方法唤醒,那么该函数还是会因为超时而返回。

如果timeout设置为0则和wait方法效果一样,因为在wait方法内部就是调用了wait(0)。

如果timeout < 0,则会抛出IllegalArgumentException异常。

3. wait(long timeout, int nanos)函数

在其内部调用的是wait(long timeout)函数,如下代码只有在nanos>0是才使参数timeout递增1

源码

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);
    }

4. notify()函数

一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上掉员工wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程竞争该锁,只有该线程竞争到了共享变量的监视器后才可以继续执行。

类似wait系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛出IllegalMonitorStateException异常。

5. notifyAll()函数

notifyAll()方法会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

需要注意的是:在共享变量上掉员工notifyAll()方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。 如果调用notifyAll()方法后,一个线程调用了该共享变量的wait()方法而被放入阻塞集合,则该线程是不会被唤醒的。

示例

public class NotifyAndNotifyAll {

    //创建资源
    private static volatile Object resourceA = new Object();

    public static void main(String[] args) throws InterruptedException{

        //创建线程
        Thread threadA = new Thread(new Runnable() {
            public void run(){

                //获取resourceA共享资源的监视器锁
                synchronized(resourceA){

                    System.out.println("threadA get resourceA lock");
                    try{
                        System.out.println("threadA begin wait");
                        resourceA.wait();
                        System.out.println("threadA end wait");
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                }
            }
        });

        //创建线程
        Thread threadB = new Thread(new Runnable() {
            public void run(){

                //获取resourceA共享资源的监视器锁
                synchronized(resourceA){
                    System.out.println("threadB get resourceA lock");
                    try{
                        System.out.println("threadB begin wait");
                        resourceA.wait();
                        System.out.println("threadB end wait");
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                }
            }
        });

        //创建线程
        Thread threadC = new Thread(new Runnable() {
            public void run(){
                synchronized(resourceA){
                    System.out.println("threadC begin notify");
                    //resourceA.notify();                 //唤醒一个在共享变量resourceA上调用wait方法所阻塞的线程
                    resourceA.notifyAll();                //唤醒所有在共享变量resourceA上调用wait方法所阻塞的线程
                }
            }
        });

        //启动线程
        threadA.start();
        threadB.start();

        Thread.sleep(1000);                         //主线程休眠1s
        threadC.start();

        //等待线程结束
        threadA.join();
        threadB.join();
        threadC.join();

        System.out.println("main over");
    }
}

调用notify()的运行结果

只有一个线程A被唤醒,线程B没有被唤醒

image.png

调用notifyAll()的运行结果

线程A和线程B都会被唤醒

image.png

三、等待线程执行终止的join方法

在开发过程中,有时候我们需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。

Thread类中有一个join方法可以做这个事情。前面介绍的等待通知方法是Object类中的方法,而join方法则是Thread类直接提供的。join是无参且返回值为void 的方法。

示例:

    public static void main(String[] args) throws InterruptedException{
        Thread threadA = new Thread(new Runnable(){
            public void run(){
                try{
                    Thread.sleep(1000);
                }catch(Exception e){
                    e.printStackTrace();
                }

                System.out.println("chile threadA over!");
            }
        });

        Thread threadB = new Thread(new Runnable(){
            public void run(){
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }

                System.out.println("chile threadA over!");
            }
        });

        //启动子线程
        threadA.start();
        threadB.start();

        System.out.println("wait all child thread over!");

        //等待子线程执行完毕,返回
        threadA.join();
        threadB.join();

        System.out.println("all child thread over !");
    }

上面代码在主线程创建了两个子线程threadA和threadB,然后启动它们,并分别调用它们的join()方法,那么主线程首先会在调用threadA.join()方法后被阻塞,等待threadA执行完毕后返回。然后调用threadB.join()方法后被阻塞,等待threadB执行完毕后返回。

运行结果

image.png

如果线程A调用线程B的join方法后会被阻塞,当其他线程调用了线程A的interrupt()方法中断了线程A时,线程A会抛出InterruptedException异常而返回。

四、让线程睡眠的sleep方法

Thread类中有一个静态的sleep方法,当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,即在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。 指定时间到了后该函数会正常返回,线程就处于就绪状态,然后处于CPU的调度,获取CPU资源后就可以继续运行了。

如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回。

在调用Thread.sleep(long millis)时,如果millis参数的值是负数,则会抛出IllegalArgumentException异常。

举例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SleepTest {

    //创建一个独占锁
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException{

        //创建线程A
        Thread threadA = new Thread(new Runnable(){
            public void run(){
                //获取独占锁
                lock.lock();

                try{
                    System.out.println("child threadA is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child threadA is in awaked");
                }catch(Exception e){
                    e.printStackTrace();
                }finally {
                    //释放锁
                    lock.unlock();
                }

            }
        });

        //创建线程B
        Thread threadB = new Thread(new Runnable(){
            public void run(){
                //获取独占锁
                lock.lock();

                try{
                    System.out.println("child threadB is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child threadB is in awaked");
                }catch(Exception e){
                    e.printStackTrace();
                }finally{
                    //释放锁
                    lock.unlock();
                }
            }
        });

        //启动线程
        threadA.start();
        threadB.start();
    }

}

运行结果

image.png

如上代码首先创 个独占锁,然后创建了两个线程,每个线程在内部先获取锁,然后睡眠 睡眠结束后会释放锁。首先,无论你执行多少遍上面的代码都是线程A先输出或者线程B先输出,不会出现线程A和线程B交叉输出的情况 从执行结果来看,线程A先获取了锁,那么线程A会先输出一行,然后调用 sleep 方法让自己睡眠 10s,在线程A睡眠的这10s内那个独占锁 lock 还是线程A自己持有,线程B会一直阻塞直到线程A醒来后执行 lock 释放锁。

五、让出CPU执行权的yield方法

Thread 有一个静态 yield 方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己 CPU 使用,但是线程调度器可以无条件忽略这个暗示。

当一个线程调用 yield 方法时, 当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 PU 的那个线程来获取CPU执行行权。

六、线程中断

Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

接下来介绍三中线程中断的方法:

1. void interrupt()方法:

中断线程,例如线程A运行时,当线程A运行时,线程B可以调用线程interrupt()方法来设置线程A的中断标志为 true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。

如果线程A因为调用了wait系列函数、join()方法或者sleep()方法阻塞挂起,这时若线程B调用线程iterrupt()方法,线程A会在调用这些方法的地方抛 InterruptedException异常并返回。

2. boolean isInterrupted()方法:

检测当前线程是否被中断,如果是则返回true,否则返回false,不会去除中断标志

3. boolean interrupted()方法:

检测当前线程是否被中断,如果是返回 true,否则返回false。与 lnterrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法是 static 方法,可以通过 Thread 类直接调用。另外从下面的代码可以知道 interrupted()内部是获取当前调用线程的中断标志 不是调用interrupted()方法的实例对象的中断标志。

public static boolean  interrupted(){
    //清除中断标志
    return currentThread().isInterrupted(true);
}

七、理解线程上下文切换

1. 概念

在多线程编程中,线程个数一般都大于CPU的个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转策略 ,也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用这就是上下文切换,从当前线程的上下文切换到了其他线程。

切换线程上下文时需要保存当前线程的执行现场,当再次执行是根据报错的执行现场信息恢复执行线程。

线程上下文切换时机有: 当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。

2. 如何减少上下文切换

减少上下文切换的方法有4种,分别是无锁并发编程、CAS算法、使用最少线程和使用协程。怎么说可能有点模糊,下面是各种方法的介绍。

无锁并发编程: 多线程竞争锁时,会引发上下文切换,所以多线程处理数据时,可以用一些并发来避免使用锁,比如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

CAS算法: Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

使用最少线程: 避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。

协程: 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

3. 资源限制带来的挑战

3.1 什么是资源限制

在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。其中,硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的了解和socket连接数等。

3.2 资源限制引发的问题

在并发编程中,将代码执行速度加快是原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的数据。

3.3 如何解决资源限制的问题

对于硬件资源限制,可以考虑使用集群并发执行程序。既然单机的资源有限制,那么就让程序在多机上运行。

对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。

3.4 在资源限制情况下并发编程

根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源————带宽和硬盘读写速度。有数据库操作时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。

八、线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

1. 死锁产生必须具备的四个条件

互斥条件: 指线程对己经获取到的资源进行排它性使用 即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资线程释放该资源。

请求并持有条件: 指一个线程己经持有了至少一个资源 但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己经获取的资源。

不可剥夺条件: 指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完后才由自己释放该资源。

环路等待条件: 指在发生死锁时,必然存在一个线程————资源的环形链。

下面通过一个例子来说明线程死锁:

public class DeadLockTest {

    //创建资源
    private static Object resourceA = new Object();
    private static Object resourceB = new Object();

    public static void main(String[] args){

        //创建线程A
        Thread threadA = new Thread(new Runnable(){
            public void run(){
                synchronized(resourceA){
                    System.out.println(Thread.currentThread()+" get resourceA");

                    try{
                        Thread.sleep(1000);                 //让当前线程休眠1s
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread()+" waiting get resourceB");
                    synchronized(resourceB){
                        System.out.println(Thread.currentThread()+" get resourceB");
                    }
                }
            }
        });

        //创建线程B
        Thread threadB = new Thread(new Runnable(){
            public void run(){
                synchronized(resourceB){
                    System.out.println(Thread.currentThread()+" get resourceB");

                    try{
                        Thread.sleep(1000);
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread()+" waiting get resourceA");

                    synchronized(resourceA){
                        System.out.println(Thread.currentThread()+" get resourceA");
                    }
                }
            }
        });

        //启动线程
        threadA.start();
        threadB.start();

    }
}

运行结果

image.png

下面分析以上代码是如何满足死锁的四个条件的 首先 resourceA和sourceB都是互斥资源,当线程A调用 synchronized(resource A)方法获取到resourceA监视器锁并释放前,线程B再调用synchronized(resourceA)方法尝试获取该资源会被阻塞,只有线程A主动释放该锁,线程B才能获得,这满足了资源互斥

线程A首先通过synchronized(resourceA)方法获取到resourceA上的监视器锁资源,然后通过synchronized(res oureB)方法等待获取recourceB监视器锁资源,这就构成了请求并持有条件

线程A在获取resourA上的监视器锁资源后,该资源不会被线程B掠夺走,只有线程自己主动释放recourceA资源时,它才会放弃对该资源的持有权,这构成了资源的不可剥夺条件。

线程A持有objectA资源并等待获取objectB资源,而线程B持有objectB 资源并等待objectA 资源,这构成了环路等待条件,所以线程A和线程B就进入了死锁状态。

2. 如何避免线程死锁

要想避免死锁,至少要破坏掉构造死锁的一个必要条件,目前只有请求并持有和环路等待条件是可以被破坏的。

上面导致死锁的代码,我们可以通过修改线程B对资源申请的顺序,就可以避免死锁,线程B的代码修改如下:

//创建线程B
        Thread threadB = new Thread(new Runnable(){
            public void run(){
                synchronized(resourceA){
                    System.out.println(Thread.currentThread()+" get resourceB");

                    try{
                        Thread.sleep(1000);
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread()+" waiting get resourceA");

                    synchronized(resourceB){
                        System.out.println(Thread.currentThread()+" get resourceA");
                    }
                }
            }
        });

运行结果

image.png

可以发现,线程B这次是先申请resourceA,再申请resourceB,要想获取resourceA,只能等待线程A释放资源,所以当线程A运行结束释放资源resourceA,线程B才能拿到资源由阻塞状态进入就绪状态,然后在获得CPU资源后开始运行。

资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。

九、守护线程与用户线程

Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程。其实在JVM内部同时还启动了好多守护线程,比如垃圾回收线程

那么守护线程和用户线程有什么区别呢? 区别之一是当最后一个非守护线程结束时,JVM正常退出,而不管当前是否守护线程,也就是说守护线程是否结束并不影响JVM退出。言外之意,只要有一个用户线程还没结束 正常情况下,JVM就不会退出。

1. 如何创建一个守护线程?

public static void main(String[] args){
        Thread daemonThread = new Thread(new Runnable(){
            public void run(){

            }
        });

        //设置为守护进程
        daemonThread.setDaemon(true);
        daemonThread.start();
    }

从代码中可以发现,只需要设置线程的daemon参数为true即可。

2. 使用守护线程和用户线程的时机

如果你希望在主线程结束后,JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。

十、ThreadLocal

ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。

当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存。

1. ThreadLocal使用示例

public class ThreadLocalTest {

    //print函数
    static void print(String str){
        //1.1 打印当前线程本地内存中localVariable变量的值
        System.out.println(str+":"+localVariable.get());
        //1.2 清除的当前线程本地内存中的localVariable变量
        localVariable.remove();
    }

    //创建ThreadLocal变量
    static ThreadLocal<String> localVariable = new ThreadLocal<>();

    public static void main(String[] args){
        //创建线程one
        Thread threadOne = new Thread(new Runnable(){
            public void run(){
                //设置线程one中本地变量localVariable的值
                localVariable.set("threadOne local variable");
                //调用打印函数
                print("threadOne");
                //打印本地变量值
                System.out.println("threadOne remove after: "+localVariable.get());
            }
        });

        //创建线程two
        Thread threadTwo = new Thread(new Runnable(){
            public void run(){
                //设置值
                localVariable.set("threadTwo local variable");
                //调用打印函数
                print("threadTwo");
                //打印本地变量值
                System.out.println("threadTwo remove after: "+localVariable.get());
            }
        });

        //启动线程
        threadOne.start();
        threadTwo.start();
    }
}

运行结果

image.png

在线程One中,通过set反复设置了localVariable的值,这其实设置的是线程One本地内存中的一个副本,这个副本线程Two是访问不了的。然后调用print函数,print函数通过get函数获取了当前线程(线程One)本地内存中的localVariable的值并输出,然后取出当前线程本地内存中的localVariable变量。

线程Two的执行类似于线程One。

2. get、set、remove方法介绍

这三个方法都是ThreadLocal变量调用的,get方法用于获取对应线程的本地内存变量值,set方法用于设置对应线程的本地内存变量值,remove方法用于删除对应线程的threadLocals中的本地变量。

每个线程的本地变量存放在线程自己的内存变量threadLocal中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,一次使用完毕后记得调用ThreadLocal的remove方法输出对应线程的threadLocals中的本地变量。

3. ThreadLocal不支持继承性

看下面例子:

public class ThreadLocal不支持继承性 {

    //创建线程变量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static void main(String[] args){
        //设置线程变量
        threadLocal.set("hello world");

        //创建并启动子线程
        Thread thread = new Thread(new Runnable(){
            public void run(){
                System.out.println("thread: "+threadLocal.get());           //输出null,因为子线程获取不到父线程设置的值
            }
        });
        thread.start();

        System.out.println("main: "+threadLocal.get());
    }
}

运行结果

image.png

也就是说,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。

根据上面的介绍,这应该是正常现象,因为在子线程thread里面调用get方法时当前线程为hread 线程,而这里调用set方法设置线程变量的是main线程,两者是不同的线程,自然子线程访问时返回null。

4. InheritableThreadLocal类

InheritableThreadLocal继承自ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。

第3点的示例,要想thread线程也可以访问到主线程设置的值,只要修改创建线程的代码,即:

public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

修改为

public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>();

扩展:要想访问父线程中的threadlocal变量,除了使用InheritableThreadLocal之外,还有其他方式,比如在创建线程时传入父线程中的变量,并将其复制到子线程中。

文末: 感谢看完,文中并没有详细说明一些方法的源码实现,如果想更深入的了解,推荐看《Java并发编程之美》。