Java并发编程——线程基础

PunkLu 2020年01月11日 58次浏览
线程基础

什么是线程

线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。

在Java中,当启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。

一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。

程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。线程是占用CPU执行的基本单位,而CPU一般是使用时间片轮转方式让线程轮询占用的,所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行。程序计数器就是为了记录该线程让出CPU时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。需要注意的是,如果执行的是native方法,那么pc计数器记录的是undefined地址,只有执行的是Java代码时pc计数器记录的才是下一条指令的地址。

另外每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧。

堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例。

方法区则用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的。

线程创建与运行

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

首先看继承Thread类方式的实现:

/**
 * 使用继承Thread创建线程的方式
 */
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();

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

使用继承方法的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;不好的地方是Java不支持多继承,继承了这个Thread类就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runnable则没有这个限制。

使用实现Runnable接口的run方法方式:

/**
 * 实现Runnable接口创建线程的方式
 */
public class RunableThread {

    public static class RunableTask implements Runnable{
        public void run() {
            System.out.println("I am a child thread");
        }
    }

    public static void main(String[] args) {
        RunableTask task = new RunableTask();
        // 两个线程共用一个task代码逻辑
        new Thread(task).start();
        new Thread(task).start();
    }
}

如上所示,两个线程共用一个task代码逻辑,如果需要,可以给RunnableTask添加参数进行任务区分。另外,RunnableTask可以继承其他类。但是上面介绍的两种方式,任务都没有返回值。

/**
 * 使用FutureTask创建线程的方式
 */
public class FutureTaskThread {

    public static class CallerTask implements Callable<String>{

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

    public static void main(String[] args) throws InterruptedException{
        // 创建异步任务
        FutureTask<String> futureTask = new FutureTask<String>(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()函数

当一个线程调用一个共享变量的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(long timeout,int nanos)函数

该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将timeout设置为0则和wait()方法效果一样,因为在wait方法内部就是调用了wait(0)。

notify()函数

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

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

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

notifyAll()函数

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

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

在项目实践中经常会遇到一个场景,就是需要等待某几件事完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。Thread类中有一个join方法就可以做这个事情,前面的等待通知方法是Object类中的方法,而join方法则是Thread类直接提供的。join是无参且返回值为void的方法。下面看一个简单的例子:

public class JoinDemo {

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

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

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


        /**
         * 主线程里启动了两个子线程,然后分别调用了它们的join()方法,那么主线程首先会在调用threadOne.join()方法后被阻塞,
         * 等待threadOne执行完毕后返回。
         */

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

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

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

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

如上代码在主线程里面启动了两个线程,然后分别调用了它们的join()方法,那么主线程首先会在调用threadOne.join()方法后被阻塞,等待threadOne执行完毕后返回。threadOne执行完毕后threadOne.join()就会返回,然后主线程调用threadTwo.join()方法后再次被阻塞,等待threadTwo执行完毕后返回。这里只是演示join方法的作用,在这种情况下使用CountDownLatch是更好的选择。

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

让线程睡眠的sleep方法

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

代码:

public class SleepDemo {

    // 创建一个独占锁
    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 (InterruptedException 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 (InterruptedException e){
                    e.printStackTrace();
                }finally {
                    // 释放锁
                    lock.unlock();
                }
            }
        });

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

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

让出CPU执行权的yield方法

Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。操作系统是为每个线程分配一个时间片来占有CPU的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法yield时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。

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

代码:

public class YieldDemo implements Runnable {

    YieldDemo(){
        // 创建并启动线程
        Thread t = new Thread(this);
        t.start();
    }

    public void run() {

        for (int i = 0;i<5;i++){
            // 当i=0时让出CPU执行权,放弃时间片,进行下一轮调度
            if ((i%5)==0){
                System.out.println(Thread.currentThread() + "yield cpu...");

                // 当前线程让出CPU执行权,放弃时间片,进行下一轮调度
                Thread.yield();
            }
        }

        System.out.println(Thread.currentThread() + "is over");
    }

    public static void main(String[] args) {
        new YieldDemo();
        new YieldDemo();
        new YieldDemo();
    }
}

一般很少使用这个方法,在调试或者测试时,这个方法或许可以帮助复现由于并发竞争条件导致的问题。

yield()和sleep()的区别

sleep与yield方法的区别在于,当线程调用sleep方法时会被阻塞挂起指定的时间,在这期间线程调度器不回去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

线程中断

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

  1. void interrupt()方法

    中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。如果线程A因为调用了wait系列函数、join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。

  2. boolean isInterrupted()方法

    检测当前线程是否被中断,如果是返回true,否则返回false。

  3. boolean interrupted()方法

    检测当前线程是否被中断,如果是返回true,否则返回false。与isInterrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法是static方法,可以通过Thread类直接调用。

线程使用Interrupted优雅退出的经典例子:

public void run(){
	try{
		....
		// 线程退出条件
		while(!Thread.currentThread().isInterrupted() && more work to do){
			// do more work;
		}
	}catch(InterruptedException e){
		// thread was interrupted during sleep or wait
	}finally{
		// cleanup,if required
	}
}

根据中断标志判断线程是否终止的例子:

public class InterruptExample {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                // 如果当前线程被中断则退出循环
                while (!Thread.currentThread().isInterrupted()){
                    System.out.println(Thread.currentThread() + " hello");
                }
            }
        });

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


        // 主线程休眠1s,以便中断前让子线程输出
        Thread.sleep(1000);

        // 中断子线程
        System.out.println("main thread interrupt thread");
        thread.interrupt();

        // 等待子线程执行完毕
        thread.join();
        System.out.println("main is over");
    }
}

在如上代码中,子线程thread通过检查当前线程中断标志来控制是否退出循环,主线程在休眠1s后调用thread的interrupt()方法设置了中断标志,所以thread退出了循环。

另一种情况是,当线程为了等待一些特定条件的到来时,一般会调用sleep函数、wait系列函数或者join()函数来阻塞挂起当前线程。比如一个线程调用了Thread.sleep(3000),那么调用线程会被阻塞,直到3s后才会从阻塞状态变为激活状态。但是有可能在3s内条件已被满足,如果一直等到3s后再返回有点浪费时间,这时候可以调用该线程的interrupt()方法,强制sleep方法抛出InterruptedException异常而返回,线程恢复到激活状态。下面看一个例子:

public class InterruptDemo {

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

        Thread threadOne = new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("threadOne begin sleep for 2000 seconds");
                    Thread.sleep(2000000);
                    System.out.println("threadOne awaking");
                }catch (InterruptedException e){
                    System.out.println("threadOne is interrupted while sleeping");
                    return;
                }

                System.out.println("threadOne-leaving normally");
            }
        });

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

        // 确保子线程进入休眠状态
        Thread.sleep(1000);

        // 打断子线程的休眠,让子线程从sleep函数返回
        threadOne.interrupt();

        // 等待子线程执行完毕
        threadOne.join();

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

在如上代码中,threadOne线程休眠了2000s,在正常情况下该线程需要等到2000s后才会被唤醒,但是本例通过调用threadOne.interrupt()方法打断了该线程的休眠,该线程会在调用sleep方法处抛出InterruptedException异常后返回。

再通过一个例子了解interrupted()和isInterrupted()方法的不同之处:

public class InterruptedAndisInterrupted {

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

        Thread threadOne = new Thread(new Runnable() {
            public void run() {
                for (;;){

                }
            }
        });

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

        // 设置中断标志
        threadOne.interrupt();

        // 读取中断标志
        System.out.println("isInterrupted:" + threadOne.isInterrupted());

        // 获取中断标志并重置
        System.out.println("isInterrupted:" + threadOne.interrupted());

        // 获取中断标志并重置
        System.out.println("isInterrupted:" + Thread.interrupted());

        // 获取中断标志
        System.out.println("isInterrupted:" + threadOne.isInterrupted());


        threadOne.join();

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

输出结果:

isInterrupted:true
isInterrupted:false
isInterrupted:false
isInterrupted:true

因为interrupted()方法内部是获取当前线程的中断状态,这里虽然调用了threadOne的interrupted()方法,但是获取的是主线程的中断标志,因为主线程是当前线程。

理解上下文切换

在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。为了让让出CPU的线程等下次轮到自己占用CPU时知道自己之前运行到哪里了,所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。

线程上下文切换时机有:

  1. 当前线程的CPU时间片使用完处于就绪状态时
  2. 当前线程被其他线程中断时

线程死锁

什么是线程死锁

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

死锁的产生必须具备以下四个条件:

  1. 互斥条件

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

  2. 请求并持有条件

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

  3. 不可剥夺条件

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

  4. 环路等待条件

    指在发生死锁时,必然存在一个线程-资源的环形链,即线程集合{T0,T1,T2...,Tn}中的T0正在等待T1占用的资源,T1正在等待T2占用的资源,....Tn正在等待已被T0占用的资源。

线程死锁的例子:

public class DeadLockDemo {

    private static String A = "A";
    private static String B = "B";

    public static void main(String[] args) {
        new DeadLockDemo().deadLock();
    }

    private void deadLock(){
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                synchronized (A){
                    try {
                        Thread.currentThread().sleep(2000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    synchronized (B){
                        System.out.println("1");
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            public void run() {
                synchronized (B){
                    synchronized (A){
                        System.out.println("2");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

避免线程死锁

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

造成死锁的原因和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁,资源有序性即是指假如线程A和线程B都需要资源1,2,3,...,n时,对资源进行排序,线程A和线程B只有在获取了资源n-1时才能去获取资源n。

守护线程与用户线程

Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在JVM内部同时还启动了很多守护线程,比如垃圾回收线程。只要有一个用户线程还没结束,正常情况下JVM就不会退出。

创建守护线程:

public class DaemonThread {


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

        
        设置为守护线程
        // thread.setDaemon(true);
        thread.start();

    }
}

只需要设置线程的daemon参数为true即可。

代码:

public class UserThread {


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

                }
            }
        });

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

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

从上面代码的运行结果看,main线程运行结束后,JVM并没有退出,这个结果说明当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。这也说明了在用户线程还存在的情况下JVM进程并不会终止。而如果是守护线程,则在main线程执行完后,JVM发现没有用户线程在执行了,就会终止JVM进程。

ThreadLocal

多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步。

ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存。

ThreadLocal使用示例

public class ThreadLocalDemo {

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

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

    public static void main(String[] args) {

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

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

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

ThreadLocal的实现原理

Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在具体的线程内存空间中。ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。另外,Thread里面的threadLocals被设计为map结构,是因为每个线程可以关联多个ThreadLocal变量。

简单分析ThreadLocal的set、get及remove方法的实现逻辑

  1. void set

    public void set(T value){
    	// 1、获取当前线程
    	Thread t = Thread.currentThread();
    	// 2、将当前线程作为key,去查找对应的线程变量,找到则设置
    	ThreadLocalMap map = getMap(t);
    	if(map != null){
    		map.set(this,value);
    	}else{
    		// 3、第一次调用就创建当前线程对应的HashMap
    		createMap(t,value);
    	}
    }
    

    代码1首先获取调用线程,然后使用当前线程作为参数调用getMap(t)方法,getMap(Thread t)的代码如下:

    ThreadLocalMap getMap(Thread t){
    	return t.threadLocals;
    }
    

    可以看到,getMap(t)的作用是获取线程自己的变量threadLocals,threadlocal变量被绑定到了线程的成员变量上。

​ 如果getMap(t)的返回值不为空,则把value值设置到threadLocals中,也就是把当前变量值放入当前线程的内存变量threadLocals中。threadLocals是一个HashMap结构,其中key值就是当前ThreadLocal((在代码中声明的ThreadLocal类型的变量))的实例对象引用,value值是通过set方法传递的值。

如果getMap(t)返回空值则说明是第一次调用set方法,这时创建当前线程的threadLocals变量。下面来看createMap(t,value)做什么。

void createMap(Thread t,T firstValue){
	t.threadLocals = new ThreadLocalMap(this,firstValue);
}
  1. T get()

    public T get(){
    	// 4、获取当前线程
    	Thread t = Thread.currentThread();
    	// 5、获取当前线程的threadLocals变量
    	ThreadLocalMap map = getMap(t);
    	// 6、如果threadLocals不为null,则返回对应本地变量的值
    	if(map != null){
    		ThreadLocalMap.Entry e = map.getEntry(this);
    		if(e != null){
    			@SuppressWarnings("unchecked")
    			T result = (T)e.value;
    			return result;
    		}
    	}
    	// 7、threadLocals为空则初始化当前线程的threadLocals成员变量
    	return setInitialValue();
    }
    

    代码4首先获取当前线程实例,如果当前线程的threadLocals变量不为null,则直接返回当前线程绑定的本地变量,否则执行代码7进行初始化。setInitialValue()的代码如下:

    private T setInitailValue(){
    	// 8、初始化为null
    	T value = initialValue();
    	Thread t = Thread.currentThread();
    	ThreadLocalMap map = getMap(t);
    	// 9、如果当前线程的threadLocals变量不为空
    	if(map != null){
    		map.set(this,value);
    	}else{
    		// 10、如果当前线程的threadLocals变量为空
    		createMap(t,value);
    	}
    	return value;
    }
    
    protected T initialValue(){
    	return null;
    }
    

    如果当前线程的threadLocals变量不为空,则设置当前线程的本地变量值为null,否则调用createMap方法创建当前线程的createMap变量。

    1. void remove()
    public void remove(){
    	ThreadLocalMap m = getMap(Thread.currentThread);
    	if(m != null)
    		m.remove(this);
    }
    

    如上代码所示,如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例的本地变量。

ThreadLocal不支持继承性

首先看一个例子:

public class TestThreadLocal{
	// 1、创建线程变量
	public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
	
	public static void main(String[] args){
		// 2、设置线程变量
		threadLocal.set("hello world");
		// 3、启动子线程
		Thread thread = new Thread(new Runnable(){
			public void run(){
				// 4、子线程输出线程变量的值
				System.out.println("thread:" + threadLocal.get());
			}
		});
		thread.start();
		
		// 5、主线程输出线程变量的值
		System.out.println("main:" + threadLocal.get());
	}
}

输出结果如下:

main:hello world
thread:null

也就是说,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。因为在子线程thread里面调用get方法时当前线程为thread线程,而这里调用set方法设置线程变量的是main线程,两者是不同的线程,自然子线程访问时返回null。

InheritableThreadLocal类

使用InheritableThreadLocal

上面测试ThreadLocal类不支持子线程访问父线程变量的代码改为:

public class TestThreadLocal{
	// 1、创建线程变量
	public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>();
	
	public static void main(String[] args){
		// 2、设置线程变量
		threadLocal.set("hello world");
		// 3、启动子线程
		Thread thread = new Thread(new Runnable(){
			public void run(){
				// 4、子线程输出线程变量的值
				System.out.println("thread:" + threadLocal.get());
			}
		});
		thread.start();
		
		// 5、主线程输出线程变量的值
		System.out.println("main:" + threadLocal.get());
	}
}

原理

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

InheritableThreadLocal的代码:

public class InheritableThreadLocal<T> extends ThreadLocal<T>{
	// 1
	protected T childValue(T parentValue){
		return parentValue;
	}
	
	// 2
	ThreadLocalMap getMap(Thread t){
		return t.inheritableThreadLocals;
	}
	
	// 3
	void createMap(Thread t,T firstValue){
		t.inheritableThreadLocals = new ThreadLocalMap(this,firstValue);
	}
}

由如上代码可知,InheritableThreadLocal继承了ThreadLocal,并重写了三个方法,由代码3可知,InheritableThreadLocal重写了createMap方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。由代码2可知,当调用get方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals。

从逻辑上来讲,这并不能做到子线程得到父线程里的值。那秘密在那里呢?通过跟踪Thread的构造方法,你能够发现是在构造Thread对象的时候对父线程的InheritableThreadLocal进行了复制。下面是Thread的部分源码:

public class Thread implements Runnable {
      //默认人构造方法,会调用init方法进行初使化
      public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null);
    }

//最终会调用到当前这个方法
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name.toCharArray();
// parent为当前线程,也就是调用了new Thread();方法的线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            if (security != null) {
                g = security.getThreadGroup();
            }
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        g.checkAccess();
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
//在这里会继承父线程是否为后台线程的属性还有父线程的优先级
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
//这里是重点,当父线程的inheritableThreadLocals 不为空的时候,会调用 ThreadLocal.createInheritedMap方法,传入的是父线程的inheritableThreadLocals。原来复制变量的秘密在这里
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

}

通过跟踪Thread的构造方法,我们发现只要父线程在构造子线程(调用new Thread())的时候inheritableThreadLocals变量不为空。新生成的子线程会通过ThreadLocal.createInheritedMap方法将父线程inheritableThreadLocals变量有的对象复制到子线程的inheritableThreadLocals变量上。这样就完成了线程间变量的继承与传递。