Java锁
锁是一种类似于同步块的线程同步机制,除了锁比Java的同步块更复杂之外。锁(以及其他更高级的同步机制)是使用同步块创建的,因此我们无法完全摆脱synchronized
关键字。
从Java 5开始,软件包java.util.concurrent.locks
包含多个锁实现,因此我们可能不必实现自己的锁。但是我们仍然需要知道如何使用它们,并且了解实现它们背后的理论仍然很有用。
一个简单的锁
让我们开始看一下Java代码的同步块:
public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } } }
注意inc()
方法中的synchronized(this)
块。这个块确保了只有一个线程一次可以执行return ++ count
。同步块中的代码本来可以更高级,但是简单的" ++ count"足以说明要点。
可以使用Lock
而不是同步块来编写Counter
类,如下所示:
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"为真时,调用" lock()"的线程将停在" wait()"调用中。如果线程应该在没有接收到" notify()"调用(又称为"虚假唤醒")的情况下从wait()调用中意外返回,则线程会重新检查" isLocked"条件以查看是否安全进行,而不是仅仅假设被唤醒就意味着可以安全进行。如果isLocked为假,则线程退出while(isLocked)循环,并将isLocked设置回true,以锁定其他调用Lock()的线程的Lock实例。
当线程使用关键部分中的代码(" lock()"和" unlock()"之间的代码)完成时,线程将调用" unlock()"。执行unlock()
会将isLocked
设置为false,并通知(唤醒)在lock()
方法中的wait()
调用中等待的线程之一。
重锁
Java中的同步块是可重入的。这意味着,如果Java线程输入了同步的代码块,从而锁定了同步该块的监视对象,则该线程可以输入在同一监视对象上同步的其他Java代码块。这是一个例子:
public class Reentrant{ public synchronized outer(){ inner(); } public synchronized inner(){ //do something } }
注意,如何将" outer()"和" inner()"都声明为已同步,这在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()
实现时,很明显,线程第二次调用lock()
而没有在其间调用unlock()
时被阻塞的原因很明显:
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()
将会解锁该锁,即使该锁已被多次锁定。我们不希望在锁定它的线程执行与lock()
调用相同数量的unlock()
调用之前将其解锁。
锁类现在是可重入的。
公平锁
Java的同步块无法保证尝试进入线程的线程被授予访问权限的顺序。因此,如果许多线程一直在争夺对同一同步块的访问权,则存在一个风险,即一个或者多个线程永远不会被授予访问权,而该访问权总是授予其他线程。这称为饥饿。为了避免这种情况,Lock
应该是公平的。由于本文中显示的Lock
实现在内部使用同步块,因此它们不能保证公平。饥饿和公平在文本饥饿和公平中有更详细的讨论。
从最后一个子句中调用unlock()
当用"锁"保护关键部分时,关键部分可能会引发异常,从"最终"子句内部调用" unlock()"方法很重要。这样做可以确保"锁定"已解锁,以便其他线程可以锁定它。这是一个例子:
lock.lock(); try{ //do critical section code, which Jan throw exception } finally { lock.unlock(); }
这个小结构可以确保在关键部分的代码中抛出异常时,可以解锁" Lock"。如果未从"最终"子句中调用" unlock()",并且从关键部分引发了异常,则" Lock"将永远保持锁定状态,从而导致所有线程在该" Lock"上调用" lock()"。
实例无限期停止。