Java多线程技术学习笔记(二)
- 线程间的通信示例
- 等待唤醒机制
- 等待唤醒机制的优化
- 线程间通信经典问题:多生产者多消费者问题
- 多生产多消费问题的解决
- JDK1.5之后的新加锁方式
- 多生产多消费问题的新解决办法
- sleep和wait的区别
- 停止线程的方式
- 守护线程
- 线程的其他知识点
多个线程在处理同一资源,任务却不同。
假设有一堆货物,有一辆车把这批货物往仓库里面运,另外一辆车把前一辆车运进仓库的货物往外面运。这里货物就是同一资源,但是两辆车的任务却不同,一个是往里运,一个是往外运。
下面举例子来逐步展示线程间通信:首先建立一个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 Runnable
28 {
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 else
47 {
48 r.setName("Lucy");
49 r.setSex("Female");
50 }
51 x = (x + 1) % 2; //变换x的值,使得切换输入不同的对象
52 }
53 }
54 }
55
56 class Output implements Runnable
57 {
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 args
78 */
79 public static void main(String[] args)
80 {
81 //建立共享数据Person r,输入和输出都操作对象 r
82 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 }
执行结果就是一直不断输出,会发现有如下类似现象:
问题来了,程序中明明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 else
24 {
25 r.setName("Lucy");
26 r.setSex("Female");
27 }
28 }
29 x = (x + 1) % 2; //变换x的值,使得切换输入不同的对象
30 }
31 }
32 }
33
34 class Output implements Runnable
35 {
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 }
多次运行之后可以验证,输出正常:
但是注意到,输出是连续一堆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 args
116 */
117 public static void main(String[] args)
118 {
119 //建立共享数据Person r,输入和输出都操作对象 r
120 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 }
运行结果:
达到了预期。
再考虑上面写的代码,其实并不好,同步的目的是为了防止某个线程对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 args
117 */
118 public static void main(String[] args)
119 {
120 //建立共享数据Person r,输入和输出都操作对象 r
121 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 }
功能与前面代码其实是一样的。
这个问题很直接:就是一堆生产者生产产品,同时一堆消费者在消费产品!这一堆生产者和消费者对应程序中的多个线程,而产品就对应着这一堆线程共同操作的资源或者叫做共享数据。
我们想达到的目的是生产一件商品,消费一件,生产消费彼此交替!
首先来看一个生产者,一个消费者的例子,即生产一个就消费一个:
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 }
运行结果:
这其实就是前面等待唤醒机制的另一种展示!下面在此代码的基础上改成多生产者,多消费者的示例:
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 }
多次运行,会出现下面类似结果:
产生的问题:
- 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 }
- 假设生产者(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 }
多次运行会发现,结果正是我们最初想要的:生产一个就消费一个!
原因很简单:notifyAll会唤醒线程池中所有的线程,假如t0生产了一次,就会唤醒t1,t2,t3,如果t1抢到cpu执行权就会判断标记等待,然后醒着的消费线程抢到执行权, 就去消费一次,然后唤醒所有等待的线程,同样,因为消费了一次,只要消费线程抢到cpu执行权就会根据标记去等待,生产者线程抢到cpu执行权就会判断标记,然后去生产,如此循环!至此,问题得到解决!
在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 Runnable
107 {
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 Runnable
126 {
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 t0 = new Thread(NewProducer);
152 Thread t1 = new Thread(NewProducer);
153
154 Thread t2 = new Thread(NewConsumer);
155 Thread t3 = new Thread(NewConsumer);
156
157 t0.start();
158 t1.start();
159 t2.start();
160 t3.start();
161 }
162 }
之前采用同步代码块的时候 ,多个线程都放在同一个锁下面进行同步,而这个锁只能具有一组监视器(wait,notify,notifyAll),同时监视着生产线程和消费线程,比如说这个监视器的wait即能使生产线程处于等待,也可以使消费线程进入等待。同理: 唤醒方法notify和notifyAll对两种线程都起作用。目前线程别分为两类:一组线程(t0,t1)负责生产,一组线程(t2, t3)负责消费。
现在的思路: 用Lock接口,把两组(共计4个)线程放在同一个锁下同步,但是绑定两个监视器分别监视生产线程和消费线程。关键代码有注释:
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 //通过已有的锁获得两组监视器,一组监视生产者,一组监视消费者
22 Condition producerCon = lock.newCondition();
23 Condition consumerCon = lock.newCondition();
24 public void produce(String name)
25 {
26 lock.lock();
27 try
28 {
29 while (notEmpty)
30 {
31 try {
32 producerCon.await();//调用生产者监视,只对生产线程有效
33 } catch (InterruptedException e) {
34 e.printStackTrace();
35 }
36 }
37 // 如果没有产品,就无需等待,直接生产
38 // 生产的产品名称
39 this.name = name + number;
40 // 编号递增
41 number++;
42 // 输出生产的产品信息:线程名(对应在某一个生产者)+产品名
43 System.out.println(Thread.currentThread().getName() + " 生产出: " + this.name);
44 // 生产完了以后,就有了产品
45 notEmpty = true;
46 //通知其他线程
47 consumerCon.signal();//调用消费者监视器,只能唤醒消费者
48 }
49 finally
50 {
51 lock.unlock();
52 }
53 }
54
55 public void consume()
56 {
57 lock.lock();
58 try
59 {
60 // 如果没有产品,无法消费,等待
61 while (!notEmpty)
62 {
63 try {
64 consumerCon.await();//调用消费者的监视器,使消费线程等待
65 } catch (InterruptedException e) {
66 e.printStackTrace();
67 }
68 }
69 //打印产品被消费的信息:线程名(对应着某一个消费者) + 产品名
70 System.out.println(Thread.currentThread().getName() + "消费了:-> " + this.name);
71 //消费完了,通知其他线程
72 notEmpty = false;
73 //通知生产者
74 producerCon.signal();
75 }
76 finally
77 {
78 lock.unlock();
79 }
80 }
81 }
82
83 // 创建生产者线程
84 class NewProducer implements Runnable
85 {
86 private NewProduct p;
87 NewProducer(NewProduct p2)
88 {
89 this.p = p2;
90 }
91
92 public void run()
93 {
94 while (true)
95 {
96 p.produce("bread"); // 假如生产面包
97 }
98 }
99
100 }
101
102 //创建消费者线程
103 class NewConsumer implements Runnable
104 {
105 private NewProduct p;
106 NewConsumer(NewProduct p)
107 {
108 this.p = p;
109 }
110
111 public void run()
112 {
113 while (true)//消费者消费
114 {
115 p.consume();
116 }
117 }
118
119 }
120 public class LockDemo {
121
122 public static void main(String[] args)
123 {
124 // 创建共享资源
125 NewProduct p = new NewProduct();
126 // 两个生产者,两个消费者
127 NewProducer NewProducer = new NewProducer(p);
128 NewConsumer NewConsumer = new NewConsumer(p);
129 Thread t0 = new Thread(NewProducer);
130 Thread t1 = new Thread(NewProducer);
131
132 Thread t2 = new Thread(NewConsumer);
133 Thread t3 = new Thread(NewConsumer);
134
135 t0.start();
136 t1.start();
137 t2.start();
138 t3.start();
139 }
140 }
下面做一个简单的总结:
Lock接口:
- 不用自己实现,查看API文档可以发现,库里面已经有几个已经实现了该接口的类,拿来用即可;
- 它的替代了同步代码块或者同步函数;
- 将同步的隐式操作变为显示操作;
- 更为灵活,可以一个锁上加上多组监视器;
- lock():获取锁
- unlock:释放锁, 通常需要定义在finally代码块中(因为有时某些线程获取锁以后,程序在执行到unlock之前可能出现问题,但是别的线程还要执行,需要获取锁,所以必须保证锁能够得到释放)
Condition接口:
- 替代了Object中的wait,notify和notifyAll方法,将这些监视器方法单独进行了封装,变成了Condition类型的监视器对象。
- 可以和任意的锁组。
- await();
- signal();
- signalAll();
下面看一看Java Condition API文档中给出的范例,本人作出了简单的注释:
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 class BoundedBuffer {
8 // 创建ReentrantLock锁对象lock
9 final Lock lock = new ReentrantLock();
10 // lock锁绑定一个监视器监视数组是否存满
11 final Condition notFull = lock.newCondition();
12 // lock锁绑定一个监视器监视数组是否为空
13 final Condition notEmpty = lock.newCondition();
14 // 建立一个可以存储100个任何对象的数组
15 final Object[] items = new Object[100];
16 int putptr, takeptr, count;
17
18 // 向数组中填充对象 x,以供存储线程调用
19 public void put(Object x) throws InterruptedException {
20 lock.lock(); // 获取锁
21 try {
22 // 当数组存满,调用存满监视器的await,使存储线程等待
23 while (count == items.length)
24 notFull.await();
25 // 当数组没有存满,就存入一个对象
26 items[putptr] = x;
27 if (++putptr == items.length) putptr = 0;
28 ++count;
29 // 存储了对象之后,就去唤醒下面的取出元素的线程
30 // 大白话就是:俺里面不是空的,你可以来取元素了
31 notEmpty.signal();
32 } finally {
33 lock.unlock();
34 }
35 }
36
37 // 取出元素的函数,以供取元素线程调用
38 public Object take() throws InterruptedException {
39 // 获取锁
40 lock.lock();
41 try {
42 // 当元素个数为0 时,通知取元素线程等待
43 // 大白话:里面是空的了,你等会再来取!
44 while (count == 0)
45 notEmpty.await();
46 // 当元素个数不为0,取出元素
47 Object x = items[takeptr];
48 if (++takeptr == items.length) takeptr = 0;
49 --count;
50 // 取出一个元素之后,就通知存储线程
51 // 大白话:里面没有满,你可以来存元素了!
52 notFull.signal();
53 return x;
54 } finally {
55 lock.unlock();
56 }
57 }
58 }
59
- wait可以指定时间,也可以不指定时间,sleep必须指定时间
- 在同步时,对于cpu执行权和锁的处理不同,
- wait:释放执行权,释放锁
- sleep:释放执行权,不释放锁
- stop
- run方法结束
在任务中通常都有循环结构,只要控制住循环就可以结束任务。
控制循环通常就用定义标记来完成。
1 package thread.demo;
2
3 class StopThread implements Runnable
4 {
5 private boolean flag = true;
6 public void run ()
7 {
8 while (flag)
9 {
10 System.out.println(Thread.currentThread().getName() + " run...");
11 }
12 }
13
14 public void setFlag()
15 {
16 flag = false;
17 }
18 }
19 public class StopThreadDemo
20 {
21 /**
22 * @param args
23 */
24 public static void main(String[] args)
25 {
26 StopThread st = new StopThread();
27 Thread t0 = new Thread(st);
28 Thread t1 = new Thread(st);
29 t0.start();
30 t1.start();
31
32 for (int x = 0; x < 50; x++)
33 {
34 if (x == 49)
35 {
36 st.setFlag();
37 break;
38 }
39 System.out.println(Thread.currentThread().getName() + "..." + x);
40 }
41 System.out.println("over");
42 }
43
44 }
可以看出,虽然主线程结束了,但是其他线程还要执行,然后根据标志位,自己停止。如果不设置标记,如果主线程over,其他线程就无法停止了。如果我们把第34行的改为x == 50,即这个条件就无法满足,标记为无法改变,虽然主线程结束,但是自定义的线程会继续执行。
假如把上面的程序改为如下:
1 package thread.demo;
2
3 class StopThread implements Runnable
4 {
5 private boolean flag = true;
6 public synchronized void run ()
7 {
8 while (flag)
9 {
10 try {
11 wait();
12 } catch (InterruptedException e) {
13 System.out.println(Thread.currentThread().getName() + "..." + e);
14 }
15
16 System.out.println(Thread.currentThread().getName() + " run...");
17 }
18 }
19
20 public void setFlag()
21 {
22 flag = false;
23 }
24 }
25 public class StopThreadDemo
26 {
27 /**
28 * @param args
29 */
30 public static void main(String[] args)
31 {
32 StopThread st = new StopThread();
33 Thread t0 = new Thread(st);
34 Thread t1 = new Thread(st);
35 t0.start();
36 t1.start();
37
38 for (int x = 0; x < 50; x++)
39 {
40 if (x == 49)
41 {
42 st.setFlag();
43 break;
44 }
45 System.out.println(Thread.currentThread().getName() + "..." + x);
46 }
47 System.out.println("over");
48 }
49
50 }
发现主线程结束了,但是其他线程却没能停下来,很简单:线程0和线程1进入run之后,都在第11行被置为等待状态,而又没有其他线程来唤醒它们,于是一直等待,即使主线程结束,还是等待。
下面采用一种方法:
interrupt方法并不是字面上理解的中断线程(岂不是和stop一样?),注意红框中:如果线程在调用Object类的wait(), wait(long)或者wait(long, int)方法,或者该类的join(), join(long), join(long, int)或者sleep(long), sleep(long, int)方法过程中受阻,则这种受阻的状态会被强制清除,然后收到一个InterruptedException.
意思就是interrupt()方法,会把线程从wait或者sleep状态中强制恢复到运行状态中来,让线程具备cpu的执行资格,由于这个“强制”通常是不正常唤醒(正常唤醒notify/notifyAll),所以抛出异常InterruptedException,记得要处理!改动上面程序如下:
1 package thread.demo;
2
3 class StopThread implements Runnable
4 {
5 private boolean flag = true;
6 public synchronized void run ()
7 {
8 while (flag)
9 {
10 try {
11 wait();
12 } catch (InterruptedException e) {
13 // 打印异常信息
14 System.out.println(Thread.currentThread().getName() + "..." + e);
15 flag = false; // 处理异常
16 }
17
18 System.out.println(Thread.currentThread().getName() + " run...");
19 }
20 }
21
22 public void setFlag()
23 {
24 flag = false;
25 }
26 }
27 public class StopThreadDemo
28 {
29 /**
30 * @param args
31 */
32 public static void main(String[] args)
33 {
34 StopThread st = new StopThread();
35 Thread t0 = new Thread(st);
36 Thread t1 = new Thread(st);
37 t0.start();
38 t1.start();
39
40 for (int x = 0; x < 50; x++)
41 {
42 if (x == 49)
43 {
44 //st.setFlag();
45 t0.interrupt();
46 t1.interrupt();
47 break;
48 }
49 System.out.println(Thread.currentThread().getName() + "..." + x);
50 }
51 System.out.println("over");
52 }
53
54 }
看到线程都结束了,由于采用了interrupt方法,抛出了图示的异常!
首先还是看API文档关于守护线程的描述,Thread中的一个方法:
该方法将该线程标记为守护线程。当正在运行的线程都是守护线程的时候,Java 虚拟机退出。
该方法必须在启动线程前调用。
例如把上面代码的第47行的t1线程的中断注释掉,但是在在第38行将t1标记为守护线程(可以理解为后台线程):
1 package thread.demo;
2
3 class StopThread implements Runnable
4 {
5 private boolean flag = true;
6 public synchronized void run ()
7 {
8 while (flag)
9 {
10 try {
11 wait();
12 } catch (InterruptedException e) {
13 // 打印异常信息
14 System.out.println(Thread.currentThread().getName() + "..." + e);
15 flag = false; // 处理异常
16 }
17
18 System.out.println(Thread.currentThread().getName() + " run...");
19 }
20 }
21
22 public void setFlag()
23 {
24 flag = false;
25 }
26 }
27 public class StopThreadDemo
28 {
29 /**
30 * @param args
31 */
32 public static void main(String[] args)
33 {
34 StopThread st = new StopThread();
35 Thread t0 = new Thread(st);
36 Thread t1 = new Thread(st);
37 t0.start();
38 t1.setDaemon(true);
39 t1.start();
40
41 for (int x = 0; x < 50; x++)
42 {
43 if (x == 49)
44 {
45 //st.setFlag();
46 t0.interrupt();
47 // t1.interrupt();
48 break;
49 }
50 System.out.println(Thread.currentThread().getName() + "..." + x);
51 }
52 System.out.println("over");
53 }
54
55 }
由于线程t1没有能清除wait状态会一直等待,但是由于它是守护线程,Java虚拟机会退出,程序也会停止,但是t1线程就不会再抛出异常:
注意:守护线程运行时候正常线程一样,抢夺cpu执行权,就是在结束的时候,正常线程需要手动去结束:即stop,或者run方法结束等,但是守护线程是随着虚拟机退出而结束的。就好比我们的操作系统有很多后台进程是不允许我们去操作,在系统运行期间会一直存在并抢夺cpu执行权,但是他是随着系统关闭而停止的。
1 package thread.demo;
2
3
4 class Demo implements Runnable
5 {
6 public void run ()
7 {
8 for (int i = 0; i < 50; i++)
9 {
10 System.out.println(Thread.currentThread().getName() + "..." + i);
11 }
12 }
13 }
14 public class JoinDemo
15 {
16
17 /**
18 * @param args
19 * @throws InterruptedException
20 */
21 public static void main(String[] args) throws InterruptedException
22 {
23 Demo d = new Demo();
24 Thread t0 = new Thread(d);
25 Thread t1 = new Thread(d);
26
27 t0.start();
28 t0.join(); // t0线程申请加入进来运行,此时主线程会释放执行权和执行资格。
29 t1.start();
30 //t0.join(); // t0线程申请加入进来运行,此时主线程会释放执行权和执行资格。
31
32 for (int i = 0; i < 50; i++)
33 {
34 System.out.println(Thread.currentThread().getName() + "..." + i);
35 }
36 }
37
38 }
多次运行会发现,都是先执行线程0,这是因为线程0调用join方法之后,主线程会释放执行权和执行资格,一直等到线程0执行完,主线程才重新获取执行资格,如果是t1调用join方法,同理,主线程也会一直等待t1执行完才重新获取执行资格!
所以join方法常用来临时加入一个线程。
线程Thread有setPriority方法是用来设定线程的优先级,优先级越高,被随机执行的几率就越大!可以通过线程的toString方法看到线程权限:
1 package thread.demo;
2
3
4 class Demo implements Runnable
5 {
6 public void run ()
7 {
8 for (int i = 0; i < 50; i++)
9 {
10 System.out.println(Thread.currentThread().toString() + "..." + i);
11 }
12 }
13 }
14 public class JoinDemo
15 {
16
17 /**
18 * @param args
19 * @throws InterruptedException
20 */
21 public static void main(String[] args) throws InterruptedException
22 {
23 Demo d = new Demo();
24 Thread t0 = new Thread(d);
25 Thread t1 = new Thread(d);
26
27 t0.start();
28 t0.join(); // t0线程申请加入进来运行,此时主线程会释放执行权和执行资格。
29 t1.start();
30 //t0.join(); // t0线程申请加入进来运行,此时主线程会释放执行权和执行资格。
31
32 for (int i = 0; i < 50; i++)
33 {
34 System.out.println(Thread.currentThread().getName() + "..." + i);
35 }
36 }
37
38 }
关于线程的优先级有三个字段:
所以setPriority(MAX_PRIORITY)和setPriority(10)的效果是一样的.
例如在上述代码中线程1启动后加入:
t1.setPriority(10);
还可以通过线程的构造函数将线程命名和将线程分组:
分组作用:加入有10个线程都处于等待状态,先要调用interrupt方法将这10个线程全部强制唤醒就需要调用10次,显然麻烦,如果将这10个线程封装在一个组里面,对着一个组调用interrupt就能够将组里面的线程全部进行中断状态清除(即唤醒)
还有一个方法:
暂停当前线程,释放执行权,避免某一个线程一直占有cpu执行权,用的不多.
参考:传智播客Java SE教程,李刚《疯狂Java讲义》