该系列博文大多为阅读《java并发编程之美 翟陆续、薛宾田著 》的笔记
一、线程状态
线程总共有五种状态:新建(newThread)、就绪(runnable)、运行(running)、死亡(dead)、阻塞(blocked)。
0、线程状态的转换
其中等待队列是阻塞状态,不过不是调用的是object类的方法,而不是Thread类的相关方法。
1、wait()方法,Object类的方法
当一个线程调用一个共享变量的wait()方法时,该调用线程会进入阻塞状态,直到发生下面两种情况才会返回:
- 其他线程调用该共享对象的notify()或者notifyAll()方法;
- 其他线程调用了该线程的interrupt()方法,抛出InterruptException异常
想要调用wait()方法,需要现要获取对象的监视器锁,否则会报IllegalMonitorStateException。
而获取监视器锁的方式有两种
调用wait()方法后,会释放该共享变量上的锁,当前线程其他共享变量的锁不会被释放。然后会等待其他线程调用共享变量的notify()或者notifyAll()方法,重新竞争获取共享变量的锁之后,再去执行wait()方法后面的代码。
2、notify()和notifyAll()方法,Object类的方法
调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait()方法被挂起的线程。而notifyAll()会唤醒所有在该共享变量上调用wait()方法被挂起的线程。
3、join()方法,Thread类的方法
调用对象线程的join()方法后,当前线程会进入阻塞状态直到对象线程死亡才会往下执行,比如用于等待多个线程资源加载完成。
比如线程A调用线程B的join()方法,线程A称为当前线程,B成为对象线程,线程A会进入阻塞状态(不是B线程进入等待状态),等待B线程的返回 。
4、sleep()方法,Thread类的方法
让当前线程从运行态转变为超时等待状态(timed waiting)(阻塞态一种),暂时让出指定时间的cpu调度权,但不会释放拥有的监视器资源,比如锁。时间结束后继续参与cpu的调度。
5、yield()方法,Thread类的方法
当前线程请求让出自己的cpu,然后重新排队获取cpu时间片,获取到时间片后再继续执行。与sleep()不同的是,线程会进入就绪状态,而不是阻塞状态。这也就造成了yield()方法可能是一厢情愿,cpu可能还是会优先执行该线程。
6、线程中断相关方法,Thread类的方法
- void interrupt():设置对象线程的中断状态为true,对象线程并不会因此被中断,而是会继续执行下去。直到对象线程调用了wait系列函数、join方法或者sleep方法进入阻塞状态时,调用的地方将会抛出InterruptedException异常而返回。
- PS:因为wait、join、sleep方法是会使当前线程进行相应的操作,所以在A线程是无法使B线程进行这些动作的,只能在B线程里面进行调用才有相应的效果。
- boolean isInterrupted():返回对象线程的中断标志
- boolean interrupted():返回当前线程的中断标志,并且重置标志为false。比如说当前线程中断标志为true,第一次调用返回true,第二次返回false。
二、死锁
死锁是指两个或者两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象。比如线程T1拥有资源R1,过程中需要等待R2资源;线程T2拥有R2资源,却等待R1资源的释放。
1、死锁产生条件
- 互斥条件:指资源在同一时间内只能由一个线程占用,其他线程想要使用需要等待。
- 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已经被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取到的资源。
- 不可剥夺条件:指线程获取到的资源在自己使用完之前不被其他线程占有,只有自己使用完后才会被释放。
- 环路等待:发生死锁时,必然会存在线程与资源的环形链。
2、如何避免死锁
- 避免嵌套锁请求资源。
- 注意加锁的资源,不要对不必要的资源加锁。
- 注意请求资源的先后顺序,使其不能形成环路。比如已有线程T1拥有R1资源,请求R2资源,那么就不能存在线程T2拥有R2资源,请求R1资源。
- 避免无限等待,比如获取ReentrantLock的lock(long time, TimeUnit unit)方法请求锁,一段时间内请求不到会返回false;又或者使用Thread的join(long millis)方法时,加上时间限制
三、守护线程
java线程分两种:daemon(守护线程)和user(用户线程)。daemon线程会伴随着JVM的退出而销毁,而JVM退出前需要确保没有运行的user线程才能退出。
也就是说,daemon线程用于你希望某些后台线程能随着JVM关闭而关闭的场景。
设置线程为守护线程只需要设置daemon为true即可:
Thread a=new Thread(()->{
//do something
});
a.setDaemon(true);
a.start();
四、线程本地变量
- ThreadLocal:可以新建一个共享变量,让其他线程各自保存自己的本地变量
- InheritableThreadLocal:可建立一个让子线程访问到父线程的对象。