前言
我只是个搬运工,尊重原作者的劳动成果,本文来源下列文章链接:
https://zhuanlan.zhihu.com/p/129374075
https://blog.csdn.net/jisuanji12306/article/details/86363390
线程之间为什么要通信?
通信的目的是为了更好的协作,线程无论是交替式执行,还是接力式执行,都需要进行通信告知。首先,要短信线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析:
题目:有两个线程A、B,A线程向一个集合里面依次添加元素"abc"字符串,一共添加十次,当添加到第五次的时候,希望B线程能够收到A线程的通知,然后B线程执行相关的业务操作。
方式一:使用 volatile 关键字
基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式
(对这个关键字还不了解的,可以看看这个voatile详解)
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。
volatile语义保证线程可见性有两个原则保证
所有volatile修饰的变量一旦被某个线程更改,必须立即刷新到主内存
所有volatile修饰的变量在使用之前必须重新读取主内存的值
volatile保证可见性原理图
public class TestSync {
// 定义一个共享变量来实现通信,它需要是volatile修饰,否则线程不能及时感知
static volatile boolean notice = false;
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
notice = true;
}
});
// 实现线程B
Thread threadB = new Thread(() -> {
while (true) {
if (notice) {
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
}
});
// 需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}
方式二:使用Object类的wait() 和 notify() 方法
众所周知,Object类提供了线程间通信的方法:wait()、notify()、notifyaAl(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。注意: wait和 notify、notifyaAl必须配合synchronized使用,wait方法释放锁,notify方法不释放锁
wait() 作用是使当前执行该代码的线程进入该对象锁的阻塞队列中进行等待(立即释放锁,进入等待锁的阻塞队列,下次被唤醒时,会接着往下执行)。
线程调用了对象锁的 wait 线程运行完毕以后,它会立即释放掉该对象锁。此时如果没有其他线程调用该对象锁的 notify 方法,则该调用过wait方法的线程由于没有得到该对象锁的通知,还会继续阻塞在 wait 状态,直到这个有线程调用此对象锁的 notify 或 notifyAll。
wait(long) 方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。
notify() 方法作用是随机通知一个当前对象锁的阻塞队列中的线程(即调用过wait方法的线程)。
notifyAll() 方法作用是唤醒当前对象锁阻塞队列中的所有线程(即调用过wait方法的线程)
public class TestSync {
public static void main(String[] args) {
// 定义一个锁对象
Object lock = new Object();
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
synchronized (lock) {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
lock.notify();// 唤醒B线程
}
}
});
// 实现线程B
Thread threadB = new Thread(() -> {
while (true) {
synchronized (lock) {
if (list.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
}
}
});
// 需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}
由打印结果截图可知,在线程A发出notify()唤醒通知之后,依然是走完了自己线程的业务之后,线程B才开始执行,这也正好说明了,在执行 notify 方法后,当前线程不会马上释放该对象锁,被通知的线程也并不能马上获取该对象锁,要等到执行 notify() 方法的线程将程序执行完,也就是退出 synchronized 代码块后,当前线程才会释放锁,被通知线程才可以获取该对象锁。
方式三:使用JUC工具类 CountDownLatch
jdk1.5之后在java.util.concurrent包下提供了很多并发编程相关的工具类,简化了我们的并发编程代码的书写,CountDownLatch基于AQS框架,相当于也是维护了一个线程间共享变量state
public class TestSync {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(1);
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
countDownLatch.countDown();
}
});
// 实现线程B
Thread threadB = new Thread(() -> {
while (true) {
if (list.size() != 5) {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
});
// 需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}
方式四:使用 ReentrantLock 结合 Condition
Condition接口对应的方法:
void await() 使线程进入等待状态直达其他线程使用该Condition调用signal()或者signalAll()方法。同时该线程可以响应中断
void awaitUninterruptibly() 与上述一样,但该方法使得线程不响应中断
long awaitNanos(long nanosTimeout) 超时式的等待如果返回为0或者负数表示已经超时
boolean awaitUntil(Date deadline) 到指定的时间如果依然未被通知,中断则返回false
void signal() 与 void signalAll()唤醒等待,被唤醒的线程返回时必须获取锁才行。
public class TestSync {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
lock.lock();
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
condition.signal();
}
lock.unlock();
});
// 实现线程B
Thread threadB = new Thread(() -> {
lock.lock();
if (list.size() != 5) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
lock.unlock();
});
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadA.start();
}
}
显然这种方式使用起来并不是很好,代码编写复杂,而且线程B在被A唤醒之后由于没有获取锁还是不能立即执行,也就是说,A在唤醒操作之后,并不释放锁。这种方法跟 Object 的 wait() 和 notify() 一样。
方式五:基本LockSupport实现线程间的阻塞和唤醒
LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。
public class TestSync {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 实现线程B
final Thread threadB = new Thread(() -> {
if (list.size() != 5) {
LockSupport.park();
}
System.out.println("线程B收到通知,开始执行自己的业务...");
});
// 实现线程A
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
LockSupport.unpark(threadB);
}
});
threadA.start();
threadB.start();
}
}
经典消费者和生产者的问题
测试的main方法:
public class ThreadTest12 {
public static void main(String[] args) {
String lock = new String();
P p = new P(lock);
C c = new C(lock);
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
p.setValue();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
c.getVlue();
}
}
}).start();
}
}
生产者:
class P {
private String lock;
public P(String lock) {
super();
this.lock = lock;
}
public void setValue(){
try {
synchronized (lock){
if (!ValueObject.value.equals("")){
lock.wait();
}
String value = System.currentTimeMillis() + "_" + System.nanoTime();
System.out.println("set 的值是 "+value);
ValueObject.value = value;
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
消费者:
class C {
private String lock;
public C(String lock) {
super();
this.lock = lock;
}
public void getVlue() {
try {
synchronized (lock) {
if (ValueObject.value.equals("")) {
lock.wait();
}
System.out.println("get 的值是 " + ValueObject.value);
ValueObject.value = "";
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
操作值
class ValueObject {
public static String value = "";
}
一个生产者和一个消费者,它们之间的通信是正常的,但修改为多个生产者和多个消费者,就会出现问题,修改部分源码如下:
public static void main(String[] args) {
String lock = new String();
P p = new P(lock);
C c = new C(lock);
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
p.setValue();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
c.getVlue();
}
}
}).start();
}
}
运行后,出现一直等待的假死状况。
在代码中确实已经通过 wait / notify 进行呈通信了,但不保证 notify 唤醒的是异类,也许是同类,比如“生产者”唤醒“生产者”,或“消费者”唤醒“消费者”这样的情况。如果按这样情况运行的比率积少成多,就会导致所有的线程都不能继续运行下去,大家都在等待,都呈 WAITING 状态,程序最后也就呈“假死”的状态,不能继续运行下去了。
要解决这个问题,只需要将notify换成notifyAll即可,每次都是唤醒所有线程。