锁像synchronized同步块同样,是一种线程同步机制,但比Java中的synchronized同步块更复杂。由于锁(以及其它更高级的线程同步机制)是由synchronized同步块的方式实现的,因此咱们还不能彻底摆脱synchronized关键字(译者注:这说的是Java 5以前的状况)。html
自Java 5开始,java.util.concurrent.locks包中包含了一些锁的实现,所以你不用去实现本身的锁了。可是你仍然须要去了解怎样使用这些锁,且了解这些实现背后的理论也是颇有用处的。能够参考我对java.util.concurrent.locks.Lock的介绍,以了解更多关于锁的信息。java
让咱们从java中的一个同步块开始:segmentfault
public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } } }
能够看到在inc()方法中有一个synchronized(this)代码块。该代码块能够保证在同一时间只有一个线程能够执行return ++count。虽然在synchronized的同步块中的代码能够更加复杂,可是++count这种简单的操做已经足以表达出线程同步的意思。安全
如下的Counter类用Lock代替synchronized达到了一样的目的:函数
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实例对象进行加锁,所以全部对该对象调用lock()方法的线程都会被阻塞,直到该Lock对象的unlock()方法被调用。this
这里有一个Lock类的简单实现:spa
public class Counter{ 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)循环,它又被叫作“自旋锁”。自旋锁以及wait()和notify()方法在线程通讯这篇文章中有更加详细的介绍。当isLocked为true时,调用lock()的线程在wait()调用上阻塞等待。为防止该线程没有收到notify()调用也从wait()中返回(也称做虚假唤醒),这个线程会从新去检查isLocked条件以决定当前是否能够安全地继续执行仍是须要从新保持等待,而不是认为线程被唤醒了就能够安全地继续执行了。若是isLocked为false,当前线程会退出while(isLocked)循环,并将isLocked设回true,让其它正在调用lock()方法的线程可以在Lock实例上加锁。线程
当线程完成了临界区(位于lock()和unlock()之间)中的代码,就会调用unlock()。执行unlock()会从新将isLocked设置为false,而且通知(唤醒)其中一个(如有的话)在lock()方法中调用了wait()函数而处于等待状态的线程。code
Java中的synchronized同步块是可重入的。这意味着若是一个java线程进入了代码中的synchronized同步块,并所以得到了该同步块使用的同步对象对应的管程上的锁,那么这个线程能够进入由同一个管程对象所同步的另外一个java代码块。下面是一个例子:htm
public class Reentrant{ public synchronized outer(){ inner(); } public synchronized inner(){ //do something } }
注意outer()和inner()都被声明为synchronized,这在Java中和synchronized(this)块等效。若是一个线程调用了outer(),在outer()里调用inner()就没有什么问题,由于这两个方法(代码块)都由同一个管程对象(”this”)所同步。若是一个线程已经拥有了一个管程对象上的锁,那么它就有权访问被这个管程对象同步的全部代码块。这就是可重入。线程能够进入任何一个它已经拥有的锁所同步着的代码块。
前面给出的锁实现不是可重入的。若是咱们像下面这样重写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实例已经在outer()方法中被锁住了。
两次lock()之间没有调用unlock(),第二次调用lock就会阻塞,看过lock()实现后,会发现缘由很明显:
public class Lock{ boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } ... }
一个线程是否被容许退出lock()方法是由while循环(自旋锁)中的条件决定的。当前的判断条件是只有当isLocked为false时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(); } } } ... }
注意到如今的while循环(自旋锁)也考虑到了已锁住该Lock实例的线程。若是当前的锁对象没有被加锁(isLocked = false),或者当前调用线程已经对该Lock实例加了锁,那么while循环就不会被执行,调用lock()的线程就能够退出该方法(译者注:“被容许退出该方法”在当前语义下就是指不会调用wait()而致使阻塞)。
除此以外,咱们须要记录同一个线程重复对一个锁对象加锁的次数。不然,一次unblock()调用就会解除整个锁,即便当前锁已经被加锁过屡次。在unlock()调用没有达到对应lock()调用的次数以前,咱们不但愿锁被解除。
如今这个Lock类就是可重入的了。
Java的synchronized块并不保证尝试进入它们的线程的顺序。所以,若是多个线程不断竞争访问相同的synchronized同步块,就存在一种风险,其中一个或多个线程永远也得不到访问权——也就是说访问权老是分配给了其它线程。这种状况被称做线程饥饿。为了不这种问题,锁须要实现公平性。本文所展示的锁在内部是用synchronized同步块实现的,所以它们也不保证公平性。饥饿和公平中有更多关于该内容的讨论。
若是用Lock来保护临界区,而且临界区有可能会抛出异常,那么在finally语句中调用unlock()就显得很是重要了。这样能够保证这个锁对象能够被解锁以便其它线程能继续对其加锁。如下是一个示例:
lock.lock(); try{ //do critical section code, //which may throw exception } finally { lock.unlock(); }
这个简单的结构能够保证当临界区抛出异常时Lock对象能够被解锁。若是不是在finally语句中调用的unlock(),当临界区抛出异常时,Lock对象将永远停留在被锁住的状态,这会致使其它全部在该Lock对象上调用lock()的线程一直阻塞。