【19】Java中的Locks


Lock是一个相似同步代码块(synchronized block)的线程同步机制。同步代码块而言,Lock能够作到更细粒度的控制。 Lock(或者其余高级同步机制)也是基于同步代码块(synchronized block),因此还不能彻底摒弃synchronized关键字。html

从Java 5开始,java.util.concurrent.locks包提供了几个Lock的实现类。你能够直接使用,前提是你知道如何使用它们,并且只有了解它们的内部机制,才能更好的使用它们。 更多的内容能够参考原做者的java.util.concurrent.locks.Lock相关文章,以及JDK API。java

一个简单的Lock

先来看看同步块的代码:安全

public class Counter{

  private int count = 0;

  public int inc(){
    synchronized(this){
      return ++count;
    }
  }
}

注意inc()方法中的synchronized(this)块。 这个代码块,能够确保同一时间,只有一个线程能够执行return ++count。 固然,同步块还有不少高级的用法,这里只是简单的用于保障++count的安全。并发

对于上面的Counter类,能够用Lock进行改写:this

public class Counter{

  private Lock lock = new Lock();
  private int count = 0;

  public int inc(){
    lock.lock();
    int newCount = ++count;
    lock.unlock();
    return newCount;
  }
}

Lock的lock()会让没有获得锁的线程进入等待,直到unlock()方法被调用。.net

下面看看一个简单的Lock实现:线程

public class Lock{

  private boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  public synchronized void unlock(){
    isLocked = false;
    notify();
  }
}

注意:,这里使用了一个while(isLocked)循环,起到了一个“自旋锁(spin lock)”的效果。 有关自旋锁以及wait()notify()的信息,能够去看看《线程间的信号处理》code

isLocked为true时,若是有线程调用lock()方法,那就会进入wait()等待。 此时,线程可能会被意外唤醒,从而退出wait()方法,因此,须要用循环来再次检查isLocked的条件。(这就是:伪唤醒htm

若是,isLocked为false,则线程不会进入while(isLocked)等待,而是会直接修改isLocked为true,从而达到加锁的效果。对象

当线程完成了临界区的代码,而且调用了unlock()。接着将isLocked条件置回false。而且经过notify()方法,唤醒等待线程,从而达到释放锁的效果。

重入锁

Java中的同步块是能够重入的(Reentrance)。 这就意味着,Java线程进入一个同步代码块,就能够得到对象上的监控器锁(monitor),当这个线程去访问同一个监控器锁保护的同步代码块时,就能够直接进入了。

来看看这个例子:

public class Reentrant{

  public synchronized outer(){
    inner();
  }

  public synchronized inner(){
    //do something
  }
}

注意,outer()inner()都声明为synchronized,这在Java中,就等价于synchronized(this)。 若是一个线程在outer()方法中调用了inner()方法。那么,因为这两个方法,实际都是由同一个监控器(monitor)管理的(this),因此,能够直接进入inner()方法。

若是一个线程已经持有了某个监控器,那么它就能够访问这个监控器保护的全部同步代码块。而这种特性就称之为“重入”,线程能够从新进入已经得到的锁所保护的代码块。

以前,展现的Lock实现,是一个不可重入锁。 而若是,咱们重写一个像下面这样的Reentrant实现,那么当线程调用outer()方法将会被inner()内的lock.lock()阻塞住。

public class Reentrant2{

  Lock lock = new Lock();

  public outer(){
    lock.lock();
    inner();
    lock.unlock();
  }

  public synchronized inner(){
    lock.lock();
    //do something
    lock.unlock();
  }
}

当某个线程调用outer()方法,会首先锁住Lock实例。 接着,调用inner()方法。而inner()会再次对Lock实例进行加锁。 因为Lock实例已经在out()方法中加锁了,因此这里就会失败,并且会致使线程一直阻塞下去。

线程第二次调用lock()方法期间没有调用unlock()从而致使上述问题。 因此,咱们再来看看lock()方法的实现:

public class Lock{

  boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  ...
}

内部循环的条件,决定着是否容许退出lock()方法。 这里仅仅是判断是否处于加锁状态,而不关心是哪一个线程持有着锁。

因此,须要对Lock作一些改造:

public class Lock{

  boolean isLocked = false;
  Thread  lockedBy = null;
  int     lockedCount = 0;

  public synchronized void lock()
  throws InterruptedException{
    Thread callingThread = Thread.currentThread();
    while(isLocked && lockedBy != callingThread){
      wait();
    }
    isLocked = true;
    lockedCount++;
    lockedBy = callingThread;
  }


  public synchronized void unlock(){
    if(Thread.curentThread() == this.lockedBy){
      lockedCount--;

      if(lockedCount == 0){
        isLocked = false;
        notify();
      }
    }
  }

  ...
}

如今,自旋循环的条件能够直接放行已经持有锁的线程了。 若是锁是自由状态(isLocked = false),或者执行线程就是持有锁的线程,循环都不会执行,线程能够顺利的退出lock()方法。

另外,咱们须要记录同一个线程加锁的次数。 由于,在unlock()释放锁时,咱们须要知道多少次后才能真正释放锁。 而这也将决定,unlock()须要执行与lock()对应的次数,才能释放锁。

如今,Lock就是一个可重入锁了。

锁的公平性

Java的synchronized是不保障线程进入的顺序的。 所以,若是有多个线程不断的竞争同一个同步块,那就有可能某些线程永远也没法获得访问权(比较悲催的线程每次都没有被唤醒)。 这就造成了饥饿。 为了不这个问题,就须要把Lock实现为公平性的。 因为这里的Lock都是基于synchronized的,因此就没法保障公平性。 更多有关公平性的问题,能够参见:《并发中的饥饿问题以及公平性》

在finally块中释放锁

使用Lock来保护临界区时,可能会因为异常,致使没有机会执行unlock()方法。 因此,须要经过finally来释放锁,这样才能确保安全。

lock.lock();
try{
  //do critical section code, which may throw exception
} finally {
  lock.unlock();
}

这样一个范式,能够确保Lock能够获得有效释放。

补充几点

不论对于什么类型的Lock,都须要考虑几点:

  1. 竞争激烈程度
  2. 临界区执行时常
    • 持有锁的时间
  3. 锁的粒度
    • 临界区中的逻辑尽可能简洁
    • 无必要的逻辑移出临界区
  4. 能够获得释放
    • 如非必要,不要永久阻塞等待
    • 尽量设置等待时间
  5. 锁顺序
    • 避免死锁
  6. 锁嵌套
    • 避免嵌套锁死
相关文章
相关标签/搜索