线程间通信模型:
- 生产者+消费者
- 通知等待唤醒机制
多线程编程模板(上):
- 线程 操作 资源类
- 高内聚 低耦合
多线程编程模板(中):
- 判断
- 干活
- 通知
多线程编程模板(下):
- 防止虚假唤醒(使用while,代替if)
简单案例:两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮。
使用通知等待机制wait-notify
,来完成线程间的通信。
class ShareDateOne {
private Integer number = 0;
public synchronized void increment() {
try {
// 1. 判断:是否轮到我执行,则干活;否则等待
if(number != 0) {
this.wait();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + " 执行加法 " + number);
// 3. 通知:唤醒等待的线程
this.notifyAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public synchronized void decrement() {
try {
// 1. 判断:是否轮到我执行,则干活;否则等待
if(number != 1) {
this.wait();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + " 执行减法 " + number);
// 3. 通知:唤醒等待的线程
this.notifyAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public class NotifyWaitDemo {
public static void main(String[] args) {
ShareDateOne shareDateOne = new ShareDateOne();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDateOne.increment();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDateOne.decrement();
}
}).start();
}
}
- 那如果变多几个线程呢?
3.1 虚假唤醒
【变成 4 个线程,两个加法两个减法】虚假唤醒错误
原因:在Java
多线程判断时,不能用if
,错误出在了判断if
上面。
注意:消费者被唤醒后是从wait()
方法(被阻塞的地方)后面执行,而不是重新从同步块开头;两个加法或者减法,同时运行所以原本的0 -> -1 1 -> 2
改变加法减法顺序,更是如此,甚至会其他的-2
现象 2
什么的
class ShareDateOne {
private Integer number = 0;
public synchronized void increment() {
try {
// 1. 判断:是否轮到我执行,则干活;否则等待
if(number != 0) {
this.wait();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + " 执行加法 " + number);
// 3. 通知:唤醒等待的线程
this.notifyAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public synchronized void decrement() {
try {
// 1. 判断:是否轮到我执行,则干活;否则等待
if(number != 1) {
this.wait();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + " 执行减法 " + number);
// 3. 通知:唤醒等待的线程
this.notifyAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public class NotifyWaitDemo {
public static void main(String[] args) {
ShareDateOne shareDateOne = new ShareDateOne();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDateOne.increment();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDateOne.increment();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDateOne.decrement();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDateOne.decrement();
}
}).start();
}
}
中断和虚假唤醒是可能产生的,所以要用循环,if 只判断一次,while是只要唤醒就要重新再判断一次。
多线程编程模板(下):
- 防止虚假唤醒(使用while,代替if)
if 改成 while
,因为通知是所有的线程,可能就唤醒了是哪一个,它上一个线程 if 判断的时候,在这wait这儿了。现在唤醒了,可能又一个线程也正好一起了,所以就会这样出现这样,要么是 2 要么是 -2,这种是虚拟唤醒。
while 不会这样,即使是卡在 while 那,还需要重新判断,所以不满足就不会出现这样的情况。
两个线程无所谓进行争抢,确实 if/while
无所谓,但是未来可能是多线程,通知等待唤醒,使用 while,防止虚假唤醒。
修改为 while,次数进行 100次
无论怎样,也不会出错了!
// 1. 判断:是否轮到我执行,则干活;否则等待
while (number != 1) {
this.wait();
}
3.2 线程间通信
- Synchronized 有 wait和notify,那 lock 有什么呢?
Condition 接口中对应的方法:
lock.newCondition() 的 await 和 signal
class ShareDateTwo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Integer number = 0;
public void increment() {
lock.lock();
try {
// 1. 判断:是否轮到我执行,则干活;否则等待
while(number != 0) {
condition.await();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + " 执行加法 " + number);
// 3. 通知:唤醒等待的线程
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
// 1. 判断:是否轮到我执行,则干活;否则等待
while (number != 1) {
condition.await();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + " 执行减法 " + number);
// 3. 通知:唤醒等待的线程
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
public class NotifyWaitDemo2 {
public static void main(String[] args) {
ShareDateTwo shareDateTwo = new ShareDateTwo();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDateTwo.increment();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDateTwo.decrement();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDateTwo.increment();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDateTwo.decrement();
}
}).start();
}
}
3.3 小结
- 隐式锁
synchronized
object.wait()
object.notify() //随机唤醒一个等待的线程
object.notifyAll() //唤醒所有等待的线程
- 显示锁
注意:这里创建 condition
,从前往后写才可以!
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition()
condition.await()
condition.signal() //随机唤醒一个等待的线程
condition.signalAll() //唤醒所有等待的线程
他们不要交叉使用,交叉使用await + notify
这样也是会报错的!
3.4 定制化通信方式
- 那既然
Sync
和lock
都能实现线程间同行,有啥区别呢?
在Java中,一个ReentrantLock
可以与多个Condition
对象一起使用,每个Condition
对象可以用于不同的线程协调和通信场景,以便更精细地控制多线程之间的执行顺序和互斥访问。
说白了,就是lock.condition
可以控制下一个让谁醒,那就决定了执行顺序,这样更加强大。
【案例】
多线程之间按顺序调用,实现AA->BB->CC。三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
接着
AA打印5次,BB打印10次,CC打印15次
。。。打印10轮
【分析实现方式】
- 有一个锁Lock,3把钥匙Condition
- 有顺序通知(切换线程),需要有标识位
- 判断标志位
- 输出线程名 + 内容
- 修改标识符,通知下一个
class ShareDataTwo {
private Integer flag = 1; // 线程标识类 区分线程切换
private final Lock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private final Condition condition3 = lock.newCondition();
public void print5() {
lock.lock();
try {
while(flag != 1) {
// 等待
condition1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println("AA");
}
// 通知并且唤醒
flag = 2;
condition2.signal(); // 唤醒 B
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
// 等待
while (flag != 2) {
condition2.await();
}
// 干活
for (int i = 0; i < 10; i++) {
System.out.println("BB");
}
// 通知唤醒
flag = 3;
condition3.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
// 等待
while(flag != 3) {
condition3.await();
}
// 干活
for (int i = 0; i < 15; i++) {
System.out.println("CC");
}
// 通知唤醒
flag = 1;
condition1.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
public class ThreadOrder {
public static void main(String[] args) {
// 线程操作资源类
ShareDataTwo shareDataTwo = new ShareDataTwo();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDataTwo.print5();
}
},"AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDataTwo.print10();
}
},"BB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareDataTwo.print15();
}
},"CC").start();
}
}
面试题:两个线程,一个线程打印1-52,另一个打印字母A-Z,打印顺序为12A34B...5152Z,要求用线程间通信
- 首先分析轮次,每个线程需要打印多少轮次
- 每个线程打印出来结果,需要什么
变量
等 - 进行编写
class PrintNumAndChar {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private Integer flag = 1;
private Integer number = 1;
private Character character = 'A';
public void printNumber() {
lock.lock();
try {
// 判断
while(flag != 1) {
condition1.await();
}
// 干活
for(int i = 0;i < 2;i++) {
System.out.print(number);
number++;
}
// 唤醒
flag = 2;
condition2.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public void printCharacter() {
lock.lock();
try {
while(flag != 2) {
condition2.await();
}
System.out.print(character);
character++;
flag = 1;
condition1.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
public class CustomComm {
public static void main(String[] args) {
PrintNumAndChar printNumAndChar = new PrintNumAndChar();
new Thread(() -> {
for (int i = 0; i < 26; i++) {
printNumAndChar.printNumber();
}
},"线程-1").start();
new Thread(() -> {
for (int i = 0; i < 26; i++) {
printNumAndChar.printCharacter();
}
},"线程-1").start();
}
}