多线程六 线程间的通信

àì夳堔傛蜴生んèń 2021-11-16 14:04 593阅读 0赞

线程间的通信

      1. 线程间的通信
      1. 等待唤醒机制
      • 等待唤醒中的方法
      1. ThreadLocal
      1. 生产消费者模型

1. 线程间的通信

什么是线程间的通信

多个线程协同处理同一个资源,线程的任务不相同

为什么需要线程间的通信

多个线程并发执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要协调通信,以此来帮我们达到多线程共同操作一份数据的目的。

如何保证线程间的通信

多个线程在操作同一份数据时,避免对同一共享变量的争夺,通过等待唤醒机制来保证线程能合理有有效的利用资源。

2. 等待唤醒机制

就像我们前面买票的例子,如果不加锁线程在访问共享资源的时候就会竞争,都去等待CPU的调度来争夺资源。但是,通过synchronized和Lock来对资源上锁,使资源同时只能被一个线程访问到,避免了并发带来的不安全问题。这属于线程间的竞争

就像人一样,有竞争就会有合作

线程间的合作就是一个线程进行了规定操作后,就进入等待状态wait,等待其他线程执行完他们的指定代码过后再将其唤酲notify。在有多个线程进行等待时,如果需要,可以使用notifyAll来唤酲所有的等待线程

wait / notify就是线程间的一种协作机制。

等待唤醒中的方法

wait、notify必须搭配synchronized使用

使用wait、notify()的前提:

必须在同步方法或同步代码块中使用(拿到相应对象的锁),如果没有synchronized会抛出java.lang.IllegalMonitorStateException(非法监视器状态异常)

wait——痴汉方法

持有锁的线程调用wait()后会一直阻塞,直到有线程调用notify()将其唤醒

wait的重载方法:

  1. public final native void wait(long timeout)

等待一段时间,若还未被唤醒,继续执行,默认单位为ms

  1. public class WaitTest {
  2. public static void main(String[] args) throws InterruptedException {
  3. Object obj = new Object();
  4. synchronized (obj) {
  5. System.out.println("wait开始...");
  6. obj.wait(3000);
  7. System.out.println("wait结束...");
  8. }
  9. }
  10. }

不加唤醒时间,没有notify就会一直处于等待中
2019072816141772.gif

添加唤醒时间,在规定的时间内没有被唤醒就会自动结束等待
20190728161644534.gif

notify

唤醒任意一个处于等待状态的线程(notify方法就是使等待的线程继续运行)
在这里插入图片描述
在这里插入图片描述

等待唤酲机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait: 线程不再参与竞争锁,不再等待CPU调度,进入wait set中,此时线程的状态是WAITING。它要等待别的线程执唤醒它(notify),通知它从wait set中释放出来,重新进入到调度队列 ready queue中
  2. notify: 唤醒等待的线程,通知等待的线程从wait set中释放,重新进入到调度队列 ready queue中
  3. notifyAll: 唤醒所有等待的线程

注意:

wait会释放锁,notify仅仅只是通知,不释放锁

哪怕只通知了—个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁(wait方法会释放锁来等待),所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行

  1. wait方法与notify方法必须要由同一个锁对象调用
  2. wait方法与notify方法是属于Object类的方法的
  3. wait方法与notify方法必须要在同步代码块或者是同步方法中使用

总结:

  • 如果能获取锁,线程就从WAITING状态变为就绪状态
  • 如果没有获取到锁,就从wait set中出来然后进入到entry set,线程从WAITING变为阻塞状态

notify()唤醒等待的线程:

  1. class Sync implements Runnable{
  2. //标志位来唤醒等待的线程
  3. private boolean flag;
  4. private Object obj;
  5. public Sync(Object obj,boolean flag) {
  6. this.obj = obj;
  7. this.flag = flag;
  8. }
  9. public void waitMethod() {
  10. synchronized (obj) {
  11. while (true) {
  12. try {
  13. System.out.println("wait方法开始..." + Thread.currentThread().getName());
  14. obj.wait();
  15. System.err.println("wait方法结束..." + Thread.currentThread().getName());
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }
  21. }
  22. public void notifyMethod() {
  23. synchronized (obj) {
  24. System.out.println("notify方法开始,唤醒等待的线程" + Thread.currentThread().getName());
  25. obj.notify();
  26. System.err.println("notify方法结束!!!" + Thread.currentThread().getName());
  27. }
  28. }
  29. @Override
  30. public void run() {
  31. if (flag) {
  32. this.waitMethod();
  33. }else {
  34. this.notifyMethod();
  35. }
  36. }
  37. }
  38. public class SyncWaitNotify {
  39. public static void main(String[] args) throws InterruptedException {
  40. Object obj = new Object();
  41. Sync wait = new Sync(obj,true);
  42. Sync notify = new Sync(obj,false);
  43. new Thread(wait,"wait线程").start();
  44. Thread.sleep(2000);
  45. new Thread(notify,"notify线程").start();
  46. }
  47. }

在这里插入图片描述
运行结果分析:

从结果上来看第一个线程执行的是一个waitMethod方法,该方法里面有个死循环并且使用了wait方法进入等待状态将锁释放,如果这个线程不被唤醒的话将会一直等待下去,这个时候第二个线程执行的是notifyMethod方法,该方法里面执行了一个唤醒线程的操作,并且一直将notify的同步代码块执行完毕之后才会释放锁然后继续执行wait结束打印语句。
在这里插入图片描述
在这里插入图片描述
任意一个Object及其子类对象都有两个队列:

  • 同步队列:所有尝试获取该对象Monitor失败的线程,都加入同步队列,排队获取
  • 等待队列:已经拿到了锁的线程在等待其他资源时,主动释放锁,置入该对象等待队列中,等待其被唤醒;当调用notify()会在等待队列中任意唤醒一个线程,将其置入到同步队列尾部,排队获取锁

notifyAll

将等待队列中的所有线程唤醒,并且加入到同步队列

敲黑板:

既然学习了wait(),它使当前线程处于等待中,需要用notify()或者notifyAll()来唤醒,那么它和sleep()有何区别呢?

  1. sleep()是Thread类中定义的方法,到了一定的时间后该线程自动唤醒,不会释放对象锁。
  2. wait()是Object类中定义的方法,要想唤醒必须使用notify()、notifyAll()方法才能唤醒,会释放对象锁

总结一下就是:所在的部门不同,唤醒的方法也不同

3. ThreadLocal

ThreadLocal——线程本地变量(属于线程私有资源,不与其他线程共享)

set()设置线程私有属性值

get()取得线程私有属性值

  • 在使用 ThreadLocal 类型变量进行相关操作时,都会通过当前线程获取到 ThreadLocalMap 来完成操作。
  • 每个线程的 ThreadLocalMap(存放元素)是属于线程自己的,ThreadLocalMap 中维护的值也是属于线程自己的。这就保证了ThreadLocal 类型的变量在每个线程中是独立的,在多线程环境下不会相互影响

4. 生产消费者模型

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

这个阻塞队列就是用来给生产者和消费者解耦的。大多数设计模式,都会找一个第三者出来进行解耦,如工厂模式的第三者是工厂类,模板模式的第三者是模板类。在学习一些设计模式的过程中,如果先找到这个模式的第三者,能帮助我们快速熟悉一个设计模式

举一个生活中的例子来说明:

在这里插入图片描述

作为一个即将迈入社会并被生活毒打的卑微青年,毕业后到“工地搬砖”一定会遇到租房子的问题。对于房东来说,我是一个消费者,她是房子的生产者。我要租房子,一定得和房东协商,那么,这个效率就比较低了,如果这个房子我觉得不合适,只能再去看房子,再和其它的房东进行协商;而房东呐,她也只能等着房客来看房子。

但是,有一个机构它聪明呀,他可能偷偷看了“生产者消费者模型”,理解到了其中的真谛,于是,他作为“中介”的角色出现了…现在,他到各个房东手上收集房源,然后整理出来给租客们选择,然后闷声发大财。

那么,中介这个角色是不是就相当于“容器”来解决生产者(房东)和消费者(租客)的强耦合问题。房客住的房子有问题了,找中介;房东想涨房租,找中介;中介来调和房东与房客之间的问题,不在需要房东与房客之间有联系。

代码试着来实现上述的逻辑:

  1. class Goods {
  2. private String rooms; //货物名称
  3. private int count; //货物库存
  4. //生产商品
  5. public synchronized void set(String rooms) {
  6. this.rooms = rooms;
  7. this.count = count + 1;
  8. System.out.println(Thread.currentThread().getName()+ " 生产" + this);
  9. }
  10. //消费商品
  11. public synchronized void get() {
  12. this.count = this.count - 1;
  13. System.out.println(Thread.currentThread().getName() + " 消费" + this);
  14. }
  15. @Override
  16. public String toString() {
  17. return "Goods { " +
  18. "rooms='" + rooms + '\'' +
  19. ", count=" + count +
  20. '}';
  21. }
  22. }
  23. class Producer implements Runnable {
  24. private Goods goods;
  25. public Producer(Goods goods) {
  26. this.goods = goods;
  27. }
  28. @Override
  29. public void run() {
  30. this.goods.set("海景别墅一套,房租减半,水电全免...");
  31. }
  32. }
  33. class Consumer implements Runnable {
  34. private Goods goods;
  35. public Consumer(Goods goods) {
  36. this.goods = goods;
  37. }
  38. @Override
  39. public void run() {
  40. this.goods.get();
  41. }
  42. }
  43. public class ProducerConsumer {
  44. public static void main(String[] args) throws InterruptedException {
  45. Goods goods = new Goods();
  46. new Thread(new Producer(goods),"生产者线程").start();
  47. Thread.sleep(1000);
  48. new Thread(new Consumer(goods),"消费者线程").start();
  49. }
  50. }

在这里插入图片描述
ATTENTION:

那么问题来了,将生产者线程启动和消费者线程启动的代码换个位置。此时问题产生了,生产者还没生产商品,消费者就要消费商品,导致数量不正确。
在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,593人围观)

还没有评论,来说两句吧...

相关阅读