Java并发编程实战:第13章 显式锁

1. Lock 和 ReentrantLock

Lock 接口:

1
2
3
4
5
6
7
8
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}

Lock 提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有加锁和解锁的方法都是显式的。

Lock 的实现必须提供具有与内部加锁相同的内存可见性的语义。

ReentrantLock 实现了 Lock 接口,提供了与内置锁 synchronized 相同的互斥和内存可见性保证。获得 ReentrantLock 的锁与进入 synchronized 块有着相同的内存语义,释放 ReentrantLock 锁与退出 synchronized 块有相同的内存语义。

synchronized 内置锁局限性:

  • 不能中断那些正在等待获取锁的线程。
  • 在请求锁失败情况下,会无限等待。

Lock 锁必须在 finally 中释放。如果 Lock 锁守护的代码在 try 块之外跑出了异常,它将永远不会被释放。ReentrantLock 不能完全替代 synchronized,忘记释放 Lock 是非常危险的,因为当程序的控制权离开了守护的块时,不会自动清除锁。

1.1 可轮询的和可定时的锁请求

  • 可定时的与可轮询的锁获取模式是由 tryLock() 方法实现的,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。

  • 在内置锁中,死锁是一个严重的问题,恢复程序的唯一方法是重新启动程序,而防止死锁的唯一方法就是在构造过程时避免不一致的锁顺序。

  • 可定时的与可轮询的锁提供了另一种选择:避免死锁的发生。

在第10章,有这样一个例子:转账

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import java.util.concurrent.atomic.AtomicInteger;

public class DynamicOrderDeadlock {
// Warning: 容易产生死锁
public static void transferMoney(Account fromAccount,
Account toAccount,
DollarAmount amount)
throws InsufficientFundsException {
// 获得【借出钱】锁
synchronized (fromAccount) {
// 获得【去借钱】锁
synchronized (toAccount) {
// 如果【借出钱】的人账户 amount 没这么多钱,那么就抛出【资金不足】Exception
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
} else {
// 资金充足,交易完成
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}

static class DollarAmount implements Comparable<DollarAmount> {

private int amount = 0;

public DollarAmount(int amount) {
this.amount = amount;
}

/** 增加 */
public DollarAmount add(DollarAmount d) {
this.amount = this.amount + d.amount;
return this;
}

/** 减少 */
public DollarAmount subtract(DollarAmount d) {
this.amount = this.amount - d.amount;
return this;
}

/** -1, 0, 1 ---- 小于,等于,大于 */
public int compareTo(DollarAmount dollarAmount) {
return Integer.compare(this.amount, dollarAmount.amount);
}

public int getAmount() {
return amount;
}
}

static class Account {
private DollarAmount balance;
private final int acctNo;
private static final AtomicInteger sequence = new AtomicInteger();

public Account() {
acctNo = sequence.incrementAndGet();
}

/** 将钱借出 */
void debit(DollarAmount d) {
balance = balance.subtract(d);
}

/** 去借钱 */
void credit(DollarAmount d) {
balance = balance.add(d);
}

DollarAmount getBalance() {
return balance;
}

public void setBalance(DollarAmount balance) {
this.balance = balance;
}

int getAcctNo() {
return acctNo;
}
}

static class InsufficientFundsException extends Exception {
}
}

对于使用内置锁 synchronized 的 transferMoney() 方法:

  • 如果【线程A】获取了 Account1 的锁,向 Account2 转账。
  • 而【线程B】又获取了 Account2 的锁,向 Account1 转账。
  • 那么就会出现【线程A】等待 Account2 的锁,【线程B】等待 Account1 的锁 —- 线程死锁

轮询锁:通过 tryLock 来避免锁顺序死锁。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

import static java.util.concurrent.TimeUnit.NANOSECONDS;

public class DeadlockAvoidance {
private static Random rnd = new Random();

public boolean transferMoney(Account fromAccount, Account toAccount, DollarAmount amount,
long timeout, TimeUnit unit)
throws InsufficientFundsException, InterruptedException {

long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
long randMod = getRandomDelayModulusNanos(timeout, unit);
long stopTime = System.nanoTime() + unit.toNanos(timeout);

while (true) {
// 尝试获得【借出钱】锁
if (fromAccount.lock.tryLock()) {
try {
// 尝试获得【去借钱】锁
if (toAccount.lock.tryLock()) {
try {
// 如果【借出钱】的人账户 amount 没这么多钱,那么就抛出【资金不足】Exception
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
} else {
// 资金充足,交易完成
fromAccount.debit(amount);
toAccount.credit(amount);
return true;
}
} finally {
// 释放【去借钱】锁
toAccount.lock.unlock();
}
}
} finally {
// 释放【借出钱】锁
fromAccount.lock.unlock();
}
}
if (System.nanoTime() < stopTime) {
return false;
}
NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
}
}

private static final int DELAY_FIXED = 1;
private static final int DELAY_RANDOM = 2;

/** 固定延迟 */
static long getFixedDelayComponentNanos(long timeout, TimeUnit unit) {
return DELAY_FIXED;
}

/** 随机延迟 */
static long getRandomDelayModulusNanos(long timeout, TimeUnit unit) {
return DELAY_RANDOM;
}

static class DollarAmount implements Comparable<DollarAmount> {
public int compareTo(DollarAmount other) {
return 0;
}

DollarAmount(int dollars) {
}
}

class Account {
public Lock lock;

void debit(DollarAmount d) {
}

void credit(DollarAmount d) {
}

DollarAmount getBalance() {
return null;
}
}

class InsufficientFundsException extends Exception {
}
}

定时锁

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
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static java.util.concurrent.TimeUnit.NANOSECONDS;

public class TimedLocking {
private Lock lock = new ReentrantLock();

public boolean trySendOnSharedLine(String message, long timeout, TimeUnit unit)
throws InterruptedException {
long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(message);
// 规定时间内获取不到 lock,return false
if (!lock.tryLock(nanosToLock, NANOSECONDS)) {
return false;
}
try {
return sendOnSharedLine(message);
} finally {
lock.unlock();
}
}

private boolean sendOnSharedLine(String message) {
/* send something */
return true;
}

long estimatedNanosToSend(String message) {
return message.length();
}
}

1.2 可中断的锁获取操作

  • 正如定时锁的获得操作允许在限时活动内部使用独古锁,可中断的锁获取操作允许在可取消的活动中使用。
  • lockInterruptible() 方法能够在获得锁的同时保持对中断的响应,并且由于它包含在Lock中,因此无需创建其他类型的不可中断阻塞机制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/** 可中断的锁获取请求 */
public class InterruptibleLocking {
private Lock lock = new ReentrantLock();

public boolean sendOnSharedLine(String message)
throws InterruptedException {
lock.lockInterruptibly();
try {
return cancellableSendOnSharedLine(message);
} finally {
lock.unlock();
}
}

private boolean cancellableSendOnSharedLine(String message)
throws InterruptedException {
/* send something */
return true;
}
}

1.3 非块结构的锁

  • 在内置锁(synchronized)中,锁的获取和释放都是基于使用内置锁的代码块的,并不用考虑内置锁对代码块的控制权是如何退出的。
  • 虽然自动释放锁简化了程序的分析,并且避免了潜在的代码错误造成的麻烦,但是有时候需要更灵活的加锁规则。Lock就是这样一个可定制化的锁。

2. 对性能的考量

  • 显式锁 ReentrantLock 要比内置锁 synchronized 提供更好的竞争性能。

  • 对于锁守护的程序而言,发生锁竞争时,程序的性能是可伸缩性的关键。

    :label:可伸缩性指的是:当增加计算资源的时候(比如增加额外 CPU 数量、内存、存储器、TO 带宽),吞吐量和生产量能够相应地得以改进。

    如果很多资源都花费在锁的管理和调度上,那么留给程序的就会越少。

  • 锁的实现越好,那么上下文切换会更少,在共享内存总线上发起的内存同步通信也会更少。

3. 公平性

ReentrantLock 构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
/** 创建ReentrantLock的实例。这相当于使用ReentrantLock(false)  */
public ReentrantLock() {
sync = new NonfairSync();
}

/**
* 使用给定的公平策略创建ReentrantLock的实例
* @param fair 如果此锁应使用公平排序策略,则为true
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
  • 在公平的锁上,线程将按照它们发出请求的顺序来获得锁(对于公平锁而言,可轮询的tryLock总会闯入)
  • 在非公平的锁上,则允许”插队“:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。
  • 公平的锁,在线程挂起和线程恢复时(上下文切换),存在的开销,会极大的降低程序性能。
  • 非公平的锁,允许线程在其它线程的恢复阶段进入加锁代码块。
  • 如果持有锁的时间相对较长,或者请求锁的平均时间将额较长,那么推荐使用公平锁。
  • 内置锁 synchronized 没有提供确定的公平性保证。

4. 在 synchronized 和 ReentrantLock 之间进行选择

ReentrantLock

  • ReentrantLock 功能性方面更全面,具有更强的扩展性

  • ReentrantLock 提供了 Condition,对线程的等待和唤醒等操作更加灵活。一个 ReentrantLock 能够有多个 Condition 实例。

    1
    2
    3
    4
    /** 返回与此Lock实例一起使用的Condition实例 */
    public Condition newCondition() {
    return sync.newCondition();
    }
  • ReentrantLock 可以控制线程得到锁的顺序(公平锁、非公平锁) – fair

  • ReentrantLock 可以查看锁的状态、等待锁的线程数。可以响应中断(lockInterruptibly)。

  • ReentrantLock 提供了可轮询的锁请求(tryLock)

  • 在获取 ReentrantLock 时,可以设置超时时间。

  • ReentrantLock是显式锁,需要手动释放锁,忘记释放后果非常严重。

synchronized

  • synchronized 是在JVM层面上实现的,能够通过一些监控工具监控synchronized的锁定。
  • synchronized 能够在代码块执行完成或异常退出时自动释放锁。
  • 被 synchronized 保护的代码块,一旦被线程获取锁,如果不释放,别的线程要获取该锁,会一直长时间的等待,不能被中断。

5. 读-写锁

一个资源可以被多个读操作访问,或者被一个写操作访问,读-写 两者不能同时进行。ReadWriteLock 实现的加锁策略允许多个同时存在的读者,但是只允许一个写者。

读写锁的可选实现:

  • 释放优先。写入锁释放后,应该优先选择读线程,写线程,还是最先发出请求的线程
  • 读线程插队。锁由读线程持有,写线程在等待,再来一个读线程,是继续让读线程访问,还是让写线程访问
  • 重入性。读取锁和写入锁是否可重入
  • 降级。将写入锁降级为读取锁
  • 升级。将读取锁升级为写入锁

用读写锁包裝的 Map

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteMap <K,V> {
private final Map<K, V> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();

public ReadWriteMap(Map<K, V> map) {
this.map = map;
}

public V put(K key, V value) {
writeLock.lock();
try {
return map.put(key, value);
} finally {
writeLock.unlock();
}
}

public V remove(Object key) {
writeLock.lock();
try {
return map.remove(key);
} finally {
writeLock.unlock();
}
}

public void putAll(Map<? extends K, ? extends V> m) {
writeLock.lock();
try {
map.putAll(m);
} finally {
writeLock.unlock();
}
}

public void clear() {
writeLock.lock();
try {
map.clear();
} finally {
writeLock.unlock();
}
}

public V get(Object key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}

public int size() {
readLock.lock();
try {
return map.size();
} finally {
readLock.unlock();
}
}

public boolean isEmpty() {
readLock.lock();
try {
return map.isEmpty();
} finally {
readLock.unlock();
}
}

public boolean containsKey(Object key) {
readLock.lock();
try {
return map.containsKey(key);
} finally {
readLock.unlock();
}
}

public boolean containsValue(Object value) {
readLock.lock();
try {
return map.containsValue(value);
} finally {
readLock.unlock();
}
}
}

Java并发编程实战:第13章 显式锁

https://osys.github.io/posts/46d8.html

作者

Osys

发布于

2022年08月29日

更新于

2022年08月29日

许可协议

评论