一个锁是一个线程同步机制,像同步块除了锁可以是更复杂的比java的同步块。锁(以及其他的更先进的同步机制)都是使用同步块被创建的。
从Java 5开始这个java.util.concurrent.locks包包含了几个锁的实现,以至于你可能不需要去实现你自己的锁。但是你仍然需要知道怎么样使用它们,并且知道他们背后实现的原理仍然是有用的。对于更多的信息,看我的教程在:java.util.concurrent.locks.Lock接口。
一个简单的锁
让我们通过看一个Java代码的同步锁来开始:
public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } } }注意在inc方法中的synchronized(this)这个块。这个块确保一次只能一个线程执行这个return ++count操作。在同步块中的代码可能有更高级的,但是这个简单的方式已经可以达到效果了。这个Counter类被代替像下面这样写了,使用一个Lock代替一个同步块:
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方法的所有线程被锁住了直到unlock方法被执行了。这里有一个简单的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)循环,这个也被称之为“自旋锁”。自旋锁以及这个wait方法和notify方法在线程信号文章中有更详细的描述。当isLocked为true的时候,调用lock方法的这个线程就会被挂起等待在wait方法调用。假设这个线程没有接收到一个notify调用而意外的从wait方法返回(又称之为伪唤醒),这个线程重新检查这个isLocked条件去看是否它是安全的去执行,而不只是假设被唤醒意味着就会去安全的执行。如果isLocked是false,这个线程退出while循环,并且设置这个isLocked为true,去锁住对于调用lock方法其他线程的Lock实例。
当这个线程在代码的临界区域(在lock方法和unlock方法之间的代码)被执行的时候,这个线程调用unlock方法。执行unlock方法设置isLocked会false,并且唤醒等待在wait调用中的线程中的一个,如果可以的话。
重入锁
在Java中的同步块是可重入的。这就意味着,如果一个Java线程进入一个代码的同步块,并且从而持有一个监控对象的锁,这个线程也可以进入在相同的监控对象上的其他的Java同步块中。这里有一个例子:
public class Reentrant{ public synchronized outer(){ inner(); } public synchronized inner(){ //do something } }注意这个outer方法和inner方法是怎么样被同步的声明的,这个在Java中是等同于synchronized(this)块的。如果一个线程调用outer方法,没有问题在outer方法内部去调用inner方法,因为两个方法都是被相同的监控对象同步了。如果一个线程已经持有在这个监控对象的锁了,它就可以访问在相同的监控对象上的所有的同步块了。这个就被称之为重入锁。这个线程可以重新进入任何一个它已经持有这个锁的代码块。前面展示的锁的实现是不能重入的。如果我们像下面这样重写这个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方法这个线程被锁定的原因,当我们看这个lock方法的实现就很明显了:
public class Lock{ boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } ... } 它是这个while循环(自旋锁)内部的条件决定了一个线程是否允许离开这个lock方法。当前的这个条件isLocked必须为false才能被允许的,不管哪个线程锁住了它。为了使得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方法的这个线程允许去退出这个方法。另外,我们需要计算别相同线程锁住的次数。另外,一个对于unlock方法单独的调用去释放这个锁,甚至如果这个锁被锁定多次了。我们不想这个锁被释放直到锁定它的这个线程已经执行unlock方法的次数跟lock方法调用的次数一样。
这个Lock类是可重入的。
公平锁
Java的同步块不能保证线程的执行顺序。因此,如果许多线程在不断的竞争相同的同步块的访问权限,这里有一个风险,一个或者多个线程永远不会获取到访问权限。这个也称之为饥饿。为了避免这个,锁应该是公平的。因为在这个文本中显示的Lock实现内部使用同步块,他们不会保证公平。饥饿和公平在“饥饿和公平”文章中有更详细的讨论。
在Finally子句中调用unlock方法
当使用Lock守卫一个关键部分的时候,并且这个关键部分可能会抛出异常,在finally子句内部调用unlock方法是重要的。这样做可以确保Lock可以被解锁的以至于其他的线程可以锁定它。这里有一个例子:
lock.lock(); try{ //do critical section code, which may throw exception } finally { lock.unlock(); } 这个小的构造可以确保一旦一个异常抛出,这个Lock就可以被解锁了。如果unlock方法没有在finally子句中被调用,并且一个异常被抛出,这个Lock将会永远会保持锁定的,导致调用lock方法的所有线程将会永远的停在那里了。
翻译地址:http://tutorials.jenkov.com/java-concurrency/locks.html