线程之间的通信

时间:2022-10-05 14:00:32

一、为什么要线程通信?

 

 1. 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,

   并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据

2.当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,

  但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失!

3.所以,我们才引出了线程之间的通信,​​多线程之间的通信能够避免对同一共享变量的争夺。​

二、什么是线程通信?

 

  多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作

     就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺

  于是我们引出了等待唤醒机制:(wait()notify()

  就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify);

(1)wait()方法:

   在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待

    线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行。

    要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中。

(2)notif()方法:

 notify()方法会唤醒一个等待当前对象的锁的线程。唤醒在此对象监视器上等待的单个线程。
(3)notifAll()方法:
  notifyAll()方法会唤醒在此对象监视器上等待的所有线程。   
(4)如果多个线程在等待,它们中的一个将会选择被唤醒。这种选择是随意的,和具体实现有关。(线程等待一个对象的锁是由于调用了wait方法中的一个)

 notify()方法应该是被拥有对象的锁的线程所调用。

(5)以上方法都定义在类:Object中

1.因为,这些方法在操作同步中的线程的时候,都必须标示其所操作线程所持有的锁(被该锁的对象调用),

而只有同一个对象监视器下(同一个锁上)的被等待线程,可以被持有该锁的线程唤醒,(无法唤醒不同锁上的线程)

2.所以,等待和唤醒的必须是同一个对象的监视器①下(同一个锁上)的线程。
而锁可以是任意已近确定的对象, 能被任意对象调用的方法应当定义在 Object类中。

注:①监视器(锁):同一个对象的监视器下(同一个锁上)的线程,一次只能执行一个:就是拥有监视器所有权(持有锁)的那一个线程。

三、实现线程通信的小demo


 如大众所说,体现线程通信的鲜明的例子:生产者---消费者是最明显的例子之一。那我们接下来敲一个关于生产者和消费者的简单的小demo,来演示一下线程的通信:

 

 (1)面包的类(生产和消费方法都在里面哦)

/**
*@functon 线程通信之面包类
*@author 温煦(昵称:沉沦之巅)
*@time 2017.12.5
*/

package ThreadMessage;

public class Breads {

//面包的id
private int bid;
//面包的个数
private int num;

//生产面包的方法(由于是demo,方便大家理解,就把synchronized关键字加到方法上面了哦)
public synchronized void produc(){

//当面包的数量不为0时,该方法处于等待状态
if(0 != num){
try {
wait();//等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当面包数量为0时,那么就开始生产面包了哦
num = num +1;//数量加1
bid = bid + 1 ;//id当然也得加1
String threadname = Thread.currentThread().getName();
System.out.println(threadname+"生产了一个编号为"+bid+"的面包!");
notify();//当执行完后,去唤醒其他处于等待的线程
}
//消费面包的方法
public synchronized void consume(){
//当面包的数量为0时,该方法处于等待状态
if(num == 0 ){
try {
wait();//等待
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//消费完面包了,所以面包数量降为0了
num = num -1;//数量减1
String name1 = Thread.currentThread().getName();
System.out.println(name1+"买了一个面包编号为"+bid);
notify();//当执行完后,去唤醒其他处于等待的线程
}


//set和get方法
public int getBid() {
return bid;
}

public void setBid(int bid) {
this.bid = bid;
}

public int getNum() {
return num;
}

public void setNum(int num) {
this.num = num;
}

//有参构造
public Breads(int bid, int num) {
super();
this.bid = bid;
this.num = num;
}

//无参构造
public Breads() {
super();
// TODO Auto-generated constructor stub
}
}

 

(2)生产面包的类

/**
*@functon 线程通信之生产类(继承Thread类)
*@author 温煦(昵称:沉沦之巅)
*@time 2017.12.5
*/

package ThreadMessage;

public class producer extends Thread{

//获得面包的类
private Breads bre ;

//无参构造
public producer() {
super();
}

//有参构造
public producer(Breads bre) {
super();
this.bre = bre;
}


//set和get方法
public Breads getBre() {
return bre;
}

public void setBre(Breads bre) {
this.bre = bre;
}

//继承重写run方法
@Override
public void run() {
pro();
}

//生产面包
private void pro() {
// 本系统默认循环生产20个面包
for (int i = 0; i <20; i++) {
try {
//沉睡0.3秒(演示效果需要,可以不加)
Thread.currentThread().sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用面包类里的生产面包的方法
bre.produc();
}
}
}

   (3)消费面包的类

/**
*@functon 线程通信之消费类(继承Thread类)
*@author 温煦(昵称:沉沦之巅)
*@time 2017.12.5
*/

package ThreadMessage;

public class consume extends Thread{

//获得面包的类
private Breads bre ;

//set和get方法
public Breads getBre() {
return bre;
}
public void setBre(Breads bre) {
this.bre = bre;
}

//继承重写run方法
@Override
public void run() {
con();
}

//消费面包
private void con() {
// 与生产者保持一致,本系统默认循环生产20个面包(生产几个,消费几个)
for(int i = 0;i<20;i++){
try {
//沉睡0.3秒(演示效果需要,可以不加)
Thread.currentThread().sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用面包类里的生产面包的方法
bre.consume();
}
}

//有参构造
public consume(Breads bre) {
super();
this.bre = bre;
}

//无参构造
public consume() {
super();
}
}

 

 (4)测试类

/**
*@functon 线程通信之测试类
*@author 温煦(昵称:沉沦之巅)
*@time 2017.12.5
*/

package ThreadMessage;

public class TestBreads {

public static void main(String[] args) {

//new一个面包类
Breads bre = new Breads();

//new一个生产者类
producer proth = new producer(bre);
//new一个消费者类
consume conth = new consume(bre);

//new一个包含消费者类的线程
Thread t1 = new Thread(proth,"生产者");

//new一个包含生产者类的线程
Thread t2 = new Thread(conth,"消费者");

//启动线程
t1.start();
t2.start();

}
}

 

   (5)演示效果(因数据太多,中间就省略了哦)

生产者生产了一个编号为1的面包!
消费者买了一个面包编号为1
生产者生产了一个编号为2的面包!
消费者买了一个面包编号为2
生产者生产了一个编号为3的面包!
消费者买了一个面包编号为3

...
...

 生产者生产了一个编号为17的面包!
 消费者买了一个面包编号为17
 生产者生产了一个编号为18的面包!
 消费者买了一个面包编号为18
 生产者生产了一个编号为19的面包!
 消费者买了一个面包编号为19
 生产者生产了一个编号为20的面包!
 消费者买了一个面包编号为20

 

   如上所示,案例表明生产者消费者之间就是运用了wait()和notify()这两个方法,通过通信的沉睡与唤醒机制来完成两个不同线程操作统一数据之间的通信,

当生产者生产出一个面包时,就会陷入沉睡,随后立即去唤醒消费者,就像是对他说,你去买吧,我生产好了,然后消费者就会屁颠屁颠的去买了那个面包,当他吃完那个面包后,

也会陷入沉睡,随后立即唤醒生产者,就像是对他说,你再生产一个吧,我吃完了,然后生产者就... ... 如此循环,周而复始,直到for循环结束为止。


四、多个生产者和消费者

  当我们创建多个生产者和消费者时,上述的代码就会出现一个问题,就是他们无法直到到底要唤醒哪一个,所以这时候我们就用到了​​notifAll()方法。​

  那么代码就不一一再发了,与上面的差不多,唯一不一样的就是在面包类里的if换成while循环判断,notif()换成notifAll()。

  然后再给大家写一下多个生产者和消费者的测试类:

  (1)多个生产者和消费者的测试类

/**
*@functon 线程通信之测试类
*@author 温煦(昵称:沉沦之巅)
*@time 2017.12.5
*/

package ThreadMessage;

public class TestBreads {

public static void main(String[] args) {

//new一个面包类
Breads bre = new Breads();

//new一个生产者类
producer proth = new producer(bre);
//new一个消费者类
consume conth = new consume(bre);

//new三个包含消费者类的线程
Thread t1 = new Thread(proth,"生产者1");
Thread t3 = new Thread(proth,"生产者2");
Thread t5 = new Thread(proth,"生产者3");

//new三个包含生产者类的线程
Thread t2 = new Thread(conth,"消费者1");
Thread t4 = new Thread(conth,"消费者2");
Thread t6 = new Thread(conth,"消费者3");

//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}

 

 (2)演示多个生产者和消费者通信效果

 

生产者1生产了一个编号为1的面包!
消费者3买了一个面包编号为1
生产者2生产了一个编号为2的面包!
消费者2买了一个面包编号为2
生产者3生产了一个编号为3的面包!
消费者1买了一个面包编号为3
生产者1生产了一个编号为4的面包!
消费者3买了一个面包编号为4

...
...

生产者3生产了一个编号为56的面包!
消费者3买了一个面包编号为56
生产者2生产了一个编号为57的面包!
消费者1买了一个面包编号为57
生产者1生产了一个编号为58的面包!
消费者2买了一个面包编号为58
生产者3生产了一个编号为59的面包!
消费者3买了一个面包编号为59
生产者2生产了一个编号为60的面包!
消费者1买了一个面包编号为60

    如上所示,我们创建了三个生产者和三个消费者,所以我们for循环为三次,也就是会生产和消费各60个面包;

  当进程开始后,会随机开启一个生产者线程生产一个面包后,陷入沉睡,随后会随机唤醒一个消费者线程,

  接着消费掉刚刚生产的那个面包,再次陷入沉睡,随机唤醒一个生产者线程...  周而复始,直到结束。