首页 > 学院 > 开发设计 > 正文

Java多线程技术学习笔记(二)

2019-11-14 23:07:04
字体:
来源:转载
供稿:网友
java多线程技术学习笔记(二)

目录:

  1. 线程间的通信示例
  2. 等待唤醒机制
  3. 等待唤醒机制的优化
  4. 线程间通信经典问题:多生产者多消费者问题
  5. 多生产多消费问题的解决
  6. JDK1.5之后的新加锁方式
  7. 多生产多消费问题的新解决办法
  8. sleep和wait的区别
  9. 停止线程的方式
  10. 守护线程
  11. 线程的其他知识点

一、线程间的通信示例 返目录回

多个线程在处理同一资源,任务却不同。

假设有一堆货物,有一辆车把这批货物往仓库里面运,另外一辆车把前一辆车运进仓库的货物往外面运。这里货物就是同一资源,但是两辆车的任务却不同,一个是往里运,一个是往外运。

下面举例子来逐步展示线程间通信:首先建立一个Person类。包含 name 和 sex 属性, 我们建立一个线程输入一个对象(即输入一个name和sex), 另一个线程输出该对象(即输出该对象的name 和 sex).

 1 package thread.demo; 2  3 class Person 4 { 5     PRivate String name; 6     private String sex; 7      8     public String getName()  9     {10         return name;11     }12     public void setName(String name) 13     {14         this.name = name;15     }16     public String getSex() 17     {18         return sex;19     }20     public void setSex(String sex) 21     {22         this.sex = sex;23     }24 25 }26 //输入27 class Input implements Runnable28 {29     private Person r;30     31     Input(Person r)32     {33         this.r = r;34     }35     36     public void run()37     {38         int x = 0;39         while(true)//这里加无限循环是为了方便后面观察现象40         {41             if (x == 0)42             {43                 r.setName("Mike");44                 r.setSex("Male");45             }46             else47             {48                 r.setName("Lucy");49                 r.setSex("Female");50             }51             x = (x + 1) % 2; //变换x的值,使得切换输入不同的对象52         }53     }54 }55 56 class  Output implements Runnable57 {58     private Person r;59     60     Output(Person r)61     {62         this.r = r;63     }64     65     public void run()66     {67         while(true)//这里加无限循环是为了方便后面观察现象68         {69             System.out.println(r.getName() + "..." + r.getSex());70         }71     }72 }73 public class MultithreadDemo_1 74 {75 76     /**77      * @param args78      */79     public static void main(String[] args) 80     {81         //建立共享数据Person r,输入和输出都操作对象 r82         Person r = new Person();83         Input in = new Input(r);84         Output out = new Output(r);85         86         //建立两个线程,分别执行输入任务和输出任务87         Thread t1 = new Thread(in);88         Thread t2 = new Thread(out);89         90         //开启线程91         t1.start();92         t2.start();93     }94 }
View Code

执行结果就是一直不断输出,会发现有如下类似现象:

问题来了,程序中明明Mike的sex是Male,Lucy是Female,却出现了上面图片中“诡异”的的现象,这当然是线程安全问题!来分析一下:

在上一篇博文Java多线程技术学习笔记(一)中分析了线程安全问题产生的原因:

  • 多个线程操作共享的数据
  • 操作共享数据的线程代码有多条

回头看我们的代码,共享数据Person r被两个线程操作,满足第一条;操作 r 的代码就是run方法里面的代码,看到第36行开始的run方法确实有很多条,满足第二个条件!所以出现上述诡异输出其实是很正常的现象!具体到上述代码,造成原因:

  • 假设线程0即Input线程,首先抢到cpu执行权,由于x==0, 那么Person对象r的name就是Mike, sex就是Male, 然后x = (x + 1)%2 = 1.
  • 接着有可能Input线程继续占有着cpu执行权, 由于39行的while(true)和x == 1,执行到48行,这时 r 的name = Lucy,问题来了,Input线程还没有来得及更新r的sex,即还没有来得及执行第49行代码,这时线程1,Output线程把cpu执行抢走了。
  • 于是此时r的name就是Lucy,而sex由于没来得及改变还是Male,然后Output线程输出:Lucy...Male! 产生Mike...Female的过程与此类似!

分析了原因,就来解决问题,那就是前一篇博文笔记里面说的同步:

 1 //输入 2 class Input implements Runnable 3 { 4     private Person r; 5      6     Input(Person r) 7     { 8         this.r = r; 9     }10      11     public void run()12     {13         int x = 0;14         while(true)//这里加无限循环是为了方便后面观察现象15         {16             synchronized(r)//同步代码块的锁可以使用任意对象,只要保证多个线程使用的是同一个锁即可17             {18                 if (x == 0)19                 {20                     r.setName("Mike");21                     r.setSex("Male");22                 }23                 else24                 {25                     r.setName("Lucy");26                     r.setSex("Female");27                 }28             }29             x = (x + 1) % 2; //变换x的值,使得切换输入不同的对象30         }31     }32 }33 34 class  Output implements Runnable35 {36     private Person r;37     38     Output(Person r)39     {40         this.r = r;41     }42     43     public void run()44     {45         while(true)//这里加无限循环是为了方便后面观察现象46         {47             synchronized(r)48             {49                 System.out.println(r.getName() + "..." + r.getSex());50             }51         }52     }53 }
View Code

多次运行之后可以验证,输出正常:

但是注意到,输出是连续一堆Lucy...Female,然后连续一堆Mike...Male,原因很简单:一旦切换到输出线程,该线程不可能只执行一次,一下输出多次,因为name 和 sex 由于同步的缘故,要么是Lucy...Female,要么是Mike...Male,一输出就是一片相同的Lucy或者Mike. 为了展示多线程间的通信,现在要实现的是,输入线程输入一个name和sex,就立马在输出线程输出,然后再输入一个,再输出一个,如此交替!注意输入和输出是在不同线程里面执行的!所以就需要线程间通信,即输入线程输了一个name和sex,就不在继续输入,而是去通知输出线程输出一下刚才输入的name和sex,输出一次之后,也不再继续输出,而是去通知输入线程继续输入新的内容,输入线程和输出线程如此交替... 这就是所谓的“等待唤醒机制”。

二、等待唤醒机制返目录回

要达到上面所说的输入和输出线程交替执行,需要设置一个标志位,根据标志位来判断到底是该执行输出还是输出!

涉及的方法:

  • wait():让线程处于冻结状态,被wait的线程会被存储到线程池,所有等待的线程都在这个池子里面,等待机会去执行。该方法是从java.lang.Object继承过来的:

  翻译过来意思就是:该方法会导致当前线程等待,直到其他线程调用了此线程的notify或者notifyAll方法。 注意到wait方法会抛出异常,所以在面我们的代码中加入了try/catch

  • nofity():唤醒线程池中任意一个线程。
  • notifyAll():唤醒线程池中的所有线程。

这些方法都必须定义在同步中。因为这些方法是用于操作线程状态的方法,所以必须要明确到底操作的是哪个锁上的线程。

注意到上述操作线程的方法都是放在Object类中,这是因为方法都是同步锁的方法。而锁可以是任意对象,任意的对象都可以调用的方法一定定义在Object类中。

代码思路:初始化标志位-->输入线程输入-->更改标志位-->唤醒输出线程-->输出线程输出-->更该标志位-->唤醒输入线程-->输入线程输入--> ...

代码改动如下:

  1 package thread.demo;  2 /*  3  * 等待/唤醒机制  4  */  5 class Person  6 {  7     private String name;  8     private String sex;  9     boolean full = false;//标志位,代表着是否已经更新了name和sex 10      11     public String getName()  12     { 13         return name; 14     } 15     public void setName(String name)  16     { 17         this.name = name; 18     } 19     public String getSex()  20     { 21         return sex; 22     } 23     public void setSex(String sex)  24     { 25         this.sex = sex; 26     } 27  28 } 29 //输入 30 class Input implements Runnable 31 { 32     private Person r; 33      34     Input(Person r) 35     { 36         this.r = r; 37     } 38       39     public void run() 40     { 41         int x = 0; 42         while(true)//这里加无限循环是为了方便后面观察现象 43         { 44             synchronized(r)//同步代码块的锁可以使用任意对象,只要保证多个线程使用的是同一个锁即可 45             { 46                 if (r.full) 47                 { 48                     // 如果full标志位为真 49                     // r锁的wait方法让线程冻结,在线程池中等待,就不执行后面的输入name和sex的语句 50                     try { 51                         r.wait();//注意调用wait方法要明确锁 52                     } catch (InterruptedException e) { 53                         e.printStackTrace(); 54                     } 55                 } 56                 //如果标志位为假,就执行下面语句输入name和sex 57                 if (x == 0) 58                 { 59                     r.setName("Mike"); 60                     r.setSex("Male"); 61                 } 62                 else 63                 { 64                     r.setName("Lucy"); 65                     r.setSex("Female"); 66                 } 67                 //输入了一个对象,即一对name和sex之后,将标志位置为真 68                 r.full = true; 69                 //然后通知输出线程(即唤醒输出线程)来输出刚输入的内容 70                 r.notify(); 71             } 72             x = (x + 1) % 2; //变换x的值,使得切换输入不同的对象 73         } 74     } 75 } 76  77 class  Output implements Runnable 78 { 79     private Person r; 80      81     Output(Person r) 82     { 83         this.r = r; 84     } 85      86     public void run() 87     { 88         while(true)//这里加无限循环是为了方便后面观察现象 89         { 90             synchronized(r) 91             { 92                 //如果输入线程还没有输入内容,输出线程就等待 93                 if (!r.full) 94                 { 95                     try { 96                         r.wait(); 97                     } catch (InterruptedException e) { 98                         e.printStackTrace(); 99                     }100                 }101                 //如果已经输入了内容,就直接输出102                 System.out.println(r.getName() + "..." + r.getSex());103                 //输出完了之后,将标志位置为false,表明刚才的内容应经输出了104                 r.full = false;105                 //然后通知输入线程再输入新内容106                 r.notify();107             }108         }109     }110 }111 public class MultithreadDemo_1 112 {113 114     /**115      * @param args116      */117     public static void main(String[] args) 118     {119         //建立共享数据Person r,输入和输出都操作对象 r120         Person r = new Person();121         Input in = new Input(r);122         Output out = new Output(r);123         124         //建立两个线程,分别执行输入任务和输出任务125         Thread t1 = new Thread(in);126         Thread t2 = new Thread(out);127         128         //开启线程129         t1.start();130         t2.start();131     }132 }
View Code

运行结果:

达到了预期。

三、等待唤醒机制的优化返目录回

再考虑上面写的代码,其实并不好,同步的目的是为了防止某个线程对name赋值以后,还没来得及对sex赋值时,其他线程就切了进来!所以需要同步的代码就是赋值的两行:

59,60行代码与64,65行代码代码功能重复,所以优化代码如下:

  1 package thread.demo;  2 /*  3  * 等待/唤醒机制  4  */  5 class Person  6 {  7     private String name;  8     private String sex;  9     private boolean full = false;//标志位,代表着是否已经更新了name和sex 10      11     public String getName()  12     { 13         return name; 14     } 15     public void setName(String name)  16     { 17         this.name = name; 18     } 19     public String getSex()  20     { 21         return sex; 22     } 23     public void setSex(String sex)  24     { 25         this.sex = sex; 26     } 27  28     public synchronized void set(String name, String sex) 29     { 30         if (full) 31         { 32             try  33             { 34                 this.wait(); //注意同步函数的锁是this,所以这里调用this的wait方法 35             }  36             catch (InterruptedException e)  37             { 38                 e.printStackTrace(); 39             } 40         } 41          42         this.name = name; 43         this.sex = sex; 44         full = true; 45         notify(); 46     } 47      48     public synchronized void show() 49     { 50         if (!full) 51         { 52             try  53             { 54                 this.wait(); //注意同步函数的锁是this,所以这里调用this的wait方法 55             }  56             catch (InterruptedException e)  57             { 58                 e.printStackTrace(); 59             } 60         } 61         //如果已经输入了内容,就直接输出 62         System.out.println(name + "..." + sex); 63         full = false; 64         notify();  65     } 66 } 67 //输入 68 class Input implements Runnable 69 { 70     private Person r; 71      72     Input(Person r) 73     { 74         this.r = r; 75     } 76       77     public void run() 78     { 79         int x = 0; 80         while(true)//这里加无限循环是为了方便后面观察现象 81         { 82             if (x == 0) 83             { 84                 r.set("Mike", "Male"); 85             } 86             else 87             { 88                 r.set("Lucy", "Female");     89             } 90             x = (x + 1) % 2; //变换x的值,使得切换输入不同的对象 91         } 92     } 93 } 94  95 class  Output implements Runnable 96 { 97     private Person r; 98      99     Output(Person r)100     {101         this.r = r;102     }103     104     public void run()105     {106         while(true)//这里加无限循环是为了方便后面观察现象 107         {108             r.show();109         }110     }111 }112 public class MultithreadDemo_1 113 {114 115     /**116      * @param args117      */118     public static void main(String[] args) 119     {120         //建立共享数据Person r,输入和输出都操作对象 r121         Person r = new Person();122         Input in = new Input(r);123         Output out = new Output(r);124         125         //建立两个线程,分别执行输入任务和输出任务126         Thread t1 = new Thread(in);127         Thread t2 = new Thread(out);128         129         //开启线程130         t1.start();131         t2.start();132     }133 }
View Code

功能与前面代码其实是一样的。

四、线程间通信经典问题:多生产者多消费者问题返目录回

这个问题很直接:就是一堆生产者生产产品,同时一堆消费者在消费产品!这一堆生产者和消费者对应程序中的多个线程,而产品就对应着这一堆线程共同操作的资源或者叫做共享数据。

我们想达到的目的是生产一件商品,消费一件,生产消费彼此交替!

首先来看一个生产者,一个消费者的例子,即生产一个就消费一个:

  1 package thread.demo;  2   3 /*  4  * 生产者消费者问题  5  */  6 class Product  7 {  8     private String name;// 产品名称  9     private int number = 1; // 产品编号 10     private boolean notEmpty = false;  11     public synchronized void produce(String name) 12     { 13         //如果有产品,可以停止生产一会 14         if (notEmpty) 15         { 16             try  17             { 18                 this.wait(); 19             }  20             catch (InterruptedException e)  21             { 22                 e.printStackTrace(); 23             } 24         } 25         // 如果没有产品,就无需等待,直接生产 26         // 生产的产品名称 27         this.name = name + number; 28         // 编号递增 29         number++; 30         // 输出生产的产品信息:线程名(对应在某一个生产者)+产品名 31         System.out.println(Thread.currentThread().getName() + " 生产出: " + this.name); 32         // 生产完了以后,就有了产品 33         notEmpty = true; 34         //通知消费者来消费 35         notify(); 36     } 37      38     public synchronized void consume() 39     { 40         // 如果没有产品,无法消费,等待 41         if (!notEmpty) 42         { 43             try  44             { 45                 this.wait(); 46             }  47             catch (InterruptedException e)  48             { 49                 e.printStackTrace(); 50             } 51         } 52         //打印产品被消费的信息:线程名(对应着某一个消费者) + 产品名 53         System.out.println(Thread.currentThread().getName() + "消费了:->  " + this.name); 54         //消费完了,通知生产者 55         notEmpty = false; 56         notify(); 57     } 58 } 59  60 // 创建生产者线程 61 class Producer implements Runnable 62 { 63     private Product p; 64     Producer(Product p) 65     { 66         this.p = p; 67     } 68      69     public void run() 70     { 71         while (true) 72         { 73             p.produce("bread"); // 假如生产面包 74         } 75     } 76      77 } 78  79 //创建消费者线程 80 class Consumer implements Runnable 81 { 82     private Product p; 83     Consumer(Product p) 84     { 85         this.p = p; 86     } 87      88     public void run() 89     { 90         while (true)//消费者消费 91         { 92             p.consume(); 93         } 94     } 95      96 } 97 public class ProducerConsumerDemo { 98  99     public static void main(String[] args) 100     {101         // 创建共享资源102         Product p = new Product();103         // 创建两个线程:生产和消费104         Producer producer = new Producer(p);105         Consumer consumer = new Consumer(p);106         Thread t1 = new Thread(producer);107         Thread t2 = new Thread(consumer);108         t1.start();109         t2.start();110     }111 }
View Code

运行结果:

这其实就是前面等待唤醒机制的另一种展示!下面在此代码的基础上改成多生产者,多消费者的示例:

 1 public class ProducerConsumerDemo { 2  3     public static void main(String[] args)  4     { 5         // 创建共享资源 6         Product p = new Product(); 7         // 两个生产者,两个消费者 8         Producer producer = new Producer(p); 9         Consumer consumer = new Consumer(p);10         Thread t0 = new Thread(producer);11         Thread t1 = new Thread(producer);12         13         Thread t2 = new Thread(consumer);14         Thread t3 = new Thread(consumer);15         16         t0.start();17         t1.start();18         t2.start();19         t3.start();20     }21 }
View Code

多次运行,会出现下面类似结果:

产生的问题:

  • bread15865:一个面包被生产两次
  • bread15866:一个面包被吃了两次
  • bread15867:这个面包还没消费掉就去生产下一个面包
  • 还有时生产一大片,却没能消费(多次运行会观察到)

显然这些问题都是不合理的,问题肯定出在多线程上,下面分分析。为了方便叙述,代码全部整理如下:

  1 package thread.demo;  2   3 /*  4  * 生产者消费者问题  5  */  6 class Product  7 {  8     private String name;// 产品名称  9     private int number = 1; // 产品编号 10     private boolean notEmpty = false;  11     public synchronized void produce(String name) 12     { 13         //如果有产品,可以停止生产一会 14         if (notEmpty) 15         { 16             try  17             { 18                 this.wait(); 19             }  20             catch (InterruptedException e)  21             { 22                 e.printStackTrace(); 23             } 24         } 25         // 如果没有产品,就无需等待,直接生产 26         // 生产的产品名称 27         this.name = name + number; 28         // 编号递增 29         number++; 30         // 输出生产的产品信息:线程名(对应在某一个生产者)+产品名 31         System.out.println(Thread.currentThread().getName() + " 生产出: " + this.name); 32         // 生产完了以后,就有了产品 33         notEmpty = true; 34         //通知消费者来消费 35         notify(); 36     } 37      38     public synchronized void consume() 39     { 40         // 如果没有产品,无法消费,等待 41         if (!notEmpty) 42         { 43             try  44             { 45                 this.wait(); 46             }  47             catch (InterruptedException e)  48             { 49                 e.printStackTrace(); 50             } 51         } 52         //打印产品被消费的信息:线程名(对应着某一个消费者) + 产品名 53         System.out.println(Thread.currentThread().getName() + "消费了:->  " + this.name); 54         //消费完了,通知生产者 55         notEmpty = false; 56         notify(); 57     } 58 } 59  60 // 创建生产者线程 61 class Producer implements Runnable 62 { 63     private Product p; 64     Producer(Product p) 65     { 66         this.p = p; 67     } 68      69     public void run() 70     { 71         while (true) 72         { 73             p.produce("bread"); // 假如生产面包 74         } 75     } 76      77 } 78  79 //创建消费者线程 80 class Consumer implements Runnable 81 { 82     private Product p; 83     Consumer(Product p) 84     { 85         this.p = p; 86     } 87      88     public void run() 89     { 90         while (true)//消费者消费 91         { 92             p.consume(); 93         } 94     } 95      96 } 97 public class ProducerConsumerDemo { 98  99     public static void main(String[] args) 100     {101         // 创建共享资源102         Product p = new Product();103         // 两个生产者,两个消费者104         Producer producer = new Producer(p);105         Consumer consumer = new Consumer(p);106         Thread t0 = new Thread(producer);107         Thread t1 = new Thread(producer);108         109         Thread t2 = new Thread(consumer);110         Thread t3 = new Thread(consumer);111         112         t0.start();113         t1.start();114         t2.start();115         t3.start();116     }117 }
View Code

  • 假设生产者(t0或者t1线程)得到cpu执行权,开始notEmpty为false,那么就需要去生产,执行到第27行,表示生产出了一个面包。然后number加1变为2 --> 打印信息-->notEmpty变为true-->notify()-->释放线程锁this;
  • 因为nofity是唤醒与锁相关的线程池中的任意线程,所以生产者有可能再次抢到cpu执行权,再次进入第11行的同步函数,但是进入之后发现notEmpty为true,就进入了等待状态。同样如果生产者再次抢到cpu执行权,还是会等待。
  • 继续,如果消费者(t2或者t3线程)抢到执行权,进入第38行的同步函数,判断(!notEmpty)为false,就去53行打印消费信息,代表消费了面包,然后notEmpty变为false,调用notify,释放线程锁,如上面生产者情况类似。倘若消费者再次抢到cpu执行权,就会因为判断标志位而变为等待。
  • 继续,生产者抢到cpu执行权,循环上面的步骤...

按照上面分析,是不会出现上述运行现象的,于是进一步分析:

  • 假设生产线程t0生产了第一个面包,标志位转换,t0接着又抢到执行权,根据14行标记判断,就转为等待,假设接着生产线程t1又抢到cpu执行权,同样在14行判断标记,t1又转为等待;
  • 然后消费者线程t2此时切入,消费了一次,标志位转换,然后notify去唤醒任意线程,假设此时等待的t0被唤醒,即具有执行资格,但是不一定抢到执行权;
  • 如果t3接着抢到执行权,根据第41行标志位判断,转为等待;
  • 注意此时t1, t2, t3都在等待,被唤醒状态的只有t0;
  • 于是t0很容易得到执行权,从第18行开始继续往下执行,由于没有异常发生,就不会执行catch语句,然后接着27行开始往下执行,生产了第二个面包,标志位转换true,然后notify(),问题来了,由于notify唤醒线程池中的处于等待的t1,t2,t3中的任意一个线程,如果此时唤醒了t2或者t3(消费线程),那就是正常的.
  • 但是如果唤醒了t1, 倘若t0此时继续占有cpu执行权,继续执行,判断标志,t0转为等待, t1同样从18行开始往下执行, 于是第二个面包还没被消费,t1又生产了第三个面包!!
  • 上面的过程就是t0唤醒t1,然后t1唤醒t0,即两个生产者之间可以互相唤醒,有可能一直生产不消费,也有可能生产线程执行了几次才去唤醒消费线程一次,即生产了多次才消费一次!
  • 同样的道理,两个消费者线程t2和t3也可以互相唤醒,就会导致对同一面包直消费,或者消费多次之后才去生产一次。

把程序中的四个线程画图分析如下:

其中双向箭头表示所连接的两线程可以互相唤醒。假如存在A箭头或者B箭头连续执行的情况,就会出现连续生产多个产品而不消费的情况,或者连续消费同一个产品而不生产的情况。很显然只要发生中间四个箭头的情况,就会生产一个,消费一个,从而满足我们的目的。所以解决的原因显而易见:防止A和B情况的发生,即生产者线程不能唤醒生产者线程,只能唤醒消费者线程,而消费者线程也只允许唤醒生产者线程。

五、多生产多消费问题的解决返目录回

上面分析到,t0唤醒t1后,由于t1从wait处醒过来不判断标记就继续往下执行,就出现了多生产,试想如果t1在被唤醒之后判断一下标记,t1会再次等待,即使t0再次过来也再次判断标记,也会一直等待,而不会去连续多次生产了,所以把14行和41行的 if 改为while,这样,每一个线程被唤醒之后就必须重新判断标记,改动之后运行结果如下:

现象就是运行若干次程序停止了,即就是在Java多线程技术学习笔记(一)提到的死锁现象,分析原因如下:

  • 生产一次后,t0, t1等待,然后通知消费者
  • 消费一次,t2,t3等待,notEmpty就变为true,
  • 假如唤醒了t0,有可能出现:t0判断标记true, t0等待,唤醒线程t1,t1判断标记true,也等待
  • 由于t1等待,无法执行到下面的notify方法,线程都无法被唤醒,所以四个线程都等待,程序就一直等

虽然线程每次都重新判断了标记,但是会出现上面死锁的现象,考虑到上面说到notifyAll方法还没有出场过,试着把notify改为notifyAll:

  1 package thread.demo;  2   3 /*  4  * 生产者消费者问题  5  */  6 class Product  7 {  8     private String name;// 产品名称  9     private int number = 1; // 产品编号 10     private boolean notEmpty = false;  11     public synchronized void produce(String name) 12     { 13         //如果有产品,可以停止生产一会 14         while (notEmpty) 15         { 16             try  17             { 18                 this.wait(); 19             }  20             catch (InterruptedException e)  21             { 22                 e.printStackTrace(); 23             } 24         } 25         // 如果没有产品,就无需等待,直接生产 26         // 生产的产品名称  27         this.name = name + number; 28         // 编号递增 29         number++; 30         // 输出生产的产品信息:线程名(对应在某一个生产者)+产品名 31         System.out.println(Thread.currentThread().getName() + " 生产出: " + this.name); 32         // 生产完了以后,就有了产品 33         notEmpty = true; 34         //通知其他线程 35         //notify(); 36         notifyAll(); 37          38     } 39      40     public synchronized void consume() 41     { 42         // 如果没有产品,无法消费,等待 43         while (!notEmpty) 44         { 45             try  46             { 47                 this.wait(); 48             }  49             catch (InterruptedException e)  50             { 51                 e.printStackTrace(); 52             } 53         } 54         //打印产品被消费的信息:线程名(对应着某一个消费者) + 产品名 55         System.out.println(Thread.currentThread().getName() + "消费了:->  " + this.name); 56         //消费完了,通知其他线程 57         notEmpty = false; 58         //notify(); 59         notifyAll(); 60     } 61 } 62  63 // 创建生产者线程 64 class Producer implements Runnable 65 { 66     private Product p; 67     Producer(Product p) 68     { 69         this.p = p; 70     } 71      72     public void run() 73     { 74         while (true) 75         { 76             p.produce("bread"); // 假如生产面包 77         } 78     } 79      80 } 81  82 //创建消费者线程 83 class Consumer implements Runnable 84 { 85     private Product p; 86     Consumer(Product p) 87     { 88         this.p = p; 89     } 90      91     public void run() 92     { 93         while (true)//消费者消费 94         { 95             p.consume(); 96         } 97     } 98      99 }100 public class ProducerConsumerDemo {101 102     public static void main(String[] args) 103     {104         // 创建共享资源105         Product p = new Product();106         // 两个生产者,两个消费者107         Producer producer = new Producer(p);108         Consumer consumer = new Consumer(p);109         Thread t0 = new Thread(producer);110         Thread t1 = new Thread(producer);111         112         Thread t2 = new Thread(consumer);113         Thread t3 = new Thread(consumer);114         115         t0.start();116         t1.start();117         t2.start();118         t3.start();119     }120 }
View Code

多次运行会发现,结果正是我们最初想要的:生产一个就消费一个!

原因很简单:notifyAll会唤醒线程池中所有的线程,假如t0生产了一次,就会唤醒t1,t2,t3,如果t1抢到cpu执行权就会判断标记等待,然后醒着的消费线程抢到执行权, 就去消费一次,然后唤醒所有等待的线程,同样,因为消费了一次,只要消费线程抢到cpu执行权就会根据标记去等待,生产者线程抢到cpu执行权就会判断标记,然后去生产,如此循环!至此,问题得到解决!

六、JDK1.5之后的新加锁方式返目录回

在API文档中有一个Lock接口:

翻译:Lock 实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。

方法如下:

lock方法获取锁,unlock方法释放锁。

以前使用同步代码块和一个对象相结合的方式,实现线程同步,有了Lock接口以后,可以通过一个锁对象完成线程的同步。

使用Lock接口的一个已知实现类ReentrantLock来改写上面的多生产多消费程序,首先看看ReentrantLock的API文档里写的怎么用这个类:

而Lock接口的描述里面还提到:Lock 可以支持多个相关的 Condition 对象,Condition的API:

翻译:Condition将Object锁的监视器方法:wait,notify和notifyAll分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合,为每个对象提供多个等待set.其中,Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。

大致意思就是把这些锁的方法wait,notify和notifyAll封装在Condition中,而锁Lock和Condition是什么关系呢?在上面Lock方法的截图中:

即newCondition方法返回绑定到此Lock实例的新 Condition实例,所以Lock和Condition就是通过这个方法绑定一起,然后就能通过Condition实例调用与该锁想关的wait,notify和notifyAll方法。

但是注意wait,notify和notifyAll方法在Condition中的名称有所改变,但是功能是一样的:

好了,根据上面的知识,得出修改的代码:

  1 package thread.demo;  2   3 import java.util.concurrent.locks.Condition;  4 import java.util.concurrent.locks.Lock;  5 import java.util.concurrent.locks.ReentrantLock;  6   7 /*  8  * 生产者消费者问题  9  */ 10 class NewProduct 11 { 12     private String name;// 产品名称 13     private int number = 1; // 产品编号 14     private boolean notEmpty = false;  15      16     // 创建一个锁对象 17     Lock lock = new ReentrantLock(); 18      19     // 通过已有的锁获取该锁上的监视器对象 20     Condition c = lock.newCondition(); 21     public void produce(String name) 22     { 23         lock.lock();  24         try 25         { 26             //如果有产品,可以停止生产一会 27             while (notEmpty) 28             { 29                 /* 30                 try  31                 { 32                     this.wait(); 33                 }  34                 catch (InterruptedException e)  35                 { 36                     e.printStackTrace(); 37                 } 38                 */ 39  40                 try { 41                     c.await(); 42                 } catch (InterruptedException e) { 43                     e.printStackTrace(); 44                 } 45             } 46             // 如果没有产品,就无需等待,直接生产 47             // 生产的产品名称  48             this.name = name + number; 49             // 编号递增 50             number++; 51             // 输出生产的产品信息:线程名(对应在某一个生产者)+产品名 52             System.out.println(Thread.currentThread().getName() + " 生产出: " + this.name); 53             // 生产完了以后,就有了产品 54             notEmpty = true; 55             //通知其他线程 56             //notify(); 57             //notifyAll(); 58             c.signalAll(); 59         } 60         finally 61         { 62             lock.unlock(); 63         } 64     } 65      66     public void consume() 67     { 68         lock.lock(); 69         try 70         { 71             // 如果没有产品,无法消费,等待 72             while (!notEmpty) 73             { 74                 /* 75                 try  76                 { 77                     this.wait(); 78                 }  79                 catch (InterruptedException e)   80                 { 81                     e.printStackTrace(); 82                 } 83                 */ 84                 try { 85                     c.await(); 86                 } catch (InterruptedException e) { 87                     e.printStackTrace(); 88                 } 89             } 90             //打印产品被消费的信息:线程名(对应着某一个消费者) + 产品名 91             System.out.println(Thread.currentThread().getName() + "消费了:->  " + this.name); 92             //消费完了,通知其他线程 93             notEmpty = false; 94             //notify(); 95             //notifyAll(); 96             c.signalAll(); 97         } 98         finally 99         {100             lock.unlock();101         }102     }103 }104 105 // 创建生产者线程106 class NewProducer implements Runnable107 {108     private NewProduct p;109     NewProducer(NewProduct p2)110     {111         this.p = p2;112     }113     114     public void run()115     {116         while (true)117         {118             p.produce("bread"); // 假如生产面包119         }120     }121     122 }123 124 //创建消费者线程125 class NewConsumer implements Runnable126 {127     private NewProduct p;128     NewConsumer(NewProduct p)129     {130         this.p = p;131     }132     133     public void run()134     {135         while (true)//消费者消费136         {137             p.consume();138         }139     }140     141 }142 public class LockDemo {143 144     public static void main(String[] args) 145     {146         // 创建共享资源147         NewProduct p = new NewProduct();148         // 两个生产者,两个消费者149         NewProducer NewProducer = new NewProducer(p);150         NewConsumer NewConsumer = new NewConsumer(p);151         Thread
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表