线程间通信

女爷i 2022-02-28 09:33 490阅读 0赞

注意:

  • 必须在同步方法中使用wait和notify方法,因为执行wait和notify的前提条件是必须持有同步方法(或块)的monitor的所有权,否则将会抛出异常。
  • 同步代码的monitor必须与执行wait notify方法的对象一致,简单地说就是用哪个对象的monitor进行同步,就只能用哪个对象进行wait和notify操作。

1、单生产者单消费者模式

  1. 顾名思义,就是一个线程消费,一个线程生产。我们先来看看等待/通知机制下的生产者消费者模式:我们假设这样一个场景,我们是卖北京烤鸭店铺,我们现在只有一条生产线也只有一条消费线,也就是说只能生产线程生产完了,再通知消费线程才能去卖,如果消费线程没烤鸭了,就必须通知生产线程去生产,此时消费线程进入等待状态。在这样的场景下,我们不仅要保证共享数据(烤鸭数量)的线程安全,而且还要保证烤鸭数量在消费之前必须有烤鸭。下面我们通过java代码来实现:
  2. package com.zejian.test;
  3. public class KaoYaResource {
  4. private String name;
  5. private int count = 1;//烤鸭的初始数量
  6. private boolean flag = false;//判断是否有需要线程等待的标志
  7. /**
  8. * 生产烤鸭
  9. */
  10. public synchronized void product(String name){
  11. if(flag){
  12. //此时有烤鸭,等待
  13. try {
  14. this.wait();
  15. } catch (InterruptedException e) {
  16. e.printStackTrace()
  17. ;
  18. }
  19. }
  20. this.name=name+count;//设置烤鸭的名称
  21. count++;
  22. System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
  23. flag=true;//有烤鸭后改变标志
  24. notifyAll();//通知消费线程可以消费了
  25. }
  26. /**
  27. * 消费烤鸭
  28. */
  29. public synchronized void consume(){
  30. if(flag){//如果没有烤鸭就等待
  31. try{this.wait();}catch(InterruptedException e){}
  32. }
  33. System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);//消费烤鸭1
  34. flag = false;
  35. notifyAll();//通知生产者生产烤鸭
  36. }
  37. }

在这个类中我们有两个synchronized的同步方法,一个是生产烤鸭的,一个是消费烤鸭的,之所以需要同步是因为我们操作了共享数据count,同时为了保证生产烤鸭后才能消费也就是生产一只烤鸭后才能消费一只烤鸭,我们使用了等待/通知机制,wait()和notify()。当第一次运行生产现场时调用生产的方法,此时有一只烤鸭,即flag=false,无需等待,因此我们设置可消费的烤鸭名称然后改变flag=true,同时通知消费线程可以消费烤鸭了,即使此时生产线程再次抢到执行权,因为flag=true,所以生产线程会进入等待阻塞状态,消费线程被唤醒后就进入消费方法,消费完成后,又改变标志flag=false,通知生产线程可以生产烤鸭了………以此循环。

  1. package com.zejian.test;
  2. /**
  3. * @author zejian
  4. * @time 2016年3月12日 下午10:29:12
  5. * @decrition 单生产者单消费者模式
  6. */
  7. public class Single_Producer_Consumer {
  8. public static void main(String[] args)
  9. {
  10. KaoYaResource r = new KaoYaResource();
  11. Producer pro = new Producer(r);
  12. Consumer con = new Consumer(r);
  13. //生产者线程
  14. Thread t0 = new Thread(pro);
  15. //消费者线程
  16. Thread t2 = new Thread(con);
  17. //启动线程
  18. t0.start();
  19. t2.start();
  20. }
  21. }
  22. /**
  23. * @author zejian
  24. * @time 2016年3月12日 下午11:02:22
  25. * @decrition 生产者线程
  26. */
  27. class Producer implements Runnable
  28. {
  29. private KaoYaResource r;
  30. Producer(KaoYaResource r)
  31. {
  32. this.r = r;
  33. }
  34. public void run()
  35. {
  36. while(true)
  37. {
  38. r.product("北京烤鸭");
  39. }
  40. }
  41. }
  42. /**
  43. * @author zejian
  44. * @time 2016年3月12日 下午11:02:05
  45. * @decrition 消费者线程
  46. */
  47. class Consumer implements Runnable
  48. {
  49. private KaoYaResource r;
  50. Consumer(KaoYaResource r)
  51. {
  52. this.r = r;
  53. }
  54. public void run()
  55. {
  56. while(true)
  57. {
  58. r.consume();
  59. }
  60. }
  61. }

在这个类中我们创建两个线程,一个是消费者线程,一个是生产者线程,我们分别开启这两个线程用于不断的生产消费,运行结果如下:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0ppblhZYW4_size_16_color_FFFFFF_t_70

很显然的情况就是生产一只烤鸭然后就消费一只烤鸭。运行情况完全正常,嗯,这就是单生产者单消费者模式。上面使用的是synchronized关键字的方式实现的。

2、多生产者多消费者模式

  1. 分析完了单生产者单消费者模式,我们再来聊聊多生产者多消费者模式,也就是多条生产线程配合多条消费线程。既然这样的话我们先把上面的代码Single\_Producer\_Consumer.java类修改成新类,大部分代码不变,仅新增2条线程去跑,一条t1的生产 共享资源类KaoYaResource不作更改,代码如下
  2. package com.zejian.test;
  3. /**
  4. * @author zejian
  5. * @time 2016年3月13日 上午10:35:05
  6. * @decrition 多生产者多消费者模式
  7. */
  8. public class Mutil_Producer_Consumer {
  9. public static void main(String[] args)
  10. {
  11. KaoYaResource r = new KaoYaResource();
  12. Mutil_Producer pro = new Mutil_Producer(r);
  13. Mutil_Consumer con = new Mutil_Consumer(r);
  14. //生产者线程
  15. Thread t0 = new Thread(pro);
  16. Thread t1 = new Thread(pro);
  17. //消费者线程
  18. Thread t2 = new Thread(con);
  19. Thread t3 = new Thread(con);
  20. //启动线程
  21. t0.start();
  22. t1.start();
  23. t2.start();
  24. t3.start();
  25. }
  26. }
  27. /**
  28. * @author zejian
  29. * @time 2016年3月12日 下午11:02:22
  30. * @decrition 生产者线程
  31. */
  32. class Mutil_Producer implements Runnable
  33. {
  34. private KaoYaResource r;
  35. Mutil_Producer(KaoYaResource r)
  36. {
  37. this.r = r;
  38. }
  39. public void run()
  40. {
  41. while(true)
  42. {
  43. r.product("北京烤鸭");
  44. }
  45. }
  46. }
  47. /**
  48. * @author zejian
  49. * @time 2016年3月12日 下午11:02:05
  50. * @decrition 消费者线程
  51. */
  52. class Mutil_Consumer implements Runnable
  53. {
  54. private KaoYaResource r;
  55. Mutil_Consumer(KaoYaResource r)
  56. {
  57. this.r = r;
  58. }
  59. public void run()
  60. {
  61. while(true)
  62. {
  63. r.consume();
  64. }
  65. }
  66. }

就多了两条线程,我们运行代码看看,结果如下:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0ppblhZYW4_size_16_color_FFFFFF_t_70 1

  1. 共享数据count的获取方法都进行synchronized关键字同步了呀!那怎么还会出现数据混乱的现象啊?
  2. 分析:确实,我们对共享数据也采用了同步措施,而且也应用了等待/通知机制,但是这样的措施只在单生产者单消费者的情况下才能正确应用,但从运行结果来看,我们之前的单生产者单消费者安全处理措施就不太适合多生产者多消费者的情况了。那么问题出在哪里?可以明确的告诉大家,肯定是在资源共享类,下面我们就来分析问题是如何出现,又该如何解决?直接上图!

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0ppblhZYW4_size_16_color_FFFFFF_t_70 2

解决后的资源代码如下只将if改为了while:

  1. package com.zejian.test;
  2. /**
  3. * @author zejian
  4. * @time 2016年3月12日 下午10:44:25
  5. * @decrition 烤鸭资源
  6. */
  7. public class KaoYaResource {
  8. private String name;
  9. private int count = 1;//烤鸭的初始数量
  10. private boolean flag = false;//判断是否有需要线程等待的标志
  11. /**
  12. * 生产烤鸭
  13. */
  14. public synchronized void product(String name){
  15. while(flag){
  16. //此时有烤鸭,等待
  17. try {
  18. this.wait();
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. this.name=name+count;//设置烤鸭的名称
  24. count++;
  25. System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
  26. flag=true;//有烤鸭后改变标志
  27. notifyAll();//通知消费线程可以消费了
  28. }
  29. /**
  30. * 消费烤鸭
  31. */
  32. public synchronized void consume(){
  33. while(!flag){//如果没有烤鸭就等待
  34. try{this.wait();}catch(InterruptedException e){}
  35. }
  36. System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);//消费烤鸭1
  37. flag = false;
  38. notifyAll();//通知生产者生产烤鸭
  39. }
  40. }

运行代码,结果如下:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0ppblhZYW4_size_16_color_FFFFFF_t_70 3

到此,多消费者多生产者模式也完成,不过上面用的是synchronied关键字实现的,而锁对象的解决方法也一样将之前单消费者单生产者的资源类中的if判断改为while判断即可代码就不贴了哈。

3.线程死锁

现在我们再来讨论一下线程死锁问题,从上面的分析,我们知道锁是个非常有用的工具,运用的场景非常多,因为它使用起来非常简单,而且易于理解。但同时它也会带来一些不必要的麻烦,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用。我们先通过一个例子来分析,这个例子会引起死锁,使得线程t1和线程t2互相等待对方释放锁。

  1. package com.zejian.test;
  2. /**
  3. * @author zejian
  4. * @time 2016年3月13日 下午2:45:52
  5. * @decrition 死锁示例
  6. */
  7. public class DeadLockDemo {
  8. private static String A="A";
  9. private static String B="B";
  10. public static void main(String[] args) {
  11. DeadLockDemo deadLock=new DeadLockDemo();
  12. while(true){
  13. deadLock.deadLock();
  14. }
  15. }
  16. private void deadLock(){
  17. Thread t1=new Thread(new Runnable(){
  18. @SuppressWarnings("static-access")
  19. @Override
  20. public void run() {
  21. synchronized (A) {
  22. try {
  23. Thread.currentThread().sleep(2000);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. synchronized(B){
  29. System.out.println("1");
  30. }
  31. }
  32. });
  33. Thread t2 =new Thread(new Runnable() {
  34. @Override
  35. public void run() {
  36. synchronized (B) {
  37. synchronized (A) {
  38. System.out.println("2");
  39. }
  40. }
  41. }
  42. });
  43. //启动线程
  44. t1.start();
  45. t2.start();
  46. }
  47. }

同步嵌套是产生死锁的常见情景,从上面的代码中我们可以看出,当t1线程拿到锁A后,睡眠2秒,此时线程t2刚好拿到了B锁,接着要获取A锁,但是此时A锁正好被t1线程持有,因此只能等待t1线程释放锁A,但遗憾的是在t1线程内又要求获取到B锁,而B锁此时又被t2线程持有,到此结果就是t1线程拿到了锁A同时在等待t2线程释放锁B,而t2线程获取到了锁B也同时在等待t1线程释放锁A,彼此等待也就造成了线程死锁问题。虽然我们现实中一般不会向上面那么写出那样的代码,但是有些更为复杂的场景中,我们可能会遇到这样的问题,比如t1拿了锁之后,因为一些异常情况没有释放锁(死循环),也可能t1拿到一个数据库锁,释放锁的时候抛出了异常,没有释放等等,所以我们应该在写代码的时候多考虑死锁的情况,这样才能有效预防死锁程序的出现。下面我们介绍一下避免死锁的几个常见方法:

  • 1.避免一个线程同时获取多个锁。
  • 2.避免在一个资源内占用多个 资源,尽量保证每个锁只占用一个资源。
  • 3.尝试使用定时锁,使用tryLock(timeout)来代替使用内部锁机制。
  • 4.对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
  • 5.避免同步嵌套的发生

转载:

https://blog.csdn.net/javazejian/article/details/50878665

发表评论

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

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

相关阅读

    相关 线通信ThreadLocal

    ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这 个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal

    相关 线通信

    为什么需要线程通讯 线程是操作系统调度的最小单位,有自己的栈空间,可以按照既定的代码逐步的执行,但是如果每个线程间都孤立的运行,那就会造资源浪费。所以在现实中,我们需要这

    相关 Java线通信

    上述例题无条件的阻塞了其他线程异步访问某个方法。Java对象中隐式管程的应用是很强大的,但是你可以通过进程间通信达到更微妙的境界。这在Java中是尤为简单的。 像前面

    相关 线通信

    线程和线程之间不是独立的个体,它们彼此之间可以互相通信和协作。 线程通信就是在线程之间传递信息,保证他们能够协同工作。在线程间进行通信后,系统之间的交互性会更强大,在大大提高

    相关 线通信

    一、引言 线程与线程之间不是相互独立的存在,它们彼此之间需要相互通信和协作。最典型的例子就是生产者-消费者问题。下面首先介绍 wait/notify 机制,并对实现该机制

    相关 线通信

    注意: 必须在同步方法中使用wait和notify方法,因为执行wait和notify的前提条件是必须持有同步方法(或块)的monitor的所有权,否则将会抛出异常