线程间的通信
线程间通信又称为进程内通信,多个线程实现互斥访问共享资源时会互相发送信号或等待信号,比如线程等待数据到来的通知,线程收到变量改变的信号等。
1 单线程间通信
1.1 初识 wait 和 notify
假设我们现在需要两个线程,一个负责生产数据,一个负责消费数据,理想状态下我们希望一边生产一边消费。
public class ProduceConsumerVersion {
private final Object LOCK = new Object();
private int i;
// 生产者
public void produce() {
synchronized (LOCK) {
System.out.println("P -> " + (i++));
}
}
// 消费者
public void consumer() {
synchronized (LOCK) {
System.out.println("C -> " + i);
}
}
public static void main(String[] args) {
ProduceConsumerVersion pc = new ProduceConsumerVersion();
new Thread(() -> {
while (true)
pc.produce();
}).start();
new Thread(() -> {
while (true)
pc.consumer();
}).start();
}
}
上面的代码运行以后,结果和我们想的并不是一样的。因为我们希望的是生产一个就消费一个。所以我们使用我们使用 wait 和 notify 重构代码。代码如下:
public class ProduceConsumerVersion2 {
private final Object LOCK = new Object();
private int i;
private boolean isProduced;
// 生产者
public void produce() {
synchronized (LOCK) {
// 如果已生产者数据等待消费者消费,否则生产数据
if (isProduced) {
try {
LOCK.wait();// 等待消费者消费
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
i++;// 生产数据
System.out.println("P -> " + i);
isProduced = true;
LOCK.notify();// 唤醒消费者进行消费
}
}
}
// 消费者
public void consumer() {
synchronized (LOCK) {
// 如果已生产者数据就消费数据,否则等待生产者生产数据
if (isProduced) {
System.out.println("C -> " + i);
isProduced = false;
LOCK.notify();// 唤醒生产者生产数据
} else {
try {
LOCK.wait();// 等待生产者生产数据
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ProduceConsumerVersion2 pc = new ProduceConsumerVersion2();
new Thread(() -> {
while (true)
pc.produce();
}).start();
new Thread(() -> {
while (true)
pc.consumer();
}).start();
}
}
生产者生产数据以后会通知消费者消费,并进入阻塞等待状态;消费者消费以后会唤醒 生产者生产数据,并进入阻塞等待状态,如此循环往复。该结果正是我们需要的。但如果是多个消费者和生产者的,上面的程序就会有问题。所以接下来讨论多线程通信问题。
2 多线程通信
消费者和生产者不变,我们改变线程个数进行调用。
public static void main(String[] args) {
ProduceConsumerVersion3 pc = new ProduceConsumerVersion3();
Stream.of("P1", "P2").forEach(p -> {
new Thread(() -> {
while (true)
pc.produce();
}).start();
});
Stream.of("C1", "C2").forEach(c -> {
new Thread(() -> {
while (true)
pc.consumer();
}).start();
});
}
该代码运行,会出现一直等待的问题。
2.1 问题分析:
当两个生产者和消费者都处于等待状态之后,notify就不知道去唤醒谁,从而导致他们一直在等待唤醒,从来导致程序出现假死状态。
2.2 认识notifyAll
通过 notifyAll 可以解决以上问题,我们只需要唤醒所有其他线程即可,然后等待 CPU 调度获得锁资源继续执行。
public class ProduceConsumerVersion4 {
private final Object LOCK = new Object();
private int i;
private boolean isProduced;
// 生产者
public void produce() {
synchronized (LOCK) {
// 如果已生产者数据等待消费者消费,否则生产数据
if (isProduced) {
try {
LOCK.wait();// 等待消费者消费
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
i++;// 生产数据
System.out.println(Thread.currentThread().getName() + " -> " + i);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isProduced = true;
LOCK.notifyAll();// 唤醒消费者进行消费
}
}
}
// 消费者
public void consumer() {
synchronized (LOCK) {
// 如果已生产者数据就消费数据,否则等待生产者生产数据
if (isProduced) {
System.out.println(Thread.currentThread().getName() + " -> " + i);
isProduced = false;
LOCK.notifyAll();// 唤醒生产者生产数据
} else {
try {
LOCK.wait();// 等待生产者生产数据
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ProduceConsumerVersion4 pc = new ProduceConsumerVersion4();
Stream.of("P1", "P2", "P3").forEach(p -> {
new Thread(p) {
@Override
public void run() {
while (true)
pc.produce();
}
}.start();
});
Stream.of("C1", "C2", "C3", "C4", "C5").forEach(c -> {
new Thread(c) {
@Override
public void run() {
while (true)
pc.consumer();
}
}.start();
});
}
}
运行以上代码以后,从结果可以看到之前的问题已经得到解决。
3 wait 和 sleep 的区别
- sleep 是属于 Thread 的方法,wait 属于 Object;
- sleep 方法不需要被唤醒,wait 需要;
- sleep 方法不需要 synchronized,wait 需要;
- sleep 不会释放锁,wait 会释放锁并将线程加入 wait 队列;
4 脏读
对于对象的同步和异步的方法,一定要考虑问题的整体,不然就会出现数据不一致的错误,很经典的错误就是脏读( dirtyread)示例代码如下:
/**
* 业务整体需要使用完整的 synchronized,保持业务的原子性。
*/
public class DirtyRead {
private String username = "bjsxt";
private String password = "123";
public synchronized void setValue(String username, String password) {
this.username = username;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.password = password;
System.out.println("setValue最终结果:username = " + username + " , password = " + password);
}
public synchronized void getValue() {
System.out.println("getValue方法得到:username = " + this.username + " , password = " + this.password);
}
public static void main(String[] args) {
final DirtyRead dr = new DirtyRead();
Thread t1 = new Thread(() -> dr.setValue("z3", "456"));
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
dr.getValue();
}
}
示例总结:
在我们对一个对象的方法加锁的时候,需要考虑业务的整体性,即为setValue/getValue 方法同时加锁 synchronized 同步关键字,保证业务(service)的原子性,不然会出现业务错误(也从侧面保证业务的一致性)。