java并发系列-介绍java.util.concurrent.Locks

1 介绍

简单地讲,锁是一种比标准的synchronized代码块更加灵活和精巧的线程同步机制。

Lock自Java1.5引入。定义在java.util.concurrent.lock包中,提供与锁相关的可扩展操作。

这篇文章研究一下Lock接口类的不同实现和它们的应用场景。

2 Lock和Synchronized Block的区别

一些synchronized blockLockAPI之间的区别:

  • synchronized block整个存在于一个方法中 - *LockAPI的lock()和*unlock() 操作可以在不同的方法中
  • synchronized block不支持公平,任何线程在锁释放后都可以获得锁,不能设置获取锁的条件。而使用LockAPI通过设置公平属性使获取锁的保证公平。这样可以确保最长等待线程优先获得锁。
  • 如果线程没有获得synchronized block的访问权就会被阻塞。LockAPI提供了tryLock() 方法,线程可以在适当的时候获取锁。这样可以减少线程阻塞的事件。
  • 线程处于等待获取synchronized block访问权时,不能被中断。LockAPI提供lockInterruptibly() 方法当等待锁时可以中断。

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实例

一个被锁的实例应该总是要执行unlock以避免死锁情况。使用锁阻塞线程的推荐写法应该要有try/catchfinally块:

1
2
3
4
5
6
7
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用于同步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SharedObject {
//...
ReentrantLock lock = new ReentrantLock();
int counter = 0;

public void perform() {
lock.lock();
try {
// Critical section here
count++;
} finally {
lock.unlock();
}
}
//...
}

确保将lock()unlock()调用放在try-finally块中以避免死锁情况。

下面看看tryLock()怎样工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void performTryLock(){
//...
boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);

if(isLockAcquired) {
try {
//Critical section here
} finally {
lock.unlock();
}
}
//...
}

这个例子中,线程调用tryLock(), 等待一秒钟没有拿到锁就放弃。

4.2 ReentrantReadWriteLock

ReentrantReadWriteLock类实现了ReadWriteLock接口类。

看看一个线程获取ReadLock或者WriteLock的规则:

  • 读锁 - 如果没有线程获得写锁或者请求写锁,那么多个线程可以获得读锁
  • 写锁 - 如果没有线程在读或者写,那么有且仅有一个线程可以获得写锁

下面看看怎样使用ReadWriteLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SynchronizedHashMapWithReadWriteLock {

Map<String,String> 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();
}
}
//...
}

对于写方法,需要用写锁将临界区包住,只有一个线程可以访问它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
}
}

对于读锁,需要用读锁将临界区抱住。如果当前没有写操作,那么多个线程可以访问这个临界区。

4.3 StampedLock

StampedLockJava8引入。它也支持读写锁。不过,锁获取方法会放回一个邮戳(stamp),用于释放锁或者检查锁是否有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StampedLockDemo {
Map<String,String> 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还提供了乐观锁(optimistic locking)。大多数时候,读操作不需要等待写操作完成再读。因此,严格的读锁并不需要。

下面,我们改下读锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()方法用于线程间通信。Conditions也有相似的机制,但是它还可以提供多条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ReentrantLockWithCondition {

Stack<String> 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类处理多条件的场景。


本文为译文,作者通过翻译达到学习目的。 原文链接 | 原文源码链接 | 本站源码链接