技术开发 频道

Java并发编程:同步

  【IT168 技术文章】线程除要对共享数据保证互斥性访问外,往往还需保证线程的操作按照特定顺序进行。解决多线程按照特定顺序访问共享数据的技术称作同步。同步技术最常见的编程范式是同步保护块。这种编程范式在操作前先检测某种条件是否成立,如成立则继续操作;如不成立则有两种选择,一种是简单的循环检测,直至此条件条件成立:

1 public void guardedOperation(){
2
3   while(!condition_expression){
4
5   System.out.println("Not ready yet, I have to wait again!");
6
7   }
8
9   }
10
11

  这种方法非常消耗CPU资源,任何情况下都不应该使用这种方法。另种更好的方式是条件不成立时调用Object.wait方法挂起当前线程,使它一直等待,直至另一个线程发出激活事件。当然该事件不一定是当前线程希望等待的事件。

1 public synchronized guardedOperation() {
2
3   while(!condition_expression) {
4
5   try {
6
7   wait();
8
9   } catch (InterruptedException e) {}
10
11   }
12
13   System.out.println("Now, condition met and it is ready!");
14
15   }
16
17

  这儿有两点需要特别注意:

  1.要在循环检测中等待条件满足,这是因为中断事件并不一定是当前线程所期望的事件。线程等待被中断后应该继续检测条件,以便决定是否进入下一轮等待。

  2.当前线程在对wait方法调用时,必须是已经获得wait方法所属对象的内部锁。也就是说,wait方法必须在互斥块或者互斥方法体内调用,否则就会发生NotOwnerException错误。这种限制和前面所说的同步前提是互斥的说法是一致的。

  上面代码更通用的写法是:

1 ...
2
3   synchronized(lock){
4
5   while(!condition_expression){
6
7   try{
8
9   lock.wait();
10
11   }catch(InterruptedException ie){}
12
13   }
14
15   System.out.println("Now, condition met and it is ready!");
16
17   }
18
19   ...
20
21

  线程在synchronized语句获取对象的内部锁之后,在synchronized代码块期间就拥有了内部锁。当判断条件不成立时,可以调用该对象的wait方法进入等待状态。

  注意持有锁的线程在调用wait方法进入等待状态之后,会自动释放持有的锁。这样做的目的是允许其他的线程进入临界区继续操作,以防止死锁的发生。

  举生产者和消费者的例子。如果消费者在检查时发现没有产品生成,则调用wait方法等待生产者生产。如果此时消费者不释放该锁,生产者就会因为获取不到该锁而处于阻塞状态。而此时消费者却在等待生产者生产出产品来,这样双方就进入死锁状态。因此wait方法需要在挂起线程后释放该线程所拥有的锁。

  当wait方法调用后,线程进入等待状态,直至未来某刻其他线程获得该锁并调用其invokeAll(或invoke)方法将其唤醒。该线程通过如下类似的代码激活等待在此锁上的线程:

1 public synchronized notifyOperation(){
2
3   condition_expression=true;
4
5   notifyAll();
6
7   }
8
9

  假设线程C因检测到某种条件不满足而进入等待状态,激活C线程的P线程往往需要和C线程建立“发生过”关系。也就是说程序期望线程P和C之间按照先P后C的顺序执行。对于生产者和消费者例子来说,P就是生产者,C就是消费者,它们之间存在从P到C的“发生过”关系。

  线程P在调用notify或者notifyAll方法时需要首先获得该对象的锁,因此这些代码也需要放在synchronized代码体内。上面的激活方法更通用的写法是:

1 ...
2
3   synchronized(lock){
4
5   condition_expression=true;
6
7   lock.notifyAll();
8
9   }
10
11   ...
12
13

  现举生产者和消费者之间同步的例子。为了简化,假设生产者和消费者之间只共享一个容器。生产者生产出对象后放在在该容器中,而消费者从该容器中获取该对象进行消费。消费者和生者之间往往需要建立双向的“发生过”关系,即消费者只有在有东西才能消费,而生产者只有在有存放空间时才能生产。这儿为了简化,只假定保证消费者有东西可消费,生产者不管是否有空间可存放,只是将对象生产出来放在容器中。下面是这个例子的代码:

  1  public class TankContainer{
  2
  3   private Tank tank;
  4
  5   public synchronized void putTank(Tank tank){
  6
  7   //Dont bother to check whether it has room.
  8
  9   this.tank=tank;
10
11   notifyAll();
12
13   }
14
15   public synchronized Tank getTank(){
16
17   //Check whether there's tank to consume
18
19   while(tank==null){
20
21   //No tank yet, let's wait.
22
23   try{
24
25   wait();
26
27   }catch(InterruptedException e){}
28
29   }
30
31   Tank retValue=tank.
32
33   tank=null; //Clear tank.
34
35   return retValue;
36
37   }
38
39   }
40
41   public ProducerThread extends Thread{
42
43   //Shared TankContainer
44
45   private TankContainer container;
46
47   public ProducerThread(TankContainer container){
48
49   this.container=container;
50
51   }
52
53   ...
54
55   public void run(){
56
57   while(true){
58
59   Tank tank=produceTank();
60
61   container.putTank(tank);
62
63   }
64
65   }
66
67   ...
68
69   }
70
71   public ConsumerThread extends Thread{
72
73   //Shared TankContainer
74
75   private TankContainer container;
76
77   public ConsumerThread(TankContainer container){
78
79   this.container=container;
80
81   }
82
83   ...
84
85   public void run(){
86
87   while(true){
88
89   Tank tank=container.getTank();
90
91   consumeTank(tank);
92
93   }
94
95   }
96
97   ...
98
99   }
100
101   public class ProducerConsumer{
102
103   public static void main(String[]args){
104
105   TankContainer container=new TankContainer();//Shared TankContainer
106
107   new ProducerThread(container).start(); //Start to produce goods in its own thread.
108
109   new ConsumerThread(container).start(); //Start to consume goods in its own thread.
110
111   }
112
113   }
114
115

  总结一下,同步编程时应该要记住下面几条:

  1.两个线程应该获取同一个对象的锁。这是获取同步的互斥性前提。

  2.消费者线程应在循环体内检测条件是否成立。

  3.消费者线程在条件没有满足时应调用锁对象的wait方法等待。

  4.wait方法被中断后应进入下一轮条件检测循环。

  5.生产者线程应该在其操作或结束返回之前调用锁对象的notify或notifyAll方法激活等待线程。

  补充一下notify和notifyAll方法的区别。notify激活等待队列上的下一个线程。而notifyAll则激活所有等待线程。在生产者释放锁之后,这些被激活线程竞争获取该锁。获得该锁的线程只有一个,它从wait中返回,进入下一轮条件检测。没有获得锁的线程继续进入等待状态,等待下一次激活事件。

  Java中除了通过互斥和同步技术来获得代码线程安全共性以外,还通过所谓恒量对象(immutable objects)的模式获取线程安全性。其基本原理是恒量对象在创建完毕后就只能读取,就像final对象一样。

0
相关文章