本文翻译自http://tutorials.jenkov.com/java-util-concurrent/lock.html,人工翻译,仅供学习交流。
Java Lock
java.util.concurrent.locks.Lock接口表示一个并发锁,它可以用来防止临界区段内的竞态条件。Java Lock接口为Java同步块提供了一个更灵活的替代方案。在本Java Lock教程中,我将解释Lock接口是如何工作的,以及如何使用它。
如果您不熟悉Java同步块、竞态条件和临界区,你可以在我的教程中了解更多:
- Java Synchronized
- Race Conditions and Critical Sections
在Java并发教程中,我描述了如何实现您自己的锁,如果你感兴趣(或需要)更多详细信息,请参阅我关于锁的文章。
锁和 同步代码块的主要区别
锁和同步块之间的主要区别是:
- 同步代码块不保证等待进入它的线程的顺序
- 您不能向同步代码块的入口传递任何参数,设置访问同步代码块的超时是不可能的。
- 同步代码块必须完全包含在一个方法中,一个Lock可以在不同的方法中调用Lock()和unlock()。
Java锁实现
因为Java Lock是一个接口,所以不能直接创建Lock的实例。您必须创建实现Lock接口的类的实例。java.util.concurrent.locks包有以下Lock接口的实现:
- java.util.concurrent.locks.ReentrantLock
在下面的部分中,我将解释如何使用ReentrantLock类作为Lock。
创建可重入锁
要创建ReentrantLock类的实例,只需使用new操作符,如下所示:
Lock lock = new ReentrantLock();
现在您有了一个Java Lock实例——实际上是一个ReentrantLock实例。
锁定和解锁一个Java锁
由于Lock是一个接口,您需要使用它的一个实现来在您的应用程序中使用Lock。在下面的例子中,我创建了一个ReentrantLock实例。要锁定lock实例,必须调用它的lock()方法。要解锁Lock实例,必须调用其unlock()方法。下面是一个锁定和解锁Java锁实例的示例:
Lock lock = new ReentrantLock();
lock.lock();
//critical section
lock.unlock();
首先创建一个Lock,然后调用它的lock()方法,现在锁定了Lock实例。直到锁定该锁的线程调用unlock(),任何调用lock()的线程都会被阻塞。最后调用unlock(),锁现在被解锁,以便其他线程可以锁定它。
显然,所有线程必须共享同一个Lock实例。如果每个线程都创建了自己的Lock实例,然后它们将锁定不同的锁,从而不会阻塞彼此的访问。在本Java锁教程的后面,我将向您展示一个共享锁实例的示例。
故障安全锁定和解锁
看了上一节的例子,如果在调用lock.lock()和lock.unlock()之间抛出异常会发生什么。异常将中断程序运行,并且永远不会执行对lock.unlock()的调用,这样锁就永远锁着了。
为了避免异常永远锁定一个Lock,您应该在try-finally中锁定和解锁。
Lock lock = new ReentrantLock();
try{
lock.lock();
//critical section
} finally {
lock.unlock();
}
这样,即使在try块内部抛出异常,Lock也会被解锁。
锁保护计数器示例
为了更好地理解使用Lock与使用同步块的区别,我已经创建了两个简单的并发计数器类,以不同的方式保护它们的内部计数。第一个类使用synchronized块,第二个类使用Java Lock:
public class CounterSynchronized {
private long count = 0;
public synchronized void inc() {
this.count++;
}
public synchronized long getCount() {
return this.count;
}
}
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class CounterLock {
private long count = 0;
private Lock lock = new ReentrantLock();
public void inc() {
try {
lock.lock();
this.count++;
} finally {
lock.unlock();
}
}
public long getCount() {
try {
lock.lock();
return this.count;
} finally {
lock.unlock();
}
}
}
注意,CounterLock类比CounterSynchronized类长。如果你需要的话,在内部使用Java Lock保护count变量可以提供更高程度的灵活性。这些简单的例子并不需要它,但是更高级的计数器可能需要它。
锁可重入性
如果持有锁的线程可以再次锁定它,锁称为可重入锁。不可重入锁是一种锁定后不能再次锁定的锁,拥有锁的线程也不行。不可重入锁可能导致重入锁定,这种情况类似于死锁。
ReentrantLock类是一个可重入锁,持有锁的线程可以再次锁定它。线程解锁的次数必需与它锁定的次数相同,完全解锁的可重入锁,以便其他线程可以使用。
可重入锁在某些并发设计中很有用。下面是一个计算器的并发实现。计算器可以在内部保存当前结果,并提供了一套可以对结果进行计算的方法。
public class Calculator {
public static class Calculation {
public static final int UNSPECIFIED = -1;
public static final int ADDITION = 0;
public static final int SUBTRACTION = 1;
int type = UNSPECIFIED;
public double value;
public Calculation(int type, double value){
this.type = type;
this.value = value;
}
}
private double result = 0.0D;
Lock lock = new ReentrantLock();
public void add(double value) {
try {
lock.lock();
this.result += value;
} finally {
lock.unlock();
}
}
public void subtract(double value) {
try {
lock.lock();
this.result -= value;
} finally {
lock.unlock();
}
}
public void calculate(Calculation ... calculations) {
try {
lock.lock();
for(Calculation calculation : calculations) {
switch(calculation.type) {
case Calculation.ADDITION : add (calculation.value); break;
case Calculation.SUBTRACTION: subtract(calculation.value); break;
}
}
} finally {
lock.unlock();
}
}
}
请注意,在进行任何计算之前,calculate()方法是如何同时锁定计算器实例的Lock的,并调用add()和subtract()方法,这些方法也会锁定锁。因为ReentrantLock可重入,调用calculate的线程不会造成任何问题。
锁公平
不公平锁不能保证等待锁定的线程的获取锁的顺序,这意味着,如果其他线程一直试图锁定该锁并且优先于等待的线程,一个等待的线程可能会有永远等待的风险。这种情况会导致饥饿。我将在饥饿和公平教程中详细介绍饥饿和公平。
ReentrantLock行为在默认情况下是不公平的。但是,您可以通过其构造函数告诉它以公平模式操作。ReentrantLock类有一个构造函数,它接受布尔参数指定ReentrantLock是否应该为等待的线程提供公平性。以下是使用公平模式创建ReentrantLock实例的示例:
ReentrantLock lock = new ReentrantLock(true);
请注意,无参数的trylock()方法(稍后将在Java锁教程中介绍)不遵守ReentrantLock的公平模式。为了获得公平性,您必须使用tryLock(long timeout, TimeUnit unit)方法,例如:
lock.tryLock(0, TimeUnit.SECONDS);
锁和可重入锁方法
Java Lock接口包含以下主要方法:
- lock()
- lockInterruptibly()
- tryLock()
- tryLock(long timeout, TimeUnit timeUnit)
- unlock()
Java ReentrantLock也有一些有趣的公共方法: - getHoldCount()
- getQueueLength()
- hasQueuedThread(Thread)
- hasQueuedThreads()
- isFair()
- isHeldByCurrentThread()
- isLocked()
我将在下面的部分中更详细地介绍这些方法。
lock()
lock()方法尽可能锁定lock实例。如果Lock实例已经被锁定,调用lock()的线程被阻塞,直到锁被解锁。
lockInterruptibly()
除非调用该方法的线程被中断,否则lockInterruptibly()方法会锁定该锁实例。线程通过这个方法阻塞在等待锁定锁实例时,线程被中断,它将退出这个方法调用。
tryLock()
tryLock()方法尝试立即锁定lock实例。如果锁定成功则返回true,如果Lock已经被锁定则返回false。
这个方法从不阻塞。
tryLock(long timeout, TimeUnit timeUnit)
trylock(long timeout, TimeUnit timeUnit)类似于tryLock()方法,它会在给定时间内尝试去锁定锁实例。
unlock()
unlock()方法解锁Lock实例,Lock实现只允许锁定了Lock的线程调用此方法。调用此方法的其他线程可能会导致未检查的异常(RuntimeException)。
getHoldCount()
getHoldCount()方法返回给定线程锁定这个Lock实例的次数。由于锁重入,线程可以多次锁定一个锁。.
getQueueLength()
getQueueLength()方法返回等待锁定lock的线程数。
hasQueuedThread(Thread)
hasQueuedThread(Thread Thread)方法以线程作为参数,如果线程排队等待锁定锁返回true,反之返回false。
hasQueuedThreads()
如果有线程排队等待锁定这个锁,hasQueuedThreads()方法返回true,反之为false。
isFair()
如果 Lock保证了等待锁定它的线程之间的公平性,isFair()方法返回true,反之为false。有关锁公平性的更多信息,请参见锁公平性。
isHeldByCurrentThread()
如果锁被调用isHeldByCurrentThread()的线程持有(锁定),isHeldByCurrentThread()方法返回true,反之为false。
isLocked()
如果isLocked()方法如果Lock当前被锁定,则返回true,反之为fasle。