必须知道的线程通信
1.线程间通信
线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执
行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,这将会带来巨大的价值。在JMM内存模型中我们说道JMM本身是为了解决并发编程中三大问题:原子性、可见性、有序性。而这三大问题本质上就是多线程间通信问题。
1.1 线程间协同
要想实现多个线程的协同:如线程执行的先后顺序、获取某个线程执行结果等等。涉及到线程之间相互通信,大致分为四类:
- 文件共享
- 网络共享
- 共享变量
- jdk提供的线程协调API:
suspend/resume、wait/notify、park/unpark
文件、网络共享这里就不说了!共享变量会通过关键字(volatile)细说。这里讨论下jdk提供的API。
1.2 suspend/resume
大家对于CD机肯定不会陌生,如果把它播放音乐比作一个线程的运作,那么对音乐播放
做出的暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。
/**
* 包子店
*/
public static Object shop = null;
public void suspendResumeTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (shop == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
Thread.currentThread().suspend();
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
shop = new Object();
consumerThread.resume();
}
输出结果:
1、进入等待
2、买到包子,回家
3、通知消费者
这段代码很简单 启动一个消费者线程,如果店铺有包子就购买,没有包子则挂起等待,主线程休眠三秒后开始生产包子后唤起消费线程。这段代码开着没有问题,但是它有个隐患,消费者线程在挂起的时候是没有释放资源的,容易引起死锁!举个例子:
public void suspendResumeDeadLockTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (shop == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
// 当前线程拿到锁,然后挂起
synchronized (this) {
Thread.currentThread().suspend();
}
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
shop = new Object();
// 争取到锁以后,再恢复consumerThread
synchronized (this) {
consumerThread.resume();
}
System.out.println("3、通知消费者");
}
这段代码是不会有结果的,在消费者进入等待过程中,没有释放锁,主线程永远拿不到锁!正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用后面提到的等待/通知机制来替代。
1.3 wait/notify
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B
调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
public static Object shop = null;
public void waitNotifyTest() throws Exception {
// 启动线程
new Thread(() -> {
if (shop == null) { // 如果没包子,则进入等待
synchronized (this) {
try {
System.out.println("1、进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、买到包子,回家");
}).start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
shop = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("3、通知消费者");
}
}
调用结果:
1、进入等待
3、通知消费者
2、买到包子,回家
消费者获取到锁之后判断没有包子,就挂起等待,同时释放锁,主线程生产获取资源后生产包子,通知消费者消费。通过上面代码可以看出来消费者线程wait时释放了锁资源。
当然 wait和notify也有需要注意的地方。
/**
* 会导致程序永久等待的wait/notify
*/
public void waitNotifyDeadLockTest() throws Exception {
// 启动线程
new Thread(() -> {
if (shop == null) { // 如果没包子,则进入等待
try {
Thread.sleep(5000L);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
synchronized (this) {
try {
System.out.println("1、进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、买到包子,回家");
}).start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
shop = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("3、通知消费者");
}
}
消费者休眠5秒,主线程生产者休眠3秒,当主线程休眠结束通知消费者线程的时候,是唤不起消费者的。
通过上面两个例子,可以看出来wait、notify或者notifyAll,需要注意如下几点:
- 使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
- 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
- notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
- notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
- 从wait()方法返回的前提是获得了调用对象的锁。
从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从
wait()方法返回时能够感知到通知线程对变量做出的修改。
WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行。
1.4 wait/notify的经典范式
通过1.3 wait、notify的事例我们可以提炼出等待/通知的经典范式,该范式分为两部分,分
别针对等待方(消费者)和通知方(生产者)。
等待方遵循如下原则。
1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
synchronized(对象) {
while(条件不满足) {
对象.wait();
}
对应的处理逻辑
}
通知方遵循如下原则。
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
synchronized(对象) {
改变条件
对象.notifyAll();
}
1.6 park/unpark 的使用
无论是wait/notify 或者是已经过时的suspend/resume ,在使用上都有过多的限制和需要注意的地方。有没有一个用的更爽的方式,答案当然是有的啊,在J.U.C的包中LockSupport提供了一种更为简洁的方式:park/unpark。
public static Object shop = null;
public void parkUnparkTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (shop == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
LockSupport.park();
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
shop = new Object();
LockSupport.unpark(consumerThread);
System.out.println("3、通知消费者");
}
park 和unpark不要求调用顺序,多次调用unpark之后在调用park,线程会直接运行。但不会叠加,也就是说连续多次调用park方法,第一次会拿到“许可”直接运行,后续调用会进入等待。
1.7 Thread.join()的使用
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才
从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。
看下面事例代码:
在下面所示的例子中,创建了10个线程,编号0~9,每个线程调用前一个线程的join()方法,也就是线程0结束了,线程1才能从join()方法中返回,而线程0需要等待main线程结束。
public class Join {
public static void main(String[] args) throws Exception {
Thread previous = Thread.currentThread();
for (int i = 0; i < 10; i++) {
// 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + " terminate.");
}
static class Domino implements Runnable {
private Thread thread;
public Domino(Thread thread) {
this.thread = thread;
}
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
输出如下:
main terminate.
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.
从上述输出可以看到,每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结束通知)。
1.8 ThreadLocal的使用
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
说白了,JVM维护了一个Map<Thread,T>,每个线程就是一个map里面的key,如果想使用value,必须通过线程这个key来获取。
public class Demo7 {
/** threadLocal变量,每个线程都有一个副本,互不干扰 */
public static ThreadLocal<String> value = new ThreadLocal<>();
/**
* threadlocal测试
*
* @throws Exception
*/
public void threadLocalTest() throws Exception {
// threadlocal线程封闭示例
value.set("这是主线程设置的123"); // 主线程设置值
String v = value.get();
System.out.println("线程1执行之前,主线程取到的值:" + v);
new Thread(new Runnable() {
@Override
public void run() {
String v = value.get();
System.out.println("线程1取到的值:" + v);
// 设置 threadLocal
value.set("这是线程1设置的456");
v = value.get();
System.out.println("重新设置之后,线程1取到的值:" + v);
System.out.println("线程1执行结束");
}
}).start();
Thread.sleep(5000L); // 等待所有线程执行结束
v = value.get();
System.out.println("线程1执行之后,主线程取到的值:" + v);
}
public static void main(String[] args) throws Exception {
new Demo7().threadLocalTest();
}
}
执行结果如下:
线程1执行之前,主线程取到的值:这是主线程设置的123
线程1取到的值:null
重新设置之后,线程1取到的值:这是线程1设置的456
线程1执行结束
线程1执行之后,主线程取到的值:这是主线程设置的123