1 线程间通信
线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析
场景:两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求用线程间通信
1.1 synchronized方法
// 第一步 创建资源类,定义属性和操作方法
class Share{
//初始值
private int number = 0;
// +1的方法
public synchronized void incr() throws InterruptedException {
// 判断
while(number != 0) {
this.wait();
}
// 干活
number++;
System.out.println(Thread.currentThread().getName() + " :: " + number);
//通知其他线程
this.notifyAll();
}
// -1的方法
public synchronized void decr() throws InterruptedException {
// 判断
while(number != 1) {
this.wait();
}
// 干活
number--;
System.out.println(Thread.currentThread().getName() + " :: " + number);
//通知其他线程
this.notifyAll();
}
}
public class ThreadDemo1 {
// 第三步创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
// 创建线程
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.incr();//+1
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"AA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"BB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.incr();//+1
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"CC").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"DD").start();
}
}
1.2 Lock方案
// 第一步 创建资源类,定义属性和操作方法
class Share{
//初始值
private int number = 0;
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// +1
public void incr() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (number != 0){
condition.await();
}
//干活
number++;
System.out.println(Thread.currentThread().getName() + " :: " + number);
//通知
condition.signalAll();
}finally {
// 解锁
lock.unlock();
}
}
// -1
public void decr() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (number != 1){
condition.await();
}
//干活
number--;
System.out.println(Thread.currentThread().getName() + " :: " + number);
//通知
condition.signalAll();
}finally {
// 解锁
lock.unlock();
}
}
}
public class ThreadDemo2 {
// 第三步创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"AA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"BB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"CC").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"DD").start();
}
}
1.3 虚假唤醒问题
在Java中,线程间通信通常使用 Object
类的 wait()
、notify()
和 notifyAll()
方法来实现。这些方法与条件变量类似,但同样可能会出现虚假唤醒(Spurious Wakeup)的问题。
1.3.1 什么是虚假唤醒?
虚假唤醒是指一个线程在没有被显式通知的情况下被唤醒。换句话说,即使没有调用 notify()
或 notifyAll()
方法,等待的线程也可能被唤醒。这种现象在某些操作系统或线程库中是允许的,因为它可以简化某些实现。
1.3.2 为什么会出现虚假唤醒?
虚假唤醒的原因可能包括:
- 操作系统调度:操作系统可能在某些情况下唤醒线程,即使没有显式的通知。
- 多核处理器:在多核处理器上,线程可能在不同的核心上运行,导致某些同步机制不完全可靠。
- Java 实现:Java 的线程库实现可能会允许虚假唤醒,以提高性能或简化实现。
1.3.3 如何处理虚假唤醒?
为了避免虚假唤醒带来的问题,通常的做法是在循环中检查条件变量。这样,即使线程被虚假唤醒,它也会在循环中重新检查条件,如果条件不满足,线程会继续等待。
以下是一个使用 wait()
和 notifyAll()
的典型示例:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SpuriousWakeupExample {
private static final Lock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
private static boolean ready = false;
public static void main(String[] args) throws InterruptedException {
Thread workerThread = new Thread(new Worker());
workerThread.start();
// 主线程设置条件并通知
Thread.sleep(1000); // 模拟一些工作
lock.lock();
try {
ready = true;
condition.signalAll(); // 通知等待的线程
} finally {
lock.unlock();
}
workerThread.join();
}
static class Worker implements Runnable {
@Override
public void run() {
lock.lock();
try {
// 在循环中检查条件
while (!ready) {
condition.await(); // 等待条件满足
}
// 条件满足,执行工作
System.out.println("Worker thread is processing data");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
}
1.3.4 关键点
- 循环检查条件:在
await()
方法周围使用while
循环来检查条件,而不是if
语句。这样可以确保即使线程被虚假唤醒,它也会重新检查条件。 - 使用
Lock
和Condition
:在示例中使用了ReentrantLock
和Condition
,这是 Java 中更灵活的同步机制。你也可以使用synchronized
关键字和Object
的wait()
、notify()
和notifyAll()
方法,但原理是相同的。 - 处理中断:在
await()
方法中捕获InterruptedException
,并处理线程中断的情况。
通过这种方式,可以有效避免虚假唤醒带来的问题,确保线程在条件真正满足时才继续执行。
1.4 多线程编程步骤
-
第一步创建资源类,在资源类创建属性和操作方法
-
第二步在资源类操作方法
- 判断
- 干活
- 通知
-
第三步创建多个线程,调用资源类的操作方法
-
第四步防止虚假唤醒问题
2 线程间定制化通信
让线程按照指定顺序进行通信。
2.1 案例介绍
启动三个线程,按照如下要求执行:
AA线程打印 5 次 A,BB 线程打印 10 次 B,CC 线程打印 15 次 C,按照此顺序循环 10 轮
2.2 流程分析
2.3 代码实现
// 第一步 创建资源类,定义属性和操作方法
class ShareResourse{
// 定义标识位
private int flag = 1; // 1:AA; 2:BB; 3:CC
//创建Lock
private Lock lock = new ReentrantLock();
//创建三个condition
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
//打印5次,参数第几轮
public void print5(int loop) throws InterruptedException {
//上锁
lock.lock();
try{
while (flag != 1){
//等待
c1.await();
}
//干活
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + i +", 轮数:"+ loop);
}
//通知
flag = 2; //修改标识位2
c2.signal();//通知BB线程
}finally {
// 释放锁
lock.unlock();
}
}
//打印10次,参数第几轮
public void print10(int loop) throws InterruptedException {
//上锁
lock.lock();
try{
while (flag != 2){
//等待
c2.await();
}
//干活
for (int i = 0; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + i +", 轮数:"+ loop);
}
//通知
flag = 3; //修改标识位3
c3.signal();//通知CC线程
}finally {
// 释放锁
lock.unlock();
}
}
//打印15次,参数第几轮
public void print15(int loop) throws InterruptedException {
//上锁
lock.lock();
try{
while (flag != 3){
//等待
c3.await();
}
//干活
for (int i = 0; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + i +", 轮数:"+ loop);
}
//通知
flag = 1; //修改标识位1
c1.signal();//通知AA线程
}finally {
// 释放锁
lock.unlock();
}
}
}
public class ThreadDemo3 {
// 第三步创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
ShareResourse shareResourse = new ShareResourse();
new Thread(()->{
for (int i = 0; i <= 10; i++) {
try {
shareResourse.print5(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"AA").start();
new Thread(()->{
for (int i = 0; i <= 10; i++) {
try {
shareResourse.print10(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"BB").start();
new Thread(()->{
for (int i = 0; i <= 10; i++) {
try {
shareResourse.print15(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"CC").start();
}
}
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的class对象。
对于同步方法块,锁是synchonized括号里配置的对象
3 思维导图
4 参考链接
【【尚硅谷】大厂必备技术之JUC并发编程】