Java线程同步 - synchronized机制

在JDK5之前,Java的多线程(包括它的性能)一直是个软肋,只有synchronized、Thread.sleep()、Object.wait/notify这样有限的方法,而synchronized的效率还特别地低,开销比较大。JDK5在多线程上有了彻底的提高,其引进了并发编程大师Doug Lea的java.util.concurrent包,支持了现代CPU的CAS原语,不仅在性能上有了很大提升,在自由度上也有了更多的选择,此时J.U.C的效率在高并发环境下的效率远优于synchronized。但JDK6中对synchronized的内在机制做了大量显著的优化,加入了CAS的概念以及偏向锁、轻量级锁,使得synchronized的效率与J.U.C不相上下,并且官方说后面该关键字还有继续优化的空间,所以在现在JDK7的时代,synchronized已经成为一般情况下的首选,在某些特殊场景——如可中断的锁、条件锁、等待获得锁一段时间如果失败则停止——下,J.U.C是适用的,所以对于多线程研究来说,了解其原理以及各自的适用场景是必要的。

所以现在的状况是两者效率相差无几,而synchronized使用更简单、更不容易出错,所以其是专家组推荐的首选,除非需要用到J.U.C的特殊功能(可以参考我的上一篇文章)。

既然这样,那么我们就一起来看一下synchronized机制的一些要点。

首先需要明确的是Java中的多线程同步是通过锁的概念来体现。锁不是一个对象、不是一个具体的东西,而是一种机制的名称。锁机制需要保证如下两种特性:

  • 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。

  • 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。

基本概念及方法

下面谈一谈sychronized机制下的一些常用的方法:

wait()是使持有对象锁的线程释放锁;
wait(long)是使持有对象锁的线程释放锁时间为long(毫秒)后,再次获得锁,wait()和wait(0)等价;
notify()是唤醒一个正在等待该对象锁的线程,如果等待的线程不止一个,那么被唤醒的线程由jvm确定;
notifyAll是唤醒所有正在等待该对象锁的线程.

对于上述方法,只有在当前线程中才能使用,否则报运行时错误java.lang.IllegalMonitorStateException: current thread not owner.

另外还有一个关键字:sychronized:

synchronized(b){…};的意思是定义一个同步块,使用b作为资源锁。b.wait();的意思是临时释放锁,并阻塞当前线程,好让其他使用同一把锁的线程有机会执行,在这里要用同一把锁的就是b线程本身.这个线程在执行到一定地方后用notify()通知wait的线程,锁已经用完,待notify()所在的同步块运行完之后,wait所在的线程就可以继续执行.

关于使用这些方法一些需要注意的概念是:

  • 调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) {…} 代码段内。
  • 调用obj.wait()后,线程A就释放了obj的锁,否则线程B无法获得obj锁,也就无法在synchronized(obj) {…} 代码段内唤醒A。

  • 当obj.wait()方法返回后,线程A需要再次获得obj锁,才能继续执行。
    如果A1,A2,A3都在obj.wait(),则B调用obj.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定)。

  • obj.notifyAll()则能全部唤醒A1,A2,A3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,A1,A2,A3只有一个有机会获得锁继续执行,例如A1,其余的需要等待A1释放obj锁之后才能继续执行。

  • 当B调用obj.notify/notifyAll的时候,B正持有obj锁,因此,A1,A2,A3虽被唤醒,但是仍无法获得obj锁。直到B退出synchronized块,释放obj锁后,A1,A2,A3中的一个才有机会获得锁继续执行。

接下来是synchronized和wait()、notify()之间的关系:

  • 有synchronized的地方不一定有wait,notify

  • 有wait,notify的地方必有synchronized.这是因为wait和notify不是属于线程类,而是每一个对象都具有的方法,而且,这两个方法都和对象锁有关,有锁的地方,必有synchronized。

  • 另外,请注意一点:如果要把notify和wait方法放在一起用的话,必须先调用notify后调用wait,因为如果调用完wait,该线程就已经不是current thread了。如下例:

例子

最好以一个生产者/消费者的例子来演示一下synchronized的使用方法

Producer:

class Producer implements Runnable {
       private final List<Integer> taskQueue;
       private final int           MAX_CAPACITY;

       public Producer(List<Integer> sharedQueue, int size) {
          this.taskQueue = sharedQueue;
          this.MAX_CAPACITY = size;
       }

       @Override
       public void run(){
          int counter = 0;
          while (true){
             try {
                produce(counter++);
             } catch (InterruptedException ex) {
                ex.printStackTrace();
             }
          }
       }

       private void produce(int i) throws InterruptedException {
          synchronized (taskQueue) {
             while (taskQueue.size() == MAX_CAPACITY) {
                System.out.println("Queue is full " + Thread.currentThread().getName() + " is waiting , size: " + taskQueue.size());
                taskQueue.wait();
             }

             Thread.sleep(1000);
             taskQueue.add(i);
             System.out.println("Produced: " + i);
             taskQueue.notifyAll();
          }
       }
}

Consumer:

class Consumer implements Runnable {
       private final List<Integer> taskQueue;

       public Consumer(List<Integer> sharedQueue) {
          this.taskQueue = sharedQueue;
       }

       @Override
       public void run() {
          while (true) {
             try {
                consume();
            } catch (InterruptedException ex) {
                ex.printStackTrace();
             }
          }
       }

       private void consume() throws InterruptedException {
          synchronized (taskQueue) {
             while (taskQueue.isEmpty()) {
                System.out.println("Queue is empty " + Thread.currentThread().getName() + " is waiting , size: " + taskQueue.size());
                taskQueue.wait();
             }
             Thread.sleep(1000);
             int i = (Integer) taskQueue.remove(0);
             System.out.println("Consumed: " + i);
             taskQueue.notifyAll();
          }
       }
}

Test program:

public class ProducerConsumerExampleWithWaitAndNotify {
       public static void main(String[] args) {
          List<Integer> taskQueue = new ArrayList<Integer>();
          int MAX_CAPACITY = 5;
          Thread tProducer = new Thread(new Producer(taskQueue, MAX_CAPACITY), "Producer");
          Thread tConsumer = new Thread(new Consumer(taskQueue), "Consumer");
          tProducer.start();
          tConsumer.start();
       }
}

Reference: