本文代码示例已放入gitHub:请点击我
快速导航-------->src.main.java.yq.Thread.Communication
在昨天我们在-- java并发编程之内存模型&多线程三大特性 -- 之中说到了线程安全问题就是因为线程之间通讯引起的,并且分析了为什么会产生线程安全问题。那么今天就来说说通讯。
什么是线程之间通讯?
答:其实就是多个线程同时操作同一个资源,只是操作的动作不同而已。说白了就是线程一个生产,一个读取。
今天的目标:学会 wait ,notify ,Condition
接下来我们就使用代码实现使用多线程实现生产者和消费者的例子。
//测试多线程
@Data
public class MyThread {
//volatile可以解决从排序,和线程可见性
private volatile String userSex = null;
private volatile String userName = null;
//用于计数
private volatile Integer number = 0;
// //是否创建
// private volatile Boolean isFalg = false;
static class ProducerThread extends Thread {
private MyThread myThread;
public ProducerThread(MyThread myThread) {
this.myThread = myThread;
}
@Override
public void run() {
int index = 0;
while (true) {
try {
Thread.sleep(50);
if (index % 2 == 0) {
myThread.setUserName("小红");
myThread.setUserSex("女");
myThread.setNumber(index);
} else {
myThread.setUserName("张三");
myThread.setUserSex("男");
myThread.setNumber(index);
}
index++;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
static class ConsumerThread extends Thread {
private MyThread myThread;
public ConsumerThread(MyThread myThread) {
this.myThread = myThread;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(30);
System.out.println(Thread.currentThread().getName() + myThread.getUserName() + "--------" + myThread.getUserSex() + "---------" + myThread.getNumber());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
new ProducerThread(myThread).start();
new ConsumerThread(myThread).start();
}
}
通过上面的例子。我们创建了了一个消费者线程,和一个生产者线程,作用很简单,一个负责生产,一个负责消费,但是我们故意使用Thread.sleep()进行阻塞线程,而且生产者线程阻塞更久,用于模拟线程安全问题。
我们从运行结果上看出来了,确实产生了线程安全问题,那这个时候怎么办呢?有人说使用lock锁或者使用synchronized关键字,解决线程安全问题。
答案是不可以的,加锁使用场景是多个线程共享同一个变量的时候,一次只允许一个变量对功效变量进行操作,而我们这里只有一个生产者线程,也就是说根本就不会发生线程安全问题,拿有人会说,为什么我们这里发生了重复消费呢,这就是模拟的一个网络延迟的情况,确实生产者只生产了一个,但是在下次生产好之前,我们的消费者线程又去取了,所以才产生了重复读取,那么这种一对一,生产一个消费一个我们怎么解决呢?
1:使用wait,notify
//测试多线程
@Data
public class MyThread {
//volatile可以解决从排序,和线程可见性
private volatile String userSex = null;
private volatile String userName = null;
//用于计数
private volatile Integer number = 0;
//是否创建
private volatile Boolean isFalg = false;
static class ProducerThread extends Thread {
private MyThread myThread;
public ProducerThread(MyThread myThread) {
this.myThread = myThread;
}
@Override
public void run() {
int index = 0;
while (true) {
try {
synchronized (myThread) {
//如果已经生产好了,还没人消费,那么我们这个线程就等待,先不进行生产
if(myThread.getIsFalg()){
myThread.wait();
}
Thread.sleep(50);
if (index % 2 == 0) {
myThread.setUserName("小红");
myThread.setUserSex("女");
myThread.setNumber(index);
} else {
myThread.setUserName("张三");
myThread.setUserSex("男");
myThread.setNumber(index);
}
index++;
//表示我们已经生产好了
myThread.setIsFalg(true);
//放开阻塞的线程
myThread.notify();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
static class ConsumerThread extends Thread {
private MyThread myThread;
public ConsumerThread(MyThread myThread) {
this.myThread = myThread;
}
@Override
public void run() {
while (true) {
try {
synchronized (myThread) {
//如果还没有生产好,我们就阻塞当前线程
if(! myThread.getIsFalg()){
myThread.wait();
}
Thread.sleep(30);
System.out.println(Thread.currentThread().getName() + myThread.getUserName() +
"--------" + myThread.getUserSex() + "---------" + myThread.getNumber());
myThread.setIsFalg(false);
myThread.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
new ProducerThread(myThread).start();
new ConsumerThread(myThread).start();
}
}
同样是上面的例子,我们稍加改造,增加了一个标识,flag--->这个标识标识的是否生产好了一个实例,同样我们加入了wait和notify关键字,以及synchronized关键字,我们看看运行效果。
这个时候我们发现之前的重复读取的问题解决了,而且顺序很友好,一个女一个男,很现然已经达到了我们的预期效果,那么是为什么会达到这个效果呢?请看下面:
wati | 使前线程阻塞 |
notify | 放开当前阻塞的线程 |
notifyAll | 放开所有被阻塞的线程 |
到了这里我们就应该明白为什么要那么写了,就是如果生产线程已经生产好了实例,但是没有被消费线程消费,那么我们就阻塞生产线程不让其生产,那么消费者线程也是同理,如果有已经被生产好的实例,我们就消费,如果还没有实例被生产好的实例,我们就阻塞消费线程。另外,wait,notify必须结合synchronized来使用,而且要使用同一个锁对象,不然会抛出异常:IllegalMonitorStateException
这里的wait是阻塞当前线程,那么我们之前还有个东西,Thread.sleep(毫秒单位),同样也是阻塞当前线程,那么二者的区别是什么?
区别:
- sleep方法虽然会阻塞线程,让出CPU资源给其他线程执行,但是其还是在监管状态,到了时间又会自动执行。
- sleep是属于Thread的一个方法,只能使用Thread进行调用。
- wait是属于Object类的,所有的对象都可以调用wait方法,也就是说仍和对象都可以作为所对象。
- wait不会自动执行,必须结合notify进行使用。
2:使用Condition实现生产者消费者功能
那么我们开始改造代码
//测试多线程
@Data
public class MyThread {
//volatile可以解决从排序,和线程可见性
private volatile String userSex = null;
private volatile String userName = null;
//用于计数
private volatile Integer number = 0;
//是否创建
private volatile Boolean isFalg = false;
private Lock lock = new ReentrantLock();
static class ProducerThread extends Thread {
private Condition condition;
private MyThread myThread;
public ProducerThread(MyThread myThread, Condition condition) {
this.myThread = myThread;
this.condition = condition;
}
@Override
public void run() {
int index = 0;
while (true) {
try {
myThread.getLock().lock();
//如果已经生产好了,还没人消费,那么我们这个线程就等待,先不进行生产
if (myThread.getIsFalg()) {
condition.await();
}
Thread.sleep(50);
if (index % 2 == 0) {
myThread.setUserName("小红");
myThread.setUserSex("女");
myThread.setNumber(index);
} else {
myThread.setUserName("张三");
myThread.setUserSex("男");
myThread.setNumber(index);
}
++index;
//表示我们已经生产好了
myThread.setIsFalg(true);
//放开阻塞的线程
condition.signal();
} catch (Exception e) {
e.printStackTrace();
}finally {
myThread.getLock().unlock();
}
}
}
}
static class ConsumerThread extends Thread {
private Condition condition;
private MyThread myThread;
public ConsumerThread(MyThread myThread, Condition condition) {
this.myThread = myThread;
this.condition = condition;
}
@Override
public void run() {
while (true) {
try {
myThread.getLock().lock();
//如果还没有生产好,我们就阻塞当前线程
if (!myThread.getIsFalg()) {
condition.await();
}
Thread.sleep(30);
System.out.println(Thread.currentThread().getName() + myThread.getUserName() +
"--------" + myThread.getUserSex() + "---------" + myThread.getNumber());
myThread.setIsFalg(false);
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
myThread.getLock().unlock();
}
}
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
Condition condition = myThread.lock.newCondition();
new ProducerThread(myThread,condition).start();
new ConsumerThread(myThread,condition).start();
}
}
大家可以同样看到,使用Conidtion也同样达到了synchronized配合wait和notify的功能,另外Conidtion是属于lock锁的。
至于这样写的原因跟上面使用wait和notify的原理是一样的,就不过多解释为什么这样写了
名称 | 作用 |
void await() throws InterruptedException; | 使线程进行阻塞 |
void awaitUninterruptibly(); | 使线程进行阻塞,并且使用Thread.interrupt()不会报错 |
long awaitNanos(long nanosTimeout) throws InterruptedException; | 阻塞线程,并且可以指定阻塞的时间,到时间就算没有使用signal或者signalAll也会重新执行,类型为long类型 |
boolean await(long time, TimeUnit unit) throws InterruptedException; | 同上,但是可以指定时间类型 |
boolean awaitUntil(Date deadline) throws InterruptedException; | 同上,但是时间类型是Date类型 |
void signal(); | 唤醒被阻塞的线程 |
void signalAll(); | 唤醒所有被阻塞的线程 |
名称 | 作用 |
interrupt() | 在一个线程中调用另一个线程的interrupt()方法,即会向那个线程发出信号——线程中断状态已被设置。至于那个线程何去何从,由具体的代码实现决定。 |
isInterrupted() | 用来判断当前线程的中断状态(true or false)。 |
interrupted() | 用来恢复中断状态 |
如果想详细了解interrupt()请点击我:java中的interrupt使用
实现生产者消费者的两种方式:
- 使用 synchronized 加上wiat和notify实现。
- 使用 lock 配合Condition接口的await和signal实现。
- 两者的区别,就是Condition可以指定时间,到了时间就可以自动进行让线程进行自动执行。
到了这里我们基本就实现了生产者消费者的功能,也可以说是线程的通讯把,希望可以和大家共同进步,谢谢大家的阅读~~
本文代码示例已放入gitHub:请点击我
快速导航-------->src.main.java.yq.Thread.Communication
如果写的不对,还望大佬指出!