问题
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作,即多个线程在操作同一份数据时, 避免对同一共享变量的争夺。
线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。
实现线程通信的方式:
- 使用 volatile 关键字
- 使用 Object 类的 wait()/notify()
- 使用 JUC 工具类 CountDownLatch
- 使用 ReentrantLock 结合 Condition
- 基本 LockSupport 实现线程间的阻塞和唤醒
一、使用 volatile 关键字
基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想。大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式
public class VolatileSyncTest {
static volatile boolean notice = false;
public static void main(String[] args) {
// 定义线程A
Thread threadA = new Thread(() -> {
System.out.println("线程A开始执行");
notice = true;
System.out.println("线程A执行完成");
});
// 定义线程B,须等待notice=true时再执行
Thread threadB = new Thread(() -> {
System.out.println("线程B开始执行");
while (true) {
if (notice) {
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
}
System.out.println("线程B执行完成");
});
// 启动测试 先启动线程B,再启动线程A
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadA.start();
}
}
执行结果打印:
线程B开始执行
线程A开始执行
线程A执行完成
线程B收到通知,开始执行自己的业务...
线程B执行完成
二、使用 Object 类的 wait/notify
wait/notify 是基于 monitor 实,也就是 synchronized 内置锁。当调用 wait 方法时,线程进入 WaitSet 中;当被 notify 唤醒时,线程重新进入 EntryList,等待竞争锁。
public class WaitNotifyTest {
public static void main(String[] args) {
//定义一个对象
Object o = new Object();
// 定义线程A
Thread threadA = new Thread(() -> {
synchronized (o) {
System.out.println("线程A开始执行");
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程A继续执行");
}
});
// 定义线程B
Thread threadB = new Thread(() -> {
synchronized (o) {
System.out.println("线程B开始执行");
o.notify();
}
});
// 测试 先启动线程A,1s后再启动线程B
threadA.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadB.start();
}
}
由输出结果,在线程 A 发出 notify()
唤醒通知之后,依然是走完了自己线程的业务之后,线程 B 才开始执行,正好说明 notify()
不释放锁,而 wait()
释放锁。
执行结果打印:
线程A开始执行
线程B开始执行
线程A继续执行
三、使用JUC工具类 CountDownLatch
jdk1.5 之后在java.util.concurrent
包下提供了很多并发编程相关的工具类,简化了并发编程代码的书写,CountDownLatch
基于 AQS 框架,相当于也是维护了一个线程间共享变量 state。
public class CountDownLatchSync {
public static void main(String[] args) throws InterruptedException {
//定义一个线程池和CountDownLatch
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch countDownLatch = new CountDownLatch(10);
//定义线程A
Thread threadA = new Thread(() -> {
System.out.println("线程A开始执行");
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("countDownLatch count=" + countDownLatch.getCount());
System.out.println("线程A继续执行");
});
//测试 先启动线程A,再new 10个线程,countDownLatch--操作
threadA.start();
System.out.println("countDownLatch count=" + countDownLatch.getCount());
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
System.out.println("线程:" + Thread.currentThread().getName() + "开始执行,countDownLatch--");
countDownLatch.countDown();
});
}
}
}
执行结果打印:
countDownLatch count=10
线程A开始执行
线程:pool-1-thread-1开始执行,countDownLatch--
线程:pool-1-thread-2开始执行,countDownLatch--
线程:pool-1-thread-4开始执行,countDownLatch--
线程:pool-1-thread-5开始执行,countDownLatch--
线程:pool-1-thread-3开始执行,countDownLatch--
线程:pool-1-thread-6开始执行,countDownLatch--
线程:pool-1-thread-7开始执行,countDownLatch--
线程:pool-1-thread-8开始执行,countDownLatch--
线程:pool-1-thread-9开始执行,countDownLatch--
线程:pool-1-thread-10开始执行,countDownLatch--
countDownLatch count=0
线程A继续执行
四、使用 ReentrantLock 结合 Condition
public class ReentrankLockSync {
public static void main(String[] args) throws InterruptedException {
// 定义ReentrankLock和Condition
// Condition对象:让线程在合适的时间等待,或在某特定时刻得到通知,继续执行
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 定义线程A
Thread threadA = new Thread(() -> {
System.out.println("线程A开始执行," + DateUtil.now());
lock.lock();
System.out.println("线程A获得锁," + DateUtil.now());
try {
condition.await();
System.out.println("线程A继续执行," + DateUtil.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
System.out.println("线程A释放锁," + DateUtil.now());
});
// 测试
threadA.start();
System.out.println("主线程睡眠2s," + DateUtil.now());
Thread.sleep(2000);
lock.lock();
System.out.println("主线程获得锁,唤醒等待线程," + DateUtil.now());
condition.signal();
System.out.println("主线程睡眠3s后释放锁," + DateUtil.now());
Thread.sleep(3000);
lock.unlock();
}
}
这种方式使用起来并不是很好,代码编写复杂,而且线程 B 在被 A 唤醒之后由于没有获取锁还是不能立即执行,也就是说,A 在唤醒操作之后,并不释放锁。这种方法跟 Object 的 wait()/notify()
一样。
执行结果打印:
主线程睡眠2s,2022-06-30 11:35:05
线程A开始执行,2022-06-30 11:35:05
线程A获得锁,2022-06-30 11:35:05
主线程获得锁,唤醒等待线程,2022-06-30 11:35:07
主线程睡眠3s后释放锁,2022-06-30 11:35:07
线程A继续执行,2022-06-30 11:35:10
线程A释放锁,2022-06-30 11:35:10
五、基本 LockSupport 实现线程间的阻塞和唤醒
LockSupport
是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个!当调用 park 方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;
public class LockSupport2Sync {
public static void main(String[] args) throws InterruptedException {
// 定义线程A
Thread threadA = new Thread(() -> {
System.out.println("线程A开始执行,进入等待," + DateUtil.now());
LockSupport.park();
System.out.println("线程A继续执行,进入二次等待" + DateUtil.now());
LockSupport.park();
System.out.println("线程A继续执行," + DateUtil.now());
});
threadA.start();
System.out.println("主线程睡眠3s,生成一个凭证," + DateUtil.now());
Thread.sleep(3000);
LockSupport.unpark(threadA);
System.out.println("主线程睡眠3s,生成一个凭证," + DateUtil.now());
Thread.sleep(3000);
LockSupport.unpark(threadA);
}
}
执行结果打印:
主线程睡眠3s,生成一个凭证,2022-06-30 14:16:04
线程A开始执行,进入等待,2022-06-30 14:16:04
主线程睡眠3s,生成一个凭证,2022-06-30 14:16:07
线程A继续执行,进入二次等待2022-06-30 14:16:07
线程A继续执行,2022-06-30 14:16:10
评论区