Java并发编程(6)-同步

来源:互联网 发布:cf系统检测到数据异常 编辑:程序博客网 时间:2024/05/16 15:39

     线程除要对共享数据保证互斥性访问外,往往还需保证线程的操作按照特定顺序进行。解决多线程按照特定顺序访问共享数据的技术称作同步。同步技术最常见的编程范式是同步保护块。这种编程范式在操作前先检测某种条件是否成立,如成立则继续操作;如不成立则有两种选择,一种是简单的循环检测,直至此条件条件成立:
public void guardedOperation(){
     while(!condition_expression){
          System.out.println("Not ready yet, I have to wait again!");
}
}
        这种方法非常消耗CPU资源,任何情况下都不应该使用这种方法。另种更好的方式是条件不成立时调用Object.wait方法挂起当前线程,使它一直等待,直至另一个线程发出激活事件。当然该事件不一定是当前线程希望等待的事件。
public synchronized guardedOperation() {
    while(!condition_expression) {
        try {
            wait();
        } catch (InterruptedException e) {}
    }
    System.out.println("Now, condition met and it is ready!");
}
         这儿有两点需要特别注意:
(1)要在循环检测中等待条件满足,这是因为中断事件并不一定是当前线程所期望的事件。线程等待被中断后应该继续检测条件,以便决定是否进入下一轮等待。
(2)当前线程在对wait方法调用时,必须是已经获得wait方法所属对象的内部锁。也就是说,wait方法必须在互斥块或者互斥方法体内调用,否则就会发生NotOwnerException错误。这种限制和前面所说的同步前提是互斥的说法是一致的。
        上面代码更通用的写法是:
...
synchronized(lock){
   while(!condition_expression){
      try{
         lock.wait();
      }catch(InterruptedException ie){}
   }
   System.out.println("Now, condition met and it is ready!");
}
...
        线程在synchronized语句获取对象的内部锁之后,在synchronized代码块期间就拥有了内部锁。当判断条件不成立时,可以调用该对象的wait方法进入等待状态。
        注意持有锁的线程在调用wait方法进入等待状态之后,会自动释放持有的锁。这样做的目的是允许其他的线程进入临界区继续操作,以防止死锁的发生。

       举生产者和消费者的例子。如果消费者在检查时发现没有产品生成,则调用wait方法等待生产者生产。如果此时消费者不释放该锁,生产者就会因为获取不到该锁而处于阻塞状态。而此时消费者却在等待生产者生产出产品来,这样双方就进入死锁状态。因此wait方法需要在挂起线程后释放该线程所拥有的锁。
        当wait方法调用后,线程进入等待状态,直至未来某刻其他线程获得该锁并调用其invokeAll(或invoke)方法将其唤醒。该线程通过如下类似的代码激活等待在此锁上的线程:
public synchronized notifyOperation(){
   condition_expression=true;
   notifyAll();
}
        假设线程C因检测到某种条件不满足而进入等待状态,激活C线程的P线程往往需要和C线程建立“发生过”关系。也就是说程序期望线程P和C之间按照先P后C的顺序执行。对于生产者和消费者例子来说,P就是生产者,C就是消费者,它们之间存在从P到C的“发生过”关系。
        线程P在调用notify或者notifyAll方法时需要首先获得该对象的锁,因此这些代码也需要放在synchronized代码体内。上面的激活方法更通用的

写法是:
...
synchronized(lock){
     condition_expression=true;
     lock.notifyAll();
}
...
        现举生产者和消费者之间同步的例子。为了简化,假设生产者和消费者之间只共享一个容器。生产者生产出对象后放在在该容器中,而消费者从该容器中获取该对象进行消费。消费者和生者之间往往需要建立双向的“发生过”关系,即消费者只有在有东西才能消费,而生产者只有在有存放空间时才能生产。这儿为了简化,只假定保证消费者有东西可消费,生产者不管是否有空间可存放,只是将对象生产出来放在容器中。下面是这个例子的代码:
public class TankContainer{
   private Tank tank;
   public synchronized void putTank(Tank tank){
      //Dont bother to check whether it has room.
      this.tank=tank;
      notifyAll();
   }
   public synchronized Tank getTank(){
      //Check whether there's tank to consume
      while(tank==null){
         //No tank yet, let's wait.
         try{
             wait();
         }catch(InterruptedException e){}
      }
      Tank retValue=tank.
      tank=null; //Clear tank.
      return retValue;
   }
}
public ProducerThread extends Thread{
//Shared TankContainer
private TankContainer container;
public ProducerThread(TankContainer container){
    this.container=container;
}
...
public void run(){
    while(true){
       Tank tank=produceTank();
       container.putTank(tank);  
    }
}
...
}
public ConsumerThread extends Thread{
//Shared TankContainer
private TankContainer container;
public ConsumerThread(TankContainer container){
    this.container=container;
}
...
public void run(){
    while(true){
      Tank tank=container.getTank();
      consumeTank(tank);    
    }
}
...
}

public class ProducerConsumer{
public static void main(String[]args){
    TankContainer container=new TankContainer();//Shared TankContainer
    new ProducerThread(container).start(); //Start to produce goods in its own thread.
    new ConsumerThread(container).start(); //Start to consume goods in its own thread.
}
}

        总结一下,同步编程时应该要记住下面几条:
(1)两个线程应该获取同一个对象的锁。这是获取同步的互斥性前提。
(2)消费者线程应在循环体内检测条件是否成立。
(3)消费者线程在条件没有满足时应调用锁对象的wait方法等待。
(4)wait方法被中断后应进入下一轮条件检测循环。
(5)生产者线程应该在其操作或结束返回之前调用锁对象的notify或notifyAll方法激活等待线程。
        补充一下notify和notifyAll方法的区别。notify激活等待队列上的下一个线程。而notifyAll则激活所有等待线程。在生产者释放锁之后,这些被激活线程竞争获取该锁。获得该锁的线程只有一个,它从wait中返回,进入下一轮条件检测。没有获得锁的线程继续进入等待状态,等待下一次激活事件。
        Java中除了通过互斥和同步技术来获得代码线程安全共性以外,还通过所谓恒量对象(immutable objects)的模式获取线程安全性。其基本原理是恒量对象在创建完毕后就只能读取,就像final对象一样。

原创粉丝点击