目录
4. 线程间通信
线程间通信有两种实现方法:
关键字 synchronized 与 wait()、notify() 这两个方法一起使用可以实现等待/通知模式
Lock 接口中的 newContition() 方法返回 Condition 对象,Condition 类也可以实现等待/通知模式
用 notify()通知时,JVM 会随机唤醒某个等待的线程
使用 Condition 类可以进行选择性通知, Condition 比较常用的两个方法:
1. await() 会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行 2. signal() 用于唤醒一个等待的线程
操作线程的时候,等待线程使用wait()
通知另外的线程操作用notify()、notifyAll()
假设有两个线程,该线程在执行过程中,判断值(不是该值等待,让其他线程抢),操作值,通知另外一个线程的调度
4.1 synchronized 实现案例
实现两个线程对 num 这个值操作,一个线程加1,一个线程减1,交替实现多次
// 创建一个资源类
class Share{
// 设置临界资源
private int number = 0;
// 实现+1操作
public synchronized void incr() throws InterruptedException {
// 操作:判断、干活、通知
if (number != 0) {
// number不为0,等待
// wait 有一个特点,在哪里睡,就在哪里醒
this.wait();
}
number++;
System.out.print(Thread.currentThread().getName()+"::"+number);
// 唤醒其他线程
// 注意这里的通知是随机的,就是只能通知全部
this.notifyAll();
}
// 实现-1操作
public synchronized void decr() throws InterruptedException {
// 操作:判断、干活、通知
if (number != 1) {
// number不为0,等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
this.notifyAll();
}
}
public class InterThreadCommunication {
public static void main(String[] args) {
Share share = new Share();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
4.2 虚假唤醒问题
虚假唤醒主要出现在多线程中出现。
同样使用上述案例,现在有四个线程,分别为A,B,C,D,其中A,C线程做+1操作,B,D线程做-1操作,想要的结尾应该是A,C线程输出值为1,B,D线程输出值为0 。修改上述代码如下:
// 创建一个资源类
class Share{
// 设置临界资源
private int number = 0;
// 实现+1操作
public synchronized void incr() throws InterruptedException {
// 操作:判断、干活、通知
if (number != 0) {
// number不为0,等待
this.wait();
}
number++;
System.out.print(Thread.currentThread().getName()+"::"+number+"--->");
// 唤醒其他线程
this.notifyAll();
}
// 实现-1操作
public synchronized void decr() throws InterruptedException {
// 操作:判断、干活、通知
if (number != 1) {
// number不为0,等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
this.notifyAll();
}
}
public class InterThreadCommunication {
public static void main(String[] args) {
Share share = new Share();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
防止虚假唤醒
//需要一下修改 if(){ this.wait(); } // 将 if 改成 while while(){ this.wait(); }
查找 JDK1.8 文档,在 Object 的 wait() 方法中有如下介绍
在一个参数版本中,中断和虚假唤醒是可能的,并且该方法应该始终在循环中使用
也就是说,这种现象叫做【虚假唤醒】。所谓虚假唤醒,就是 wait()方法的一个特点,总结来说 wait() 方法使线程在哪里睡就在哪里醒。 这是什么意思呢?那就以上述代码为例。
当 A 进入临界区,BCD三个线程在 if 判断后进入 wait() 等待,当A线程完成操作,此时 number 值为1,notifyAll() 会随机唤醒一个线程。
现在C被唤醒,由于 wait() 方法使线程在哪里睡就在哪里醒,所以接下来C在执行时不会再通过 if 判断而是直接+1,此时 number 就是2了。从而导致最后输出的结果和我们预想的不一致。
4.3 Lock 实现案例
在 Lock 接口中,有一个 newCondition() 方法,该方法返回一个新 Condition 绑定到该实例 Lock 实例。
Condition 类中有 await() 和 signalAll() 等方法,和 synchronized 实现案例中的 wait() 和 notifyAll() 方法相同。所以通过 Lock 接口创建一个 Condition 对象,由该对象的方法进行等待和唤醒操作
实例代码如下,主要改动的是资源类,main方法中代码不变。
class Share {
// 设置临界资源
private int number = 0;
// 创建一个Com
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// 实现+1操作
public void incr() {
// 上锁
lock.lock();
try {
// 判断
while (number != 0) {
condition.await();
}
// 干活
number++;
System.out.print(Thread.currentThread().getName() + "::" + number + "--->");
// 通知
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} 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();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
5. 线程间定制化通信
案例实现
案列:启动三个线程,按照如下要求:
AA打印5此,BB打印10次,CC打印15次,一共进行10轮
具体思路:
每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志 位的线程
创建一个可重入锁 private Lock lock = new ReentrantLock();
分别创建三个开锁通知 private Condition c1 = lock.newCondition();(他们能实现指定唤醒)
(注意)具体资源类中的A线程代码操作
上锁,(执行具体操作(判断、操作、通知),解锁)放于try、finally,具体代码如下
class Share{
private int flag = 1;
private Lock lock = new ReentrantLock();
// 创建三个Comdition对象,为了定向唤醒相乘
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
public void Aprint(int loop) {
//上锁
lock.lock();
try{
// 判断
while(flag!=1) {
c1.await();
}
// 干活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
}
flag = 2; //修改标志位,定向唤醒 线程b
// 唤醒
c2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
public void Bprint(int loop) {
//上锁
lock.lock();
try{
// 判断
while(flag!=2) {
c2.await();
}
// 干活
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
}
flag = 3; //修改标志位,定向唤醒 线程b
// 唤醒
c3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
public void Cprint(int loop) {
//上锁
lock.lock();
try{
// 判断
while(flag!=3) {
c3.await();
}
// 干活
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
}
flag = 1; //修改标志位,定向唤醒 线程b
// 唤醒
c1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
}
public class CustomInterThreadCommunication {
public static void main(String[] args) {
Share share = new Share();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
share.Aprint(i);
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
share.Bprint(i);
}
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
share.Cprint(i);
}
}
},"C").start();
}
}
该案例需要注意
我们在学习操作系统中的同步可以知道,进程/线程同步有四个原则,都是为了禁止两个进程同时进入临界区。同步机制应该遵循以下原则
- 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区
- 忙则等待:当已经有进程进入临界区的时候,其他试图进入临界区的进程必须等待
- 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区
- 让权等待:当进程不能进入临界区的时候,应立即释放处理机,防止进程忙等待
很显然,该案例被称为单标志法。因为该案例设置一个公用整型变量flag,用于指示被允许进入临界区的进程编号。
单标志法伪代码如下
//P_0进程
while(turn!=0);
critical section;
turn=1;
remainder section;
//P_1进程
while(turn!=1);
critical section;
turn=0;
remainder section;