前提:不知道你是否和小编一样的困惑?由于小编感觉自己每次学习都不够深入,都只是停留在用的阶段,也会刷视频,但是视频也没有觉得特别的深入,就会很焦虑和迷茫。所以小编决定刷书,并把里面的图和代码都手动画和手敲,才能帮助我记忆,因为这些知识太容易遗忘了。但是速度是真的很慢,但看到一句话说:慢就是快。所以别急,一起加油啊。
1.1 什么是线程
在讨论什么是线程前有必须先说下什么是进程,因为线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程至少有一个线程,进程中的多个线程共享进程的资源。
操作系统在分配资源时是把资源分配给进程的,但是CPU资源特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。
在Java中,当我们启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。
由图可以看到,一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。那么为何要将程序计数器设计为线程私有的呢?前面说了线程是占用CPU执行的基本单位,而CPU一般是使用时间片轮转方式让线程轮询占用的,所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行 。那么如何知道之前程序执行到哪里了呢?其实程序计数器就是为了记录该线程让出CPU时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。另外需要注意的是,如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是Java代码时程序计数器记录的才是下一条指令的地址。
另外每个线程都有自己的栈资源,用于存储该线程的局部变量,这个局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧。
堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例。
方法区则用来存放JVM加载的类、常量及静态(不需要类实例即对象,即是共享的。)变量等信息,也是线程共享的。
1.2 线程创建与运行
Java中有三种线程创建方式,分别为实现Runnable接口的run方法,继承Thread类并重写run的方法,使用FutureTask方式。
首先看继承Thread类方式的实现。
package com.example.demo.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();
//启动线程
thread.start();
}
}
如上代码中的MyThread类继承了Thread类,并重写了run()方法。在main函数里面创建了一个MyThread的实例,然后调用该实例的start方法启动了线程。需要注意的是,当创建完thread对象后该线程并没有被启动执行,直到调用了start方法后才真正启动了线程。
其实调用start方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态。一旦run方法执行完毕,该线程就处于终止状态。
使用继承方法的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThred()方法;不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runnable则没有这个限制。下面看实现Runnable接口的run方法方式。
package com.example.demo.thread;
public class RunnableTest {
public static class RunnableTask implements Runnable{
@Override
public void run() {
System.out.println("I am a child thread");
}
}
public static void main(String[] args) {
RunnableTask task = new RunnableTask();
new Thread(task).start();
new Thread(task).start();
}
}
如上面代码所示,两个线程公用一个task代码逻辑,如果需要,可以给RunnableTask添加参数进行任务区分。另外,RunnableTask可以继承其他类。但是上面介绍的两种方式都有一个缺点,就是任务没有返回值。下面看最后一种,即使用FutureTask的方式。
package com.example.demo.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class FutureTaskTest {
//创建任务类,类似Runable
public static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception {
return "hello";
}
}
public static void main(String[] args) {
//创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
//启动线程
new Thread(futureTask).start();
try {
//等待任务执行完毕,并返回结果
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如上代码中的CallerTask类实现了Callable接口的call()方法。在main函数内首先创建了一个FutureTask对象(构造函数为CallerTask的实例),然后使用创建的FutureTask对象作为任务创建了一个线程并且启动它,最后通过future.get()等待任务执行完毕并返回结果。
小结:使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进程传递,不同的对象可以有不同的参数,而如果使用Runnable方式,则只能使用主线程里面被声明为final的变量。不好的地方是Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runnable则没有这个限制。前两种都没办法拿到任务的返回结果,但是Futuretask方式可以。
1.3 线程通知与等待
Java中的Object类是所有类的父类,鉴于继承机制,Java把所有类都需要的方法放到了Object类里面,其中就包含本节要讲的通知与等待系列函数。
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()方法的实例,首先通过同步块获取obj上面的监视器锁,然后在while循环内调用obj的wait()方法。
下面从一个简单的生产者和消费者例子来加深理解。如下面代码所示,其中queue为共享变量,生产者线程在调用queue的wait()方法前,使用synchronized关键字拿到了该共享变量queue的监视器锁,所以调用wait()方法才不会抛出IllegalMonitorStateException异常。如果当前队列没有空闲容量则会调用queued的wait()方法挂起当前线程,这里使用循环就是为了避免上面说的虚假唤醒问题。假如当前线程被虚假唤醒了,但是队列还是没有空余容量,那么当前线程还是会调用wait()方法把自己挂起。
//生产线程
synchronized(queue){
//消费队列满,则等待队列空闲
while(queue.size() == MAX_SIZE){
try{
//挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后获取队列里的元素
queue.wait();
} catch(Exception ex){
ex.printStackTrace();
}
}
//空闲则生成元素,并通知消费者线程
queue.add(ele);
queue.notifyAll();
}
//消费者线程
synchronized(queue){
//消息队列为空
while(queue.size() == 0){
try{
//挂起当前线程,并释放通过获取的queue上的锁,让生产者线程可以获取锁,将产生元素放入队列
queue.wait();
} catch(Exception ex){
ex.printStackTrace();
}
}
//消费元素,并通知唤醒生产者线程
queue.take();
queue.notifyAll();
}
在如上代码中假如生产者A首先通过synchronized获取到了queue上的锁,那么后续所有企图生产元素的线程和消费线程将会在获取该监视器锁的地方被阻塞挂起。线程A获取锁后发现当前队列已满会调用queue.wait()方法阻塞自己,然后释放获取的queue上的锁,这里考虑下为何要释放锁?如果不释放,由于其他生产者线程和消费者线程都已经被阻塞挂起,而线程A也被挂起,这就处于了死锁状态。这里线程A挂起自己后释放共享变量上的锁,就是为了打破死锁必要条件之一的持有并等待原则。关于死锁后面的章节会讲。线程A释放锁后,其他生产者线程和消费者线程中会有一个线程获取queue上的锁进而进入同步块,这就打破了死锁状态。
另外需要注意的是,当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。下面来看一个例子。
package com.example.demo.thread;
public class ThreadTest2 {
//创建资源
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException {
//创建线程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
//获取resourceA共享资源的监视器锁
synchronized (resourceA) {
System.out.println("threadA get resourceA lock.");
//获取resourceB共享资源的监视器锁
synchronized (resourceB){
System.out.println("threadA get resourceB lock.");
//线程A阻塞,并释放获取到的resouceA的锁
System.out.println("threadA release resouceA");
resourceA.wait();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
//获取resourceA共享资源的监视器锁
synchronized (resourceA){
System.out.println("threadB get resourceA lock");
System.out.println("threadB try to get resouceB lock");
//获取resouceB共享资源的监视器锁
synchronized (resourceB){
System.out.println("threadB get resourceB lock");
//线程B阻塞,并释放获取到的resouceA的锁
System.out.println("threadB release resouceA lock");
resourceA.wait();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程
threadA.start();
threadB.start();
//等待两个线程结束
threadA.join();
threadB.join();
System.out.println("main over");
}
}
输出结果如下:
如上代码中,在main函数里面启动了线程A和线程B,为了让线程A先获取到锁,这里让线程B先休眠了1s,线程A先后获取到共享变量resourceA和共享变量B上的锁,然后调用了resouceA的wait()方法阻塞自己,阻塞自己后线程A释放掉获取的resouceA上的锁。
线程B休眠结束后会首先尝试获取resourceA上的锁,如果当时线程A还没有调用wait()犯法释放该锁,那么线程B会被阻塞,当线程A释放了resourceA上的锁后,线程B就会获取到resourceA上的锁,然后尝试获取resourceB上的锁。由于线程A调用的resourceA上的wait()方法,所以线程A挂起自己后并没有释放获取到的resourceB上的锁,所以线程B尝试获取resourceB上的锁时会被阻塞。
这就证明了当线程调用共享对象的wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。
最后再举一个例子进行说明。当一个线程调用共享对象的wait()方法被阻塞挂起后,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常并返回。
package com.example.demo.thread;
public class WaitNotifyInterupt {
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
//创建线程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("---begin---");
//阻塞当前线程
synchronized (obj){
obj.wait();
}
System.out.println("---end---");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
Thread.sleep(1000);
System.out.println("---begin interrupt threadA---");
threadA.interrupt();
System.out.println("---end interrupt threadA---");
}
}
输出如下。
如上代码中,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则会抛出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()函数
不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。
下面举一个列子来说明nofity()和notifyAll()方法的具体含义及一些需要注意的地方,代码如下。
public class NotifyTest {
//创建资源
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
//创建线程
Thread threadA = new Thread(new Runnable() {
@Override
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 (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
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 (InterruptedException e) {
e.printStackTrace();
}
}
}
});
//创建线程
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println("threadC begin notify");
resourceA.notify();
}
}
});
//启动线程
threadA.start();
threadB.start();
Thread.sleep(1000);
threadC.start();
//等待线程结束
threadA.join();
threadB.join();
threadC.join();
System.out.println("main over");
}
}
输入结果如下。
如上代码开启了三个线程,其中线程A和线程B分别调用了共享资源resourceA的wait()方法,线程C则调用了notify()方法。这里启动线程C前首先调用sleep方法让主线程休眠1s,这样做的目的是让线程A和线程B全部执行到调用wait方法后再调用线程C的notify方法。这个例子试图在线程A和线程B都因调用共享资源resourceA和wait()方法而被阻塞后,让线程C再调用resourceA的nofity()方法,从而唤醒线程A和线程B。但是从执行结果来看,只有一个线程A被唤醒,线程B没有被唤醒:
从输出结果可知线程调度器这次先调度了线程A占用CPU来运行,线程A首先获取resourceA上面的锁,然后调用resourceA的wait()方法挂起当前线程并释放获取到的锁,然后线程B获取到resourceA上的锁并调用resourceA的wait()方法,此时线程B也被阻塞挂起并释放了resourceA上的锁,到这里线程A和线程B都被放到了resourceA的阻塞集合里面。线程C休眠结束后在共享资源resourceA上调用notify()方法,这会激活resourceA的阻塞集合里面的一个线程,这里激活了线程A,所以线程A调用的wait()方法返回了,线程A执行完毕。而线程B还处于阻塞状态。如果把线程C调用的notify()方法改为调用notifyAll()方法,则执行结果如下。
从输入结果可知线程A和线程B被挂起后,线程C调用notifyAll()方法会唤醒resourceA的等待集合里面的所有线程,这里线程A和线程B都会被唤醒,只是线程B先获取到resourceA上的锁,然后让wait()方法返回。线程A执行完毕后,主线程返回,然后打印输出。
一个需要注意的地方是,在共享变量上调用notifyAll()方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。如果调用notifyAll()方法后,一个线程调用了该共享变量的wait()方法而被放入阻塞集合,则该线程是不会被唤醒的。尝试把主线程里面休眠1s的代码注释掉,再运行程序会有一定概率输出下面的结果。
也就是在线程B调用共享变量的wait()方法前线程C调用了共享变量的notifyAll方法,这样,只有线程A被唤醒,而线程B并没有被唤醒,还是处于阻塞状态。
1.4 等待线程执行终止的join方法
在项目实践中经常会遇到一个场景,就是需要等待几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。Thread类中有一个join方法可以做这个事情,前面介绍的等待通知方法是Object类中的方法,而join方法则是Thread类直接提供的。join是无参且返回值为void的方法。下面来看一个简单的例子。
package com.example.demo.thread;
public class ThreadJoinTest {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("child threadOne over!");
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("child threadTwo over!");
}
});
//启动子线程
threadOne.start();
threadTwo.start();
System.out.println("wait all child thread over!");
//等待子线程执行完毕,返回
threadOne.join();
threadTwo.join();
}
}
如上代码在主线程里面启动了两个子线程,然后分别调用了它们的join()方法,那么主线程首先会在调用threadOne.join()方法后被阻塞,等待threadOne执行完毕后返回。threadOne执行完毕后threadOne.join()就会返回,然后主线程调用threadTwo.join()方法后再次被阻塞,等待threadTwo执行完毕后返回。这里只是为了演示join方法的作用,在这种情况下使用后面会讲到的CountDownLatch是个不错的选择。
另外,线程A调用线程B的join方法后会被阻塞,当其他线程调用了线程A的interrupt()方法中断了线程A时,线程A会抛出InterruptedException异常而返回。下面通过一个例子来加深理解。
package com.example.demo.thread;
public class ThreadInterruptTest {
public static void main(String[] args) {
//线程one
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("threadOne begin run!");
for (; ; ) {
}
}
});
//获取主线程
final Thread mainThread = Thread.currentThread();
//线程Two
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
//休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断主线程
mainThread.interrupt();
}
});
//启动子线程
threadOne.start();
threadTwo.start();
//等待线程one执行结束
try {
threadOne.join();
} catch (InterruptedException e) {
System.out.println("main thread:" + e);
}
}
}
如上代码在threadOne线程里面执行死循环,主线程调用threadOne的join方法阻塞自己等待线程threadOne执行完毕,待threadTwo休眠1s后会调用主线程的interrupt()方法设置主线程的中断标志,从结果看在主线程中的threadOne.join()处会抛出InterruptedException异常。这里需要注意的是,在threadTwo里面调用的是主线程的interrupt()方法,而不是线程threadOne的。
1.5 让线程睡眠的sleep方法
Thread类中有一个静态的sleep方法,当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回。
package com.example.demo.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SleepTest2 {
//创建一个独占锁
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
//创建线程A
Thread threadA = new Thread(new Runnable() {
@Override
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();
}
}
});
//创建线程A
Thread threadB = new Thread(new Runnable() {
@Override
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释放锁。下面再来看一下,当一个线程处于睡眠状态时,如果另外一个线程中断了它,会不会在调用sleep方法处抛出异常。
package com.example.demo.thread;
public class SleepTest4 {
public static void main(String[] args) throws InterruptedException {
//创建线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("child thread is in sleep");
Thread.sleep(10000);
System.out.println("child thread is in awaked");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程
thread.start();
//主线程休眠2s
Thread.sleep(2000);
//主线程中断子线程
thread.interrupt();
}
}
子线程在睡眠期间,主线程中断了它,所以子线程在调用sleep方法处抛出了InterruptedException异常。
另外需要注意的是,如果在调用Thread.sleep(long millis)时为millis参数传递了一个负数,则会抛出IllegalArgumentException异常,如下所示。
1.6 让出CPU执行权的yield方法
Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。我们知道操作系统是为了每个线程分配一个时间片来占有CPU的,正常情况下当一个线程调用了分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法yield时,是在告诉调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
当一个线程调度用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。下面举一个例子来加深对yield方法的理解。
package com.example.demo.thread;
public class YieldTest implements Runnable {
YieldTest() {
//创建并启动线程
Thread t = new Thread(this);
t.start();
}
@Override
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 YieldTest();
new YieldTest();
new YieldTest();
}
}
输出结果如下。
如上代码开启了三个线程,每个线程的功能都一样,都是在for循环中执行5次打印。运行多次后,上面的结果是出现次数最多的。解开Thread.yield()注释再执行,结果如下。
从结果可知,Thread.yield()方法生效了,三个线程分别在i=0时调用了Thread.yield()方法,所以三个线程自己的两行输出没有在一起,因为输出了第一行后当前线程让出了CPU执行权。
一般很少使用这个方法,在调试或者测试这个方法或许可以帮助复现由于并发竞争条件导致的问题,其在设计并发控制时或许会有用途,后面在讲解java.util.concurrent.locks包里面的锁时会看到该方法的使用。
总结:sleep和yield方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。
1.7 线程中断
Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
- void interrupt()方法:中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。如果线程A因为调用了wait系列函数、join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程A的interrupt()的方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。
- boolean isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。
public boolean isInterrupted(){
//传递false,说明不清除中断标志
return isInterrupted(false);
}
- boolean interrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。与isInterrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法时static方法,可以通过Thread类直接使用。另外从下面的代码可以知道,在interrupted()内部是获取当前调用线程的中断标志位而不是调用interrupted()方法的实例对象的中断标志位。
public static boolean interrupted(){
//清除中断标志
return currentThread.isInterrupted(true);
}
下面看一个线程使用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
}
}
下面看一个根据中断标志判断线程是否终止的例子。
package com.example.demo.thread;
public class InterruptTest3 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
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异常而返回,线程恢复到激活状态。下面看一个例子。
package com.example.demo.thread;
public class InterruptTest4 {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("threadOne begin sleep for 2000 seconds");
Thread.sleep(20000000);
System.out.println("threadOne awaking");
} catch (InterruptedException e) {
System.out.println("threadOne is interrupted while sleeping");
return;
}
}
});
//启动线程
threadOne.start();
//确保子线程进入休眠zhuangtai
Thread.sleep(1000);
//打断子线程的休眠,让子线程从sleep函数返回
threadOne.interrupt();
//等待子线程执行完毕
threadOne.join();
System.out.println("main thread is over");
}
}
在如上代码中,threadOne线程休眠了2000s,在正常情况下该线程需要等到2000s后才会被唤醒,但是本例通过调用threadOne.interrupt()方法打断了该线程的休眠,该线程会在调用sleep方法处抛出InterruptedException异常后返回。
下面再通过一个例子来了解interrupted()与isInterrupted方法的不同之处。
package com.example.demo.thread;
public class InterruptTest5 {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
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 is over");
}
}
输出结果如下。
第一行输出true这个大家应该都可以想到,但是下面三行为何是false,false,true呢?不应该是true,false,false吗?如果你有这个疑问,则说明你对这两个函数的区别还是不太清楚。上面我们介绍了在interrupted()方法内部是获取当前线程的中断状态,这里虽然调用了threadOne的interrupted()方法,但是获取的是主线程的中断标志,因为主线程是当前线程。threadOne.interrupted()和Thread.interrupted()方法的作用是一样的,目的都是获取当前线程的中断标志。修改上面的例子为如下。
package com.example.demo.thread;
public class InterruptTest5 {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
//中断标志位true时会退出循环,并且清除中断标志
while (!Thread.currentThread().interrupted()) {
}
System.out.println("threadOne isInterrupted:" + Thread.currentThread().isInterrupted());
}
});
//启动线程
threadOne.start();
//设置中断标志位
threadOne.interrupt();
threadOne.join();
System.out.println("main is over");
}
}
由输出结果可知,调用interrupted()方法后中断标志被消除了。
1.8 理解线程上下文切换
在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。那么就有一个问题,让出CPU的线程等下次轮到自己占有CPU时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。
线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。
1.9 线程死锁
1.9.1 什么是线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去,如图1-2所示。
在图1-2中,线程A已经持有了资源2,它同时还想申请资源1,线程B已经持有了资源1,它同时还想申请资源2,所以线程1和线程2就因为相互等待对方已经持有的资源,而进入了死锁状态。
那么为什么会产生死锁呢?学过操作系统的朋友应该知道,死锁的产生必须具备以下四个条件。
- 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求资源获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
- 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
- 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
- 环路等待条件:指在发生死锁时,必然存在一个线程资源的环型链,即线程集合{T0,T1,T2,....,Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,......Tn正在等待已被T0占用的资源。
下面通过一个例子来说明线程死锁。
package com.example.demo.thread;
public class DeadTest {
//创建资源
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() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread() + " get ResourceA");
try {
Thread.sleep(10000);
} 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() {
@Override
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();
}
}
下面分析代码和结果:Thread-0是线程A,Thread-1是线程B,代码首先创建了两个资源,并创建了两个线程。从输出结果可以知道,线程调度器先调度了线程A,也就是把CPU资源分配给了线程A,线程A使用synchronized(resourceA)方法获取到了resourceA的监视器锁,然后调用sleep函数休眠1s,休眠1s是为了保证线程A在获取resourceB对应的锁前让线程B抢占到CPU,获取到资源resourceB上的锁。线程A调用sleep方法后线程B会执行synchronized(resourceB)方法,这代表线程B获取到了resourceB对象的监视器锁资源,然后调用sleep函数休眠1s。好了,到了这里,线程A获取到了resourceA资源,线程B获取到了resourceB资源。线程A休眠结束后会企图获取resourceB资源,而resourceB资源被线程B所持有,所以线程A会被阻塞而等待。而同时线程B休眠结束后会企图获取resourceA资源,而resourceA资源已经被线程A持有,所以线程A和线程B就陷入相互等待的状态,也就产生了死锁。下面谈谈本例是如何满足死锁的四个条件的。
首先,resourceA和resourceB都是互斥资源,当线程A调用synchronized(resourceA)方法获取到resouceA上的监视器锁并释放前,线程B再调用synchronized(resourceA)方式尝试获取该资源会被阻塞,只有线程A主动释放该锁,线程B才能获得,这满足了资源互斥条件。
线程A首先通过synchronize(resourceA)方法获取到resourceA上的监视器锁资源,然后通过synchronize(resourceB)方法等待获取resourceB上的监视器锁资源,这就构成了请求并持有条件。
线程A在获取resourceA上的监视器锁资源后,该资源不会被线程B掠夺走,只有线程A自己主动释放resourceA资源时,它才会放弃对该资源的持有权,这构成了资源的不可剥夺条件。
线程A持有objectA资源并等待获取objectB资源,而线程B持有objectB资源并等待objectA资源,这构成了环路等待条件。所以线程A和线程B就进入了死锁状态。
1.9.2 如何避免线程死锁
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但是学过操作系统的读者应该都知道,目前只有请求并持有和环路条件是可以被破坏的。
造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁,那么什么是资源申请的有序性呢?我们对上面线程B的代码进行如下修改。
//创建线程B
Thread threadB = new Thread(new Runnable() {
@Override
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");
}
}
}
});
输出结果如下。
如上代码让在线程B中获取资源的顺序和在线程A中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程A和线程B都需要资源1,2,3,...,n时,对资源进行排序,线程A和线程B只有在获取了资源n-1时才能去获取资源n。
我们可以简单分析一下为何自愿的有序分配会避免死锁,比如上面的代码,假如线程A和线程B同时执行了synchronized(resourceA),只有一个线程可以获取到resourceA上的监视器锁,假如线程A获取到了,那么线程B就会被阻塞而不会再去获取资源B,线程A获取到resourceA的监视器锁后会去申请resourceB的监视器锁资源,这时候线程A是可以获取到的,线程A获取到resourceB资源并使用后会放弃对资源resourceB的持有,然后再释放对resourceA的持有,释放resourceA后线程B才会被从阻塞状态变为激活状态。所以资源的有序性破坏了资源的请求并持有条件,因此避免了死锁。
1.10 守护线程和用户线程
Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出。言外之意,只要有一个用户线程还没结束,正常情况下JVM就不会退出。
那么在Java中如何创建一个守护线程?代码如下。
package com.example.demo.thread;
public class DaemonTest {
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
@Override
public void run() {
}
});
//设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
}
}
只需要设置线程的daemon参数true即可。
下面通过例子来理解用户线程与守护线程的区别。首先看下面的代码。
package com.example.demo.thread;
public class UserDaemonTest {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (; ; ) {
}
}
});
//启动子线程
thread.start();
System.out.println("main thread is over");
}
}
如上代码在main线程中创建了一个thread线程,在thread线程里面是一个无限循环。从运行代码的结果看,main线程已经运行结束了,那么JVM进程已经退出了吗?在IDE的输出结果右上侧的红色方块说明,JVM进程并没有退出。另外,在mac上执行jps会输出如下结果。
这个结果说明了当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。这也说明了在用户线程还存在的情况下JVM进程并不会终止。那么我们把上面的thread线程设置为守护线程后,再来运行看看会有什么结果:
thread.setDaemon(true);
thread.start();
System.out.println("main thread is over");
输出结果如下。
在启动线程前将线程设置为守护线程,执行后的输出结果显示,JVM进程已经终止了,执行ps -eaf|grep java也看不到JVM进程了。在这个例子中,main函数是唯一的用户线程,thread线程是守护线程,当main线程运行结束后,JVM发现当前已经没有用户线程了,就会终止JVM进程。由于这里的守护线程执行的任务是一个死循环,这也说明了如果当前进程中不存在用户线程,但是还存在正在执行任务的守护线程,则JVM不等守护线程运行完毕就会结束JVM进程。
main线程运行结束后,JVM会自动启动一个叫做DestroyJavaVM的线程,该线程会等待所有用户线程结束后终止JVM进程。下面通过简单的JVM代码来证明这个结论。
翻看JVM的代码,能够发现,最终会调用到JavaMain这个C函数。
int JNICALL
JavaMain(void * _args){
...
//执行Java中的main函数
(*env) ->CallStaticVoidMethod(env,mainClass,mainID,mainArgs);
//main函数返回值
ret = (*env) ->ExceptionOccurred(env) == NULL? 0 : 1
//等待所有非守护线程结束,然后销毁JVM进程
LEAVE();
}
LEAVE是C语言里面的一个宏定义,具体定义如下。
define LEAVE()
do{
if ((*vm) ->DetachCurrentThread(vm) != JNI_OK){
JLI_ReportErrorMessage(JVM_ERROR2);
ret = 1;
}
if (JNI_TRUE){
(*vm) ->DestroyJavaVM(vm);
return ret;
}
} while(JNI_FALSE)
该宏的作用是创建一个名为DestroyJavaVM的线程,来等待所有用户线程结束。
在Tomcat的NIO实现NioEndpoint中会开启一组接受线程来接受用户的连接请求,以及一组处理线程负责具体处理用户请求,那么这些线程是用户线程还是守护线程呢?下面我们看一下NioEndpoint的startInternal方法。
在如上代码中,在默认情况下,接受线程和处理线程都是守护线程,这意味着当tomcat收到shutdown命令后并且没有其他用户线程存在的情况下tomcat进程会马上消亡,而不会等待处理线程处理完当前的请求。
总结:如果你希望在主线程结束后JVM进程马上结束,那么就在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。
1.11 ThreadLocal
多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,如图1-3所示。
同步的措施一般是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担。那么有没有一种方式可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?其实ThreadLocal就可以做这件事情,虽然ThreadLocal并不是为了解决这个问题而出现的。
ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存,如果1-4所示。
1.11.1 ThreadLocal使用示例
本节介绍如何使用ThreadLocal。本例开启了两个线程,在每个线程内部都设置了本地变量的值,然后调用print函数打印当前本地变量的值。如果打印后调用本地变量的remove方法,则会删除本地内存中的该变量,代码如下。
package com.example.demo.thread;
public class ThreadLocalTest {
//(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<>();
public static void main(String[] args) {
//(3)创建线程one
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
//3.1设置线程One中本地变量localVariable的值
localVariable.set("threadOne local variable");
//3.2调用打印函数
print("threadOne");
//3.3打印本地变量值
System.out.println("threadOne remove after" + ":" + localVariable.get());
}
});
//(3)创建线程two
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
//3.1设置线程One中本地变量localVariable的值
localVariable.set("threadTwo local variable");
//3.2调用打印函数
print("threadTwo");
//3.3打印本地变量值
System.out.println("threadTwo remove after" + ":" + localVariable.get());
}
});
threadOne.start();
threadTwo.start();
}
}
代码(2)创建了一个ThreadLocal变量。
代码(3)和(4)分别创建了线程One和Two。
代码(5)启动了两个线程。
线程One中的代码3.1通过set方法设置了localVariable的值,这其实设置的线程One本地内存中的一个副本,这个副本线程Two是访问不了的。然后代码3.2调用了print函数,代码1.1通过get函数获取了当前线程(线程One)本地内存中localVariable的值。
线程Two的执行类似于线程One。
1.11.2 ThreadLocal的实现原理
首先看一下ThreadLocal相关类的类图结构,如图1-5所示。
由该图可知,Thread类中有一个threadLocals和inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。另外,Thread里面的threadLocals为何被设计为map结构?很明显是因为每个线程可以关联多个ThreadLocal变量。
下面简单分析ThreadLocal的set、get以及remove方法的实现逻辑。
- void set(T value)
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);
}
}
可以看到,getMap(t)的作用是获取线程自己的变量threadLocals,threadlocal变量被绑定到了线程的成员变量上。
如果getMap(t)的返回值不为空,则把value值设置到threadLocals中,也就是把当前变量值放入当前线程的内存变量threadLocals中。threadLocals是一个HashMap结构,其中key就是当前ThreadLocal的实例对象引用,value是通过set方法传递的值。
如果getMap(t)返回空值则说明是第一次调用set方法,这时创建当前线程的threadLocals变量。下面来看createMap(t,value)做什么。
void createMap(Thread t,T firstValue){
t.threadLocals = new ThreadLocalMap(this,firstValue);
}
它创建当前线程的threadLocals变量。
- 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 setInitialValue(){
//(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变量。
- void remove()
public void remove(){
ThreadLocalMap m = getMap(Thread.currentThread());
if (m !=null){
m.remove(this);
}
}
如上代码所示,如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例的本地变量。
总结:如图1-6所示,在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量。在高级篇要讲解的JUC包里面的ThreadLocalRandom,就是借鉴ThreadLocal的思想实现的,后面会具体讲解。
1.11.3 ThreadLocal不支持继承性
首先看一个例子。
package com.example.demo.thread;
public class TestThreadLocal {
//(1)创建线程变量
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//(2)设置线程变量
threadLocal.set("hello world");
//(3)启动子线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//(4)子线程输出线程变量的值
System.out.println("thread:"+threadLocal.get());
}
});
thread.start();
//(5)主线程输出线程变量的值
System.out.println("main:"+threadLocal.get());
}
}
也就是说,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。根据上节的介绍,这应该是正常现象,因为在子线程thread里面调用get方法时当前线程为thread线程,而这里调用set方法设置线程变量的是main线程,两者是不同的线程,自然子线程访问时返回null。那么有没有办法让子线程能访问到父线程中的值?答案是有。
1.11.4 InheritableThreadLocal类
为了解决上节提出的问题,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(firstValue);
}
}
由如上代码可知,InheritableThreadLocal继承了ThreadLocal,并重写了三个方法。由代码(3)可知,InheritableThreadLocal重写了createMap方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。由代码(2)可知,当调用get方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals。
综上可知,在InheritableThreadLocal的世界里,变量inheritableThreadLocals替代了threadLocals。
下面我们看一下重写的代码(1)何时执行,以及如何让子线程可以访问父线程的本地变量。这里要从创建Thread的代码说起,打开Thread类的默认构造函数,代码如下。
public Thread(Runnable target){
init(null,target,"Thread-"+newThreadNum(),0);
}
private void init(ThreadGroup g,Runnable target,String name,long stackSize,AccessControlContext acc){
...
//(4)获取当前线程
Thread parent = currentThread();
...
//(5)如果父线程的inheritableThreadLocals变量不为null
if(parent.inheritableThreadLocals !=null)
//(6)设置子线程中的inheritableThreadLocals变量
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
tid = nextThreadID();
}
如上代码在创建线程时,在构造函数里面会调用init方法。代码(4)获取了当前线程(这里是指main函数所在的线程,也就是父线程),然后代码(5)判断main函数所在线程里面的inheritableThreadLocals属性是否为null,前面我们讲了InheritableThreadLocal类的get和set方法操作的是inheritableThreadLocals,所以这里的inheritableThreadLocal变量不为null,因此会执行代码(6)。下面看一下createInheritedMap的代码。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap){
return new ThreadLocalMap(parentMap);
}
可以看到,在createInheritedMap内部使用父线程的inheritableThreadLocals变量作为构造函数创建了一个新的ThreadLocalMap变量,然后赋值给了子线程的inheritableThreadLocals变量。下面我们看看在ThreadLocalMap的构造函数内部都做了什么事情。
private ThreadLocalMap(ThreadLocalMap parentMap){
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for(int j = 0; j < len; j++){
Entry e = parentTable[j];
if(e != null){
ThreadLocal<Object> key = e.get();
if(key != null){
//(7)调用重写的方法
Object value = key.childValue(e.value);//返回e.value
Entry c = new Entry(key,value);
int h = key.threadLocalHashCode & (len -1);
while(table[h] !=null){
h = nextIndex(h,len);
table[h] = c;
size++;
}
}
}
}
}
在该构造函数内部把父线程的inheritableThreadLocals成员变量的值复制到新的ThreadLocalMap对象中,其中代码(7)调用了InheritableTheadLocal类重写的代码(1)。
总结:InheritableThreadLocal类通过重写代码(2)和(3)让本地变量保存到了具体线程的inheritableThreadLocals变量里面,那么线程在通过InheritableThreadLocal类实例的set或者get方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面。
把1.11.3节中的代码(1)修改为
//(1)创建线程变量
public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>();
可见,现在可以从子线程正常获取到线程变量的值了。
那么在什么情况下需要子线程可以获取父线程的threadlocal变量呢?情况还是蛮多的,比如子线程需要使用存放在threadlocal变量中的用户登录信息,再比如一些中间件需要把统一的id追踪的整个调用链路记录下来。其实子线程使用父线程中的threadlocal方法有多种方式,比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个map作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下InheritableThreadLocal就显得比较有用。