7.3 锁与同步
一. 同步
程序员来负责多线程之间对 mutable
数据的共享操作,通过“同步”策略,避免多线程同时访问数据。
锁的实质是把一系列操作组合成一个原子操作。使用锁机制,获得对数据的独家 mutation
权,其他线程被阻塞,不得访问。
二. 锁
Lock
是 Java
语言提供的内嵌机制。每个 object
都有相关联的 lock
。使用以下方式加锁:
synchronized (lock) { // thread blocks here until lock is free
// now this thread has the lock
balance = balance + 1;
// exiting the block releases the lock
}
拥有 lock
的线程可独占式的执行该部分代码,这叫互斥。
Lock
保护共享数据,注意:要互斥,必须使用同一个 lock
进行保护。
1. Monitor
设计模式
ADT
所有方法都是互斥访问:
/** SimpleBuffer is a threadsafe EditBuffer with a simple rep. */
public class SimpleBuffer implements EditBuffer {
private String text;
...
public SimpleBuffer() {
synchronized (this) {
text = "";
checkRep();
}
}
public void insert(int pos, String ins) {
synchronized (this) {
text = text.substring(0, pos) + ins + text.substring(pos);
checkRep();
}
}
public void delete(int pos, String ins) {
synchronized (this) {
text = text.substring(0, pos) + text.substring(pos+len);
checkRep();
}
}
public int length() {
synchronized (this) {
return text.length();
}
}
public String toString() {
synchronized (this) {
return text;
}
}
}
用 ADT
自己做 lock
,所有对 ADT
的 rep
的访问都加锁。
但该模式效率低,只能顺序不能并行执行操作。
其实也可以通过:
/** SimpleBuffer is a threadsafe EditBuffer with a simple rep. */
public class SimpleBuffer implements EditBuffer {
private String text;
...
public SimpleBuffer() {
text = "";
checkRep();
}
public synchronized void insert(int pos, String ins) {
text = text.substring(0, pos) + ins + text.substring(pos);
checkRep();
}
public synchronized void delete(int pos, String ins) {
text = text.substring(0, pos) + text.substring(pos+len);
checkRep();
}
public synchronized int length() {
return text.length();
}
public synchronized String toString() {
return text;
}
}
Java
默认构造方法互斥,故上述程序的构造函数不添加 synchronized
关键字。
改变方式如下,但仍然得保证这样的两个方法不会出现 interleaving
的方式。
public class MsLunch {
private long c1 = 0
private long c2 = 0
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
原来的方式与改变后的方式相比:
- 后者需要显式的给出
lock
,且不一定非要是this
- 后者可提供更细粒度的并发控制
然而,同步机制给性能带来极大影响。除非必要,否则不要用。 Java
中很多 mutable
的类型都不是 threadsafe
就是这个原因。
故而加锁需尽可能减小 lock
的范围,并且避免在方法 spec
中加 synchronized
,而是在方法代码内部更加精细的区分哪些代码行可能有 threadsafe
风险,为其加锁。
原则上,要先去思考清楚到底 lock
谁,然后再 synchronized(…)
。
public static synchronized boolean findReplace(EditBuffer buf ,...)
注意,上一方法 (存在 static
)使用时是对类上锁,静态操作与类相关,范围很大,对性能带来极大损耗。
Synchronized
不是灵丹妙药,你的程序需要严格遵守设计原则,先尝试其他办法,实在做不到再考虑 lock
。
所有关于 threadsafe
的设计决策也都要在 ADT
中记录下来。
对同一个 mutable
对象的操作,必须要在各线程里用 synchronized
全部保护起来。
加锁原则:
- 任何共享的
mutable
变量/对象必须被lock
所保护 - 涉及到多个
mutable
变量的时候,它们必须被同一个lock
所保护
例如在 monitor pattern
中, ADT
所有方法都被同一个 synchronized(this)
所保护
class Poem {
private String title;
private List<String> lines = Collections.synchronizedList(...);
public synchronized void operation(String filter) {
Iterator<String> iter = lines.iterator();
while (iter.hasNext()) {
String line = iter.next();
if (line.contains(filter))
iter.remove();
}
title = title.toUpperCase();
}
}
happens-before
机制使用寄存器时检测寄存器值是否改变,改变了直接将改变后的值放入其中。可以防止但无法完全避免 race condition
。
三. 死锁
死锁:多个线程竞争 lock
,相互等待对方释放 lock
。
T1:synchronized(a){ synchronized(b){ … } }
T2:synchronized(b){ synchronized(a){ … } }
多个线程使用多个锁并且顺序不同,容易出现死锁。
/** An EditBuffer represents a threadsafe mutable
* string of characters in a text editor. */
public interface EditBuffer {
/**
* Modifies this by inserting a string.
* @param pos position to insert at
* (requires 0 <= pos <= current buffer length)
* @param ins string to insert
*/
public void insert(int pos, String ins);
/**
* Modifies this by deleting a substring
* @param pos starting position of substring to delete
* (requires 0 <= pos <= current buffer length)
* @param len length of substring to delete
* (requires 0 <= len <= current buffer length - pos)
*/
public void delete(int pos, int len);
/**
* @return length of text sequence in this edit buffer
*/
public int length();
/**
* @return content of this edit buffer
*/
public String toString();
}
该程序在执行以下两个线程会出现死锁:
Wizard harry = new Wizard("Harry Potter");
Wizard snape = new Wizard("Severus Snape");
//thread A //thread B
harry.friend(snape); snape.friend(harry);
harry.defriend(snape); snape.defriend(harry);
解决死锁的方式:
- 锁排序。但需要排序,效率较低;需要预先知道使用哪些锁。
public void friend(Wizard that) {
Wizard first, second;
if (this.name.compareTo(that.name) < 0) {
first = this; second = that;
} else {
first = that; second = this;
}
synchronized (first) {
synchronized (second) {
if (friends.add(that)) {
that.firend(this);
}
}
}
}
- 粗粒度锁(将多个锁变为一个锁)。
public class Wizard {
private final Castle castle;
private final String name;
private final Set<Wizard> friends;
...
public void friens(Wizard that) {
synchronized (castle) {
if (this.friends.add(that)) {
that.friend(this);
}
}
}
}
四. wait()
, notify()
, and notifyAll()
1. 保护块
某些情况下,某些条件未得到满足,所以一直在空循环检测,直到条件被满足。这是极大浪费。如:
public void guardedJoy() {
// Simple loop guard. Wastes
// processor time. Don't do this!
while(!joy) {} // 其他线程中更改 joy 之后,再往下执行
System.out.println("Joy has been achieved");
}
2. 解决方案
o.wait()
释放锁o
,阻塞当前线程,将其添加入o
的阻塞队列中o.notify()
通知被阻塞的线程恢复运行,o
的阻塞队列中随机一个被唤醒o.notifyAll()
通知被阻塞的线程恢复运行,o
的阻塞队列中所有线程被唤醒
这三个操作要求使用在synchronized(obj)
同步块中,并且 wait
前为 obj
:
synchronized (obj)
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
若不在方法前添加对象,直接使用如 wait()
实际上等价于使用 this.wait()
,锁就是当前对象,调用方法也应该是使用当前对象加锁。
public synchronized void guardedJoy() {
// This guard only loops once for each special event,
// which may not be the event we're waiting for
while(!joy) {
try {
wait();
} catch (InterruptedException e) {}
}
System.out.println("Joy and efficiency have been achieved");
}
2.1 Object.wait()
该操作使 object
所处的当前线程进入阻塞 等待状态,直到其他线程调用该对象的 notify()
操作。
2.2 Object.notify()
/ notifyAll()
Object.notify()
随机选择一个在该对象上调用 wait
方法的线程,解除其阻塞状态。
notifyAll()
所有在该对象上调用 wait
方法的线程,解除其阻塞状态。
public synchronized notifyJoy() {
joy = true;
notifyAll();
}