本篇文章讲述了 “Java 多线程的通信机制”,阅读时长大约为:10 分钟
一、引言
“Java 多线程中的等待与通知机制” 是一种线程间通信方式,用来协调线程的执行顺序和资源共享。通过这样子的机制,线程可以避免忙等待,提高资源利用率和程序执行效率。
二、Java 多线程中的通信机制概述
2.1、Java 多线程通信的核心问题
核心问题:多线程情况下,线程之间可能会共享一些数据(比如缓冲区中的数据),此时就会出现协调和数据一致性的问题。主要的两个核心问题如下:
- 数据不一致:当多个线程操作一个共享变量时,一个线程在更改该数据,另一个线程在读该数据,那么读到的数据可能是不一致的。
- 线程协调:线程需要在特定的时机执行,保证操作顺序的时候,线程执行的时机就尤为重要。比如:“消费者-生产者” 模型中,消费者需要在非空(有资源)的情况下消费,生产者需要在非满(没有资源)的情况下生产。这个时候就需要有一个协调的机制了。
Java 中,实现这种机制的方式主要有以下几种:
- synchronized 关键字以及 wait()、notify()、notifyAll() 方法
- synchronized:独占资源,是一个独占锁。
- wait()、notify()、notifyAll():实现线程的挂起和唤醒。
- 操作简单方便,但是不太灵活(加锁粒度太大)。
- ReentrantLock 和 Condition 条件变量
- ReentrantLock 相比 synchronized:更加灵活(粒度更细)。
- Condition:可以创建多个条件变量,灵活的控制唤醒哪一个线程,而不需要涉及到全部的线程。
- ReentrantReadWriteLock 读写锁
- ReentrantReadWriteLock:读写锁,适用于读写分离的场景(读多写少)。
- ReadLock 读锁:是共享锁(即读锁 和 读锁不互斥,能够一起读)。
- WriteLock 写锁:是独占锁(写锁与任何锁都互斥),如果有线程拿到了写锁,那么其他线程都不能读和写。
2.2、Java 中对象监视器的概念
在我们学习本篇内容之前,需要对 对象监视器 有一个大致的概念。
对象监视器(Object Monitor)是 Java 实现线程间通信的重要机制。每一个 java 对象在 JVM 中都隐式的内置一个监视器。该监视器用来表示对象的锁状态,实现线程对资源的独占,还实现了线程的睡眠和唤醒的机制。
对象监视器主要有两个特点:
- 互斥锁(Mutex Lock):
- 当线程进入了 synchronized 的方法或者代码块后,监视器会加锁,其他的线程想要进入该 synchronized 代码块就需要等待锁释放。
- 该锁是独占锁,确保同一时间只有一个线程执行该段代码。
- 唤醒和等待队列:
- 监视器内部会有一个等待队列,当调用了 wait() 方法之后,线程会进入挂起状态,进入到等待队列中进行等待,
- 调用可以通过调用 notify()、notifyAll() 来唤醒等待队列中的线程。notify():随机唤醒等待队列中的某一个线程,notifyAll():唤醒等待队列中的所有线程。
三、Java 实现多线程通信的方法
3.1、synchronized 和 wait()、notify()、notifyAll()
相关方法:
- synchronized:用于独占资源。
- wait():用于线程的睡眠,进入等待队列。
- notify():用于唤醒等待队列中的某一个线程。
- notifyAll():用于唤醒等待队列中的全部线程。
代码示例:wait()、notify()
public static void testWaitAndNotify() throws Throwable {
Thread t0 = new Thread(() -> {
synchronized (obj) {
String threadName = Thread.currentThread().getName();
System.out.println("当前线程: " + threadName + " 抢到了锁...");
try {
System.out.println("当前线程: " + threadName + " 准备睡眠...");
obj.wait(); // 进入 WAITING 状态,同时释放锁
System.out.println(threadName + " 苏醒了, 并且 " + threadName + " 线程任务执行完毕...");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t1 = new Thread(() -> {
synchronized (obj) {
String threadName = Thread.currentThread().getName();
System.out.println("当前线程: " + threadName + " 抢到了锁...");
try {
Thread.sleep(1000); // 模拟任务过程
System.out.println(threadName + " 尝试去唤醒之前的线程...");
obj.notify(); // 唤醒线程
System.out.println(threadName + " 线程任务执行完毕, 准备退出 synchronized 代码块...");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
t0.start();
Thread.sleep(50); // 确保 t0 先开始执行
t1.start();
// 等待任务执行完毕
t0.join();
t1.join();
System.out.println("所有线程执行完毕");
}
输出:
注意,Thread-1 线程去唤醒其他线程的时候,会继续往下执行自己原先的代码,执行完后释放锁让 Thread-0 去抢锁。
notifyAll() 代码示例
public static void testWaitAndNotifyAll() throws Throwable {
// 模拟三个准备睡眠的任务
for (int i = 0; i < 3; i++) {
Thread waiter = new Thread(() -> {
synchronized (obj) {
String threadName = Thread.currentThread().getName();
System.out.println("线程: " + threadName + " 得到了锁, 进入 WAITING 状态");
try {
Thread.sleep(100); // 模拟工作任务
obj.wait(); // 进入睡眠
System.out.println(threadName + " 苏醒, 任务执行完毕...");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
waiter.start();
}
Thread.sleep(800); // 确保上述线程进入 WAITING 状态
// 准备唤醒所有线程的任务
Thread notifier = new Thread(() -> {
synchronized (obj) {
String threadName = Thread.currentThread().getName();
System.out.println("唤醒者线程得到了锁, 准备唤醒所有线程");
obj.notifyAll(); // 唤醒所有线程,但是不会马上释放锁,会先继续执行当前任务
System.out.println("唤醒者线程唤醒了所有的线程, 并且释放了锁");
}
});
notifier.start();
notifier.join(); // 阻塞等待线程执行完毕
}
输出:
3.2、ReentrantLock 和 Condition
ReentrantLock 相比于 synchronized ,它的加锁粒度更加细,配合 Condition 条件变量,可以灵活的控制唤醒、挂起线程。
代码示例(以 “生产者-消费者” 模型为例)
public static void testConditions() throws Throwable {
// 生产者
Thread producer = new Thread(() -> {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
while(count >= 5) {
System.out.println("{producer} 已生产满, 等待consumer唤醒");
notFull.await(); // 进入睡眠, 等待consumer消费
}
++ count;
System.out.println("{producer} 生产资源, 当前资源量为: " + count);
notEmpty.signal(); // 唤醒consumer,告知可以开始消费资源
Thread.sleep(20);
}
} catch (Exception e) {
throw new RuntimeException();
} finally {
lock.unlock();
}
});
producer.start();
Thread.sleep(500);
// 消费者
Thread consumer = new Thread(() -> {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
while(count <= 0) {
System.out.println("{consumer} 已经消费完, 等待producer唤醒");
notEmpty.await(); // 进入睡眠, 等待生产出新的资源
}
-- count;
System.out.println("{consumer} 消费资源, 当前资源量为: " + count);
notFull.signal(); // 唤醒producer, 告知可以开始生产资源了
Thread.sleep(20);
}
} catch (Exception e) {
throw new RuntimeException();
} finally {
lock.unlock();
}
});
consumer.start();
// 阻塞线程, 保证线程任务执行完毕
consumer.join();
producer.join();
Thread.sleep(500);
System.out.println("所有任务执行完毕");
}
输出:
注意,我们上面创建了多个 Condition 条件变量(notFull、notEmpty)用来表示 非空、非满 状态。
流程解析:
lock.lock():用来占用锁,表示独占锁,我们只允许同一时间消费或者生产。
当我们生产者生产满了之后,调用 notFull.await() 进入睡眠状态,等待消费者消费。
当我们消费者消费完之后,调用 notEmpty.await() 进入睡眠状态,等待生产者生产。
notFull.signal():类似于 notify(),用来唤醒生产者生产资源。
notEmpty.signal():类似于 notify(),用来唤醒消费者消费资源。
3.3、ReentrantReadWriteLock 读写锁
ReentrantReadWriteLock 用来表示读写锁。
相关方法:
- readLock():得到读锁对象
- readLock().lock():尝试得到读锁
- writeLock():得到写锁对象
- writeLock().lock():尝试得到写锁
读写锁代码示例
public static void testReadWriteLock() throws Throwable {
// 创建两个读线程
for (int i = 0; i < 2; i++) {
Thread reader = new Thread(() -> {
try {
String threadName = Thread.currentThread().getName();
readWriteLock.readLock().lock(); // 获取读锁
System.out.println(threadName + " 得到了读锁...");
Thread.sleep(100);
System.out.println("线程: " + threadName + " 读到了数据: " + sharedValue);
Thread.sleep(1000);
System.out.println(threadName + " 释放了读锁...");
} catch (Exception e) {
throw new RuntimeException();
} finally {
readWriteLock.readLock().unlock(); // 释放读锁
}
});
reader.start();
}
Thread.sleep(1000); // 等待两个线程读
// 写线程
Thread writer = new Thread(() -> {
try {
String threadName = Thread.currentThread().getName();
readWriteLock.writeLock().lock(); // 获取写锁
Thread.sleep(100);
System.out.println("线程: " + threadName + " 更改了数据: " + sharedValue + " ==> " + (sharedValue += "hahaha"));
} catch (Exception e) {
throw new RuntimeException();
} finally {
readWriteLock.writeLock().unlock(); // 释放写锁
}
});
writer.start();
System.out.println("写锁线程 start 了");
writer.join();
System.out.println("所有程序执行完毕");
}
但是上面有一些我们需要注意的点:
比如说, readLock() 和 writeLock() 只是的到我们的锁对象,并不是尝试去获取到的锁。
此外,读锁是共享的,也就是说多个线程可以同时持有读锁。写锁是独占的,也就是说只要有一个线程得到了写锁,那么其他线程不管是尝试获得写锁还是读锁都是不可以的(写锁是独占锁),同理,只要有任何一个线程拥有了读锁(没有释放),那么就不能申请到写锁。写锁和读锁是互斥的。
四、总结
Synchronized:
优点:
- 隐式锁:避免了手动加锁和释放锁。
- 可重入锁:避免了死锁。
- 简单易用:实现起来较为简单方便。
缺点:
- 不够灵活:加锁粒度过大。
- 效率过低:相较于显示锁,synchronized 效率过低。
- 不可中断:其他处于 BLOCKED 状态中的线程必须要等待锁的释放才可以被中断。
ReentrantLock 和 Condition
优点:
- 可重入锁:避免了死锁。
- 粒度更细:可以通过多个 Condition 来实现对不同的线程的通信机制,更加灵活。
- 较为丰富的方法:比如获取正在等待队列中的线程的数量。
- 可以响应中断:可以中断正在 BLOCKED 状态中的线程。
- 可以实现公平锁(创建对象的时候传入 true 参数):避免线程饥饿。
缺点:
- 使用较为繁琐:需要手动释放锁,比如在 finally 中手动调用 unlock() 方法
ReentrantReadWriteLock 读写锁
优点:
- 可重入锁:避免了死锁。
- 读取效率高:读锁可以共享,提高读取数据的并发量。
- 可以响应中断:处于 BLOCKED 状态中的线程可以被中断。
- 可以实现公平锁(创建对象的时候传入true参数):避免线程饥饿。
缺点:
- 使用较为繁琐:相较于 synchronized,需要手动设置 读锁、写锁 的加锁和释放锁。
彦祖,都看到这里了还不点个赞吗
标签:Java,Thread,threadName,通信,System,线程,println,多线程,out From: https://blog.csdn.net/2401_82656016/article/details/143407178