Java核心技术1-线程同步

  |  

摘要: 本文是《Java核心技术 10th》中关于线程同步的要点总结。

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


如果两个线程存取相同的对象,并且每一个线程都调用了一个修改该对象状态的方法,将会产生竞争条件(race condition)。

$4 同步

$4-1 竞争条件的例子

模拟一个有若干账户的银行,随机地生成在这些账户之间转移钱款的交易。

下面代码的功能:每一个账户有一个线程。每一笔交易中,会从线程所服务的账户中随机转移一定数目的钱款到另一个随机账户。

  • Bank.java,使用线程。

执行交易的是 Bank 类中的 transfer 方法。

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
import java.util.*;

public class Bank {
private final double[] accounts;

public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
}

public void transfer(int from, int to, double amount) {
if(accounts[from] < amount) {
return;
}
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}

public double getTotalBalance() {
double sum = 0;
for(double a: accounts) {
sum += a;
}
return sum;
}

public int size() {
return accounts.length;
}
}
  • BankTest.java

Runnable 类的 run 方法中,每次迭代随机选择账户,调用 bank 对象的 transfer 方法进行交易,然后睡眠。

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 UnsynchBankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;

public static void main(String[] args) {
Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
for(int i = 0; i < NACCOUNTS; i++) {
int fromAccount = i;
Runnable r = () -> {
try {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
}
};
Thread t = new Thread(r);
t.start();
}
}
}

上面的程序运行后,打印出的日志是错乱的,而且总金额也不平。

$4-2 竞争条件

当两个线程试图同时更新同一个账户的时候,这个问题就出现了。例如两个线程同时执行指令:

1
accounts[to] += amount;

问题在于这不是原子操作。该指令可能被处理如下:

1) 将accounts[to] 加载到寄存器。
2) 增加amount。
3) 将结果写回accounts[to]。

可以具体看一下执行我们的类中的每一个语句的虚拟机的字节码。运行命令

1
javap -c -v Bank

其中 accounts[to] += amount; 被转换为以下字节码:

1
2
3
4
5
6
7
8
aload_0
getfield #2 // Field accounts:[D
iload_2
dup2
daload
dload_3
dadd
dastore

可以看出增值命令是由几条指令组成的,执行它们的线程可以在任何一条指令点上被中断。

$4-3 锁对象

有两种机制防止代码块受并发访问的干扰。

  • Java 语言提供一个 synchronized 关键字,自动提供一个锁以及相关的“条件”。
  • java.util.concurrent.locks.ReentrantLock 类。

ReentrantLock 保护代码块的基本结构

1
2
3
4
5
6
myLock.lock(); 
try {
...
} finally {
myLock.unlock();
}

上面的代码确保任意时刻只有一个线程进入临界区。一个线程封锁了锁对象,其它任何线程调用 lock 都会被阻塞,直到第一个线程释放锁对象。注意如果使用锁,就不能使用带资源的 try 语句。

锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪对 lock 方法的嵌套调用。线程在每一次调用 lock 都要调用 unlock 来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。

使用锁来保护 Bank 类的 transfer 方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.*;
import java.util.concurrent.locks.*;

public class Bank {
private Lock bankLock = new ReentrantLock();

public void transfer(int from, int to, double amount) {
if(accounts[from] < amount) {
return;
}
bankLock.lock();
try {
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
} finally {
bankLock.unlock();
}
}
}

非同步线程与同步线程的比较:

java.util.concurrent.locks.Lock API 总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获取这个锁;如果锁同时被另一个线程拥有则发生阻塞。
void lock()

// 释放这个锁。
void unlock()

// 返回一个与该锁相关的条件对象
Condition newCondition()

// 尝试获得锁而没有发生阻塞;如果成功返回真。这个方法会抢夺可用的锁,即使该锁有公平加锁策略,即便其他线程已经等待很久也是如此。
boolean tryLock()

// 尝试获得锁,阻塞时间不会超过给定的值;如果成功返回 true。
boolean tryLock(long time, TimeUnit unit)

// 获得锁,但是会不确定地发生阻塞。如果线程被中断,抛出一个 InterruptedException 异常。
void lockInterruptibly()

java.util.concurrent.locks.ReentrantLock API 总结

1
2
3
4
5
6
// 构建一个可以被用来保护临界区的可重入锁。
ReentrantLock()

// 构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程。
// 但是,这一公平的保证将大大降低性能。所以,默认情况下,锁没有被强制为公平的。
ReentrantLock(boolean fair)

$4-4 条件对象

有时线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。

对于银行模拟的例子,我们要在有足够资金的时候才能作为转出账户。之前我们的处理是,如果没有足够资金,则直接返回而不做转账操作:

1
2
3
4
5
6
7
8
9
if(accounts[from] < amount) {
return;
}
bankLock.lock();
try {
...
} finally {
bankLock.unlock();
}

这里换一种方式:如果转出账户中没有足够资金,等待直到另一个线程向账户中注入了资金。

但是问题是:这一线程刚刚获得了对 bankLock 的排它性访问,因此别的线程没有进行存款操作的机会。这就是为什么我们需要条件对象的原因。

一个锁对象可以有一个或多个相关的条件对象。你可以用 newCondition 方法获得一个条件对象。

设置一个条件对象表达”余额充足”条件。

1
2
3
4
5
6
7
8
class Bank {
private Condition sufficientFunds;
...
public Bank() {
...
sufficientFunds = bankLock.newCondition();
}
}

如果 transfer 方法发现余额不足,则调用条件对象的 await 方法。这样当前线程就被阻塞了,并放弃了锁。这样可以使得另一个线程可以进行增加账户余额的操作。

1
2
3
while(accounts[from] < amount) {
sufficientFunds.await();
}

等待获得锁的线程和调用 await 方法的线程存在本质上的不同。

一旦一个线程调用 await 方法,它进入该条件的等待集当锁可用时,该线程不能马上解除阻塞。而需要处于阻塞状态直到另一个线程调用同一条件上的 signalAll 方法时为止。也就是当另一个线程转账时,应该调用:

1
sufficientFunds.signalAll();

这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从 await 调用返回,获得该锁并从被阻塞的地方继续执行。

此时线程需要再次测试条件,因为 signalAll 方法仅仅是通知正在等待的线程:此时有可能已经满足条件,需要再次去检测该条件。

当一个线程调用 await 时,它没有办法重新激活自身。它寄希望于其他线程调用 signalAll。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这是一种死锁。因此什么时候调用 signalAll 很关键。按照经验应该在对象的状态有利于等待线程的方向改变时调用 signalAll。在模拟银行转账例子中,完成转账时调用 signalAll 方法。

当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用 await、signalAll 或 signal 方法。

  • Bank.java,使用锁和条件对象
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
import java.util.*;
import java.util.concurrent.locks.*;

public class Bank {
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;

public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}

public void transfer(int from, int to, double amount) throws InterruptedException {
bankLock.lock();
try {
while(accounts[from] < amount) {
sufficientFunds.await();
}
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();
} finally {
bankLock.unlock();
}
}

public double getTotalBalance() {
bankLock.lock();
try {
double sum = 0;
for(double a: accounts) {
sum += a;
}
return sum;
} finally {
bankLock.unlock();
}
}

public int size() {
return accounts.length;
}
}

java.util.concurrent.locks.Condition API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将该线程放到条件的等待集中。
void await()

// 解除该条件的等待集中的所有线程的阻塞状态。
void signalAll()

// 从该条件的等待集中随机地选择一个线程,解除其阻塞状态。
void signal()

// 进入该条件的等待集,直到线程从等待集中移出或等待了指定的时间之后才解除阻塞。如果因为等待时间到了而返回就返回 false,否则返回 true。
boolean await(long time, TimeUnit unit)

// 进入该条件的等待集,直到线程从等待集移出才解除阻塞。如果线程被中断,该方法不会抛出 InterruptedException 异常。
void awaitUninterruptibly()

$4-5 synchronized 关键字

前面我们了解了锁和条件对象的用法,这里总结一下要点:

  • 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
  • 锁可以管理试图进入被保护代码段的线程。
  • 锁可以拥有一个或多个相关的条件对象。
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

Lock 和 Condition 接口为程序设计人员提供了高度的锁定控制。但有时不需要这样的控制,此时 Java 语言内部的 synchronized 机制可以作为备用方法。

Java 中的每一个对象都有一个内部锁。如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。

使用 synchronized 的方法的代码如下:

1
2
3
public synchronized void method() {
method body
}

等价于下面这段代码:

1
2
3
4
5
6
7
8
public void method() {
this.intrinsicLock.lock();
try {
method body
} finally {
this.intrinsicLock.unlock();
}
}

内部对象锁只有一个相关条件。wait 方法添加一个线程到等待集中,notifyAll/notify 方法解除等待线程的阻塞状态。(wait、notifyAll以及notify方法是Object类的final方法。)

也就是说,调用 waitnotifyAll 等价于

1
2
intrinsicCondition.await();
intrinsicCondition.signalAll();
  • Bank.java,使用 synchronized
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
public class Bank {
private final double[] accounts;

public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
}

public synchronized void transfer(int from, int to, double amount) throws InterruptedException {
if(accounts[from] < amount) {
wait(); // intrinsic 对象的锁的条件
}
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
notifyAll(); // 通知所有等待条件的线程
}

public synchronized double getTotalBalance() {
double sum = 0;
for(double a: accounts) {
sum += a;
}
return sum;
}

public int size() {
return accounts.length;
}
}

将静态方法声明为 synchronized 也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。例如,如果 Bank 类有一个静态同步的方法,那么当该方法被调用时,Bank.class 对象的锁被锁住。

内部锁和条件存在一些局限:

  • 不能中断一个正在试图获得锁的线程。
  • 试图获得锁时不能设定超时。
  • 每个锁仅有单一的条件,可能是不够的。

$4-6 Lock/Condition/synchronized 的实践建议

  • 最好既不使用 Lock/Condition 也不使用 synchronized 关键字。许多情况下可以使用 java.util.concurrent 包中的机制,它会为你处理所有的加锁。例如,阻塞队列、并行流等。
  • 如果 synchronized 关键字适合,那么尽量使用它,可以减少编写的代码数量,减少出错的几率。
  • 如果特别需要 Lock/Condition 结构提供的独有特性时,才使用 Lock/Condition。

4-7 同步阻塞

每个 Java 对象有一个锁,线程可以通过调用 synchronized 方法获得锁。

此外还有一个机制可以获得锁,就是进入一个同步阻塞。例如下面代码获得 obj 的锁:

1
2
3
synchronized (obj) {
...
}

有时会用一个 Object 对象,专门用来使用 Java 对象锁,例如下面代码中的 lock 对象。

1
2
3
4
5
6
7
8
9
10
11
12
public class Bank {
private double[] accounts;
private Object lock = new Object();
...
public void transfer(int from, int to, int account) {
synchronized (lock) { // 一个 ad-hoc 锁
accounts[from] -= amount;
accounts[to] += amount;
}
System.out.println(...);
}
}

$4-8 监视器概念

锁和条件是线程同步的强大工具。但它们不是面向对象的,希望的是一种不需要考虑如何加锁的情况下保证多线程的安全性。

监视器 (monitor) 是一种方案,它有以下特性:

  • 监视器是只包含私有域的类。
  • 每个监视器类的对象有一个相关的锁。
  • 使用该锁对所有的方法进行加锁。换句话说,如果客户端调用 obj.method(),那么 obj 对象的锁是在方法调用开始时自动获得,并且当方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象操作时,没有其他线程能访问该域。
  • 该锁可以有任意多个相关条件。

Java 对象以不是很精确的方式采用了监视器概念:

Java 中的每一个对象有一个内部的锁和内部的条件。如果一个方法用 synchronized 关键字声明,那么它表现的就像是一个监视器方法。通过调用 wait/notifyAll/notify 来访问条件变量

Java 对象在以下三个方面不同于监视器,从而使得线程的安全性下降:

  • 域不要求必须是 private。
  • 方法不要求必须是 synchronized。
  • 内部锁对客户是可用的。

$4-9 Volatile 域

用现代的处理器与编译器,不适用同步的话,出错的可能性还是很大的:

  • 多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值。
  • 编译器可以改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变。然而,内存的值可以被另一个线程改变!

如果使用锁来保护可以被多个线程访问的代码,以上问题可以不用考虑。

下面两种使用变量的情况,必须使用同步:

  • 向一个变量写入值,而这个变量接下来可能会被另一个线程读取。
  • 从一个变量读值,而这个变量可能是之前被另一个线程写入的。

volatile 关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

volatile 例子

假如有一个对象有一个布尔标记 done,如下:

1
2
3
4
5
6
private boolean done;
public void isDone() {
return done;
public void setDone() {
done = true;
}

如果 done 被一个线程设置,但是被另一个线程查询,则我们可以使用内部锁,如下:

1
2
3
4
5
6
private boolean done;
public synchronized void isDone() {
return done;
public synchronized void setDone() {
done = true;
}

如果另一个线程已经对该对象加锁,isDone 和 setDone 可能阻塞。要解这个问题,线程可以为这一变量使用独立的锁,而不是用对象锁,但这也会带来其它问题。

此时将 done 声明为 volatile 合理,如下:

1
2
3
4
5
6
private volatile boolean done;
public void isDone() {
return done;
public void setDone() {
done = true;
}

但注意,volatile 变量不能提供原子性。例如下面的代码中,不能保证读取、翻转和写入不被中断。

1
2
3
public void filpDone() {
done = !done;
}

$4-10 final 变量

除了 volatile 外,还有一种情况可以安全地访问共享域,即这个域声明为 fianl。

$4-11 原子性

如果对共享变量除了复制之外不完成其他操作,可以将这些共享变量声明为 volatile。

java.util.concurrent.atomic 中的类的用法

java.util.concurrent.atomic 包中有很多类,使用了机器指令来保证操作的原子性。下面是几个例子:

(1) 自增

incrementAndGe t方法以原子方式将 AtomicLong 自增,并返回自增后的值。也就是获得值,增加 1,生成新值的操作不会中断。

1
2
public static AtomicLong nextNumber = new AtomicLong();
long id = nextNumber.incrementAndGet();

(2) 跟踪不同现场观察的最大值

1
2
public static AtomicLong largest = new AtomimLong();
largest.set(Math.max(largest.get(), observed)); // 错误,有竞争

上面代码是错的,第二行的更新不是原子的,需要用 compareAndSet 方法。

1
2
3
4
do {
oldValue = largest.get();
newValue = Math.max(oldValue, observed);
} while (!largest.compareAndSet(oldValue, newValue));

如果另一个线程也在更新 largest,就可能阻止这个线程更新。这样一来,compareAndSet 会返回 false,而不会设置新值。在这种情况下,循环会更次尝试,读取更新后的值,并尝试修改。最终,它会成功地用新值替换原来的值。

Java 8 以后,可以提供 lambda 表达式更新变量。如下:

1
2
largest.updateAndGet(x -> Math.max(x, observed));
largest.accumulateAndGet(observed, Math::max);

除了 AtomicLong 以外,还有下面这些类:

1
2
3
4
5
6
7
8
AtomicInteger
AtomicIntegerArray
AtomicIntegerFieldUpdater
AtomicLongArray
AtomicLongFieldUpdater
AtomicReference
AtomicReferenceArray
AtomicReference-FieldUpdater

大量线程访问相同原子值的情况

如果有大量线程(大量竞争)要访问相同的原子值,性能会大幅下降,因为乐观更新需要太多次重试。LongAdder 和 LongAccumulator 类来解决这个问题。

LongAdder 包括多个变量(加数),其总和为当前值。可以有多个线程更新不同的加数,线程个数增加时会自动提供新的加数。

LongAdder 对象可以调用 increment 让计数器自增,调用 add 增加一个量,或调用 sum 来获取总和。

1
2
3
4
5
6
7
8
9
10
11
12
13
final LongAdder adder = new LongAdder();
for (...) {
pool.submit(() -> {
while(...) {
...
if(...) {
adder.increment();
}
}
});
...
long total = adder.sum();
}

LongAccumulator 将这种思想推广到任意的累加操作。在构造器中,可以提供这个操作以及它的零元素。

LongAccumulator 对象可以调用 accumulate 方法加入新的值,调用 get 来获得当前值。

1
2
LongAccumulator adder = new LongAccumulator(Long::sum, 0);
adder.accumulate(value);

还可以选择不同的操作,例如计算最小值或最大值。但这个操作必须满足结合律和交换律。最终结果必须独立于所结合的中间值的顺序。

此外还有 DoubleAdder 和 DoubleAccumulator。

$4-11 死锁

用了锁和条件,在多线程中还是会遇到问题,例如死锁。

当程序挂起时,键入 CTRL+\,将得到一个所有线程的列表。每一个线程有一个栈踪迹,告诉你线程被阻塞的位置。可以运行 jconsole 并参考线程面板。

遗憾的是,Java 编程语言中没有任何机制可以避免或打破这种死锁现象。必须仔细设计程序,以确保不会出现死锁。

$4-12 线程局部变量

有时可能想要避免线程间共享变量,使用 ThreadLocal 辅助类为各个线程提供各自的实例。

线程不安全类的例子:java.text.SimpleDateFormat

考虑有一个该类型的静态变量:

1
public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

如果两个线程都执行以下操作:

1
String dateStamp = dateFormat.format(new Date());

结果会很混乱,因为 dateFormat 使用的内部数据结构可能由于并发访问被破坏。可以用同步,但开销很大;也可以在需要时构造局部的 SimpleDateFormat 对象,但太浪费了。

要为每个线程构造一个实例,代码如下:

1
public static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

要访问具体的方法,代码如下:

1
String dateStamp = dateFormat.get().format(new Date());

在一个给定线程中首次调用 get 时,会调用 initialValue 方法。在此之后,get 方法会返回属于当前线程的那个实例。

线程安全类的例子:java.util.Random

java.util.Random 类是线程安全的。但是如果多个线程需要等待一个共享的随机数生成器,这会很低效。

解法同样是线程局部变量,代码如下:

1
2
// ThreadLocalRandom.current() 返回特定于当前线程的 Random 实例
int random = ThreadLocalRandom.current().nextInt(upperBound);

java.lang.ThreadLocal<T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 得到这个线程的当前值。如果是首次调用get,会调用initialize来得到这个值。
T get()

// 应覆盖这个方法来提供一个初始值。默认情况下,这个方法返回null。
protected initialize()

// 为这个线程设置一个新值。
void set(T t)

// 删除对应这个线程的值。
void remove()

// 创建一个线程局部变量,其初始值通过调用给定的supplier生成。
static <S> ThreadLocal<S> withInitial(Supplier<?extends S>supplier)

$4-13 锁测试与超时

线程在调用 lock 方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。

锁测试

应该更加谨慎地申请锁。tryLock 方法试图申请一个锁,在成功获得锁后返回 true,否则,立即返回 false,线程可以立即离开去做其他事情。

1
2
3
4
5
6
7
8
9
if(mylock.tryLock()) {
try {
...
} finally {
mylock.unlock();
}
} else {
// 没获得锁,做其他事情
}

申请锁超时

申请锁时增加超时参数:myLock.tryLock(100, TimeUnit.MILLISECONDS)。TimeUnit 是一个枚举类型,可以取的值包括 SECONDS、MILLISECONDS、MICROSECONDS 和 NANOSECONDS。

lock 方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么,lock 方法就无法终止。

如果调用带有用超时参数的 tryLock,那么如果线程在等待期间被中断,将抛出 InterruptedException 异常。这是一个非常有用的特性,因为允许程序打破死锁。

lockInterruptibly 方法相当于一个超时设为无限的 tryLock 方法。

等待条件超时

等待条件增加超时: myCondition.await(100, TimeUnit.MILLISECONDS)

如果一个线程被另一个线程通过调用 signalAll 或 signal 激活,或者超时时限已达到,或者线程被中断,那么 await 方法将返回。

如果等待的线程被中断,await 方法将抛出一个 InterruptedException 异常。

$4-14 读写锁

java.util.concurrent.locks 下除了 ReentrantLock 之外,还有 ReentrantReadWriteLock 类。

如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话,则 ReentrantReadWriteLock 类很有用。此时允许对读者线程共享访问,而对写者线程互斥访问。

使用读写锁的步骤

构造一个 ReentrantReadWriteLock 对象。

1
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

抽取读锁和写锁。

1
2
private lock readLock = rwl.readLock();
private lock writeLock = rwl.writeLock();

对所有的获取方法加读锁。

1
2
3
4
5
6
7
8
public double getTotalBalance() {
readLock.lock();
try {
...
} finally {
readLock.unlock();
}
}

对所有的修改方法加写锁。

1
2
3
4
5
6
7
8
public void transfer(...) {
writeLock.lock();
try {
...
} finally {
writeLock.unlock();
}
}

Share