1.概览
简单地说,比起标准的同步块来说,lock是一个更加灵活、更加精密的线程同步机制。Lock是从java1.5开始推出的,它定义在java.util.concurrent.Lock包里,它提供了更广泛的锁操作。在这篇文章中,我们将探讨一些Lock接口的不同实现以及他们的应用。
2. Lock锁和同步块的区别
同步块和Lock API的些许区别:
1.同步块只能包含在一个方法内----而lock()和unlock()操作却可以在跨越多个不同的方法使用。
2.同步块不支持公平性,任一个线程都能获取已经被释放的锁,不能指定优先权。但我们却可以使用Lock API指定公平属性从而实现公平性。它能确保等待时间最长的线程优先获取锁。
3.当一个线程不能访问同步块时,它会被阻塞住。而 Lock API提供的有 tryLock()方法,使用该方法,只有在锁不被其他线程持有且可用时,才会真正获取锁。这将极大地降低阻塞时间。
4.那些获取访问同步块的等待线程不能被中断,Lock API提供了一个 lockInterruptbly()方法,当线程正在等待锁时,该方法可以用于中断该线程。
3.Lock API
我们来看一下Lock接口中的方法:
.void lock() - 如果锁可用就获取锁。如果锁不可用就阻塞住,直到锁被释放。
.void lockInterruptibly() - 这个方法和lock()方法很类似,但是它允许阻塞的线程被中断,并且通过抛出一个java.lang.InterruptedException可以重新运行。
.boolean tryLock() - 这是lock()方法的非阻塞版本;它会立即试图获取锁,如果锁定成功的话,就返回true。
.boolean tryLock(long timeout,TimeUnit timeUnit) -这和tryLock()方法很像,只不过该方法,会在放弃获取锁之前,等待一段指定时间。
.void unlock() -解锁该锁实例。
一个锁实例应该永远处于解锁状态,这样才能避免死锁条件。使用lock的推荐方式是:代码块中应该包含try/catch 以及finally 块。
Lock lock = ...;
lock.lock();
try{
// access to the shared resource
} finally{
lock.unlock();
}
除了Lock接口之外,我们还有一个读写锁ReadWriteLock接口,此接口包含了一对锁,一个用于只读操作,一个用于写操作。只要没有写操作,读锁可以同时被多个线程持有。
ReadWriteLock 所声明的用于获取读、写锁的方法:
.Lock readLock() - 返回用于读操作的锁。
.Lock writeLock() - 返回用于写操作的锁。
4.Lock实现
4.1 ReentrantLock
ReentrantLock类实现了Lock接口。它除了提供和synchronized代码块一样的并发和内存语义还拥有可扩展的能力。
我们来看一下,如何使用ReentrantLock做同步:
public class SharedObject {
//...
ReentrantLock lock = new ReentrantLock();
int counter = 0;
public void perform() {
lock.lock();
try {
// Critical section here
count++;
} finally {
lock.unlock();
}
}
//...
}
我们需要确保在try-finally块中包装了lock()和unlock()方法,只有这样才能避免死锁。
我们来看一下,tryLock() 是如何工作的:
public void performTryLock(){
//...
boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
if(isLockAcquired) {
try {
//Critical section here
} finally {
lock.unlock();
}
}
//...
}
在这个案例中,调用tryLock()的线程将等待1秒钟,如果锁不可用则放弃等待。
4.2 ReentrantReadWriteLock
ReentrantReadWriteLock类实现了ReadWriteLock接口。
我们来看一下,一个线程获取ReadLock或WriteLock的规则:
.Read Lock - 如果没有线程获取了写锁或请求写锁,那么可以允许多个线程获取该读锁。
. Write Lock - 如果没有线程正在读或正在写,那么只有一个线程可以获取该写锁。
我们来看一下,如何使用ReadWriteLock:
public class SynchronizedHashMapWithReadWriteLock { Map syncHashMap = new HashMap<>();
ReadWriteLock lock = new ReentrantReadWriteLock();
//...
Lock writeLock = lock.writeLock();
public void put(String key, String value) {
try {
writeLock.lock();
syncHashMap.put(key, value);
} finally {
writeLock.unlock();
}
}
...
public String remove(String key){
try {
writeLock.lock();
return syncHashMap.remove(key);
} finally {
writeLock.unlock();
}
}
//...
}
对于这两个写方法,我们都需要用write lock来包围临界区,只有一个线程可以访问它。
Lock readLock = lock.readLock();
//...
public String get(String key){
try {
readLock.lock();
return syncHashMap.get(key);
} finally {
readLock.unlock();
}
}
public boolean containsKey(String key) {
try {
readLock.lock();
return syncHashMap.containsKey(key);
} finally {
readLock.unlock();
}
}
对于上面的俩个读方法,我们需要用读锁read lock 来包围临界区。在没有写操作的情况下,多个线程可以同时访问临界区。
4.3 StampedLock
StampedLock是在java8中引进的。它也同时支持读锁和写锁。然而 获取锁的方法会返回一个标记,这个标记stamp可以用于释放锁或检查该锁是否依然有效。
public class StampedLockDemo { Map map = new HashMap<>();
private StampedLock lock = new StampedLock();
public void put(String key, String value){
long stamp = lock.writeLock();
try {
map.put(key, value);
} finally {
lock.unlockWrite(stamp);
}
}
public String get(String key) throws InterruptedException {
long stamp = lock.readLock();
try {
return map.get(key);
} finally {
lock.unlockRead(stamp);
}
}
}
StampedLock提供的另一个特性是:乐观锁。 大多数时候,读操作不需要等待写操作完成。正是基于此,我们并不需要一个完全的读锁(the full fledged read lock is not required)。作为替代,我们可以使用读锁。
public String readWithOptimisticLock(String key) {
long stamp = lock.tryOptimisticRead();
String value = map.get(key);
if(!lock.validate(stamp)) {
stamp = lock.readLock();
try {
return map.get(key);
} finally {
lock.unlock(stamp);
}
}
return value;
}
5. 结合Conditions一起使用
Condition 类为线程提供了一种条件执行能力,即线程会等待某些条件发生时,才执行临界区。
一个线程获取了对临界区的访问,但还没有执行相应动作的必要条件。例如,一个线程获取了对共享队列的访问,但该队列中没有任何可供消费的数据。
传统地,java提供的有wait()、notify()、以及notifyAll()方法用于线程间通信,Condition也有相似的机制,但是除此之外,我们可以指定多个条件Conditions:
public class ReentrantLockWithCondition { Stack stack = new Stack<>();
int CAPACITY = 5;
ReentrantLock lock = new ReentrantLock();
Condition stackEmptyCondition = lock.newCondition();
Condition stackFullCondition = lock.newCondition();
public void pushToStack(String item){
try {
lock.lock();
while(stack.size() == CAPACITY){
stackFullCondition.await();
}
stack.push(item);
stackEmptyCondition.signalAll();
} finally {
lock.unlock();
}
}
public String popFromStack() {
try {
lock.lock();
while(stack.size() == 0){
stackEmptyCondition.await();
}
return stack.pop();
} finally {
stackFullCondition.signalAll();
lock.unlock();
}
}
}
6.总结
在这篇文章中,我们已经看到了Lock接口的不同实现以及新引入的StampedLock类。我们也探索一下如何在多条件下使用Condition。
文章中的完整代码都在github上:sourceCode