前言
讲解锁等高级主题
正文
一. 死锁及其避免
-
死锁出现的原因通常是出现锁环路造成的, 不管是显示的还是隐式的
-
通过定义获取锁的顺序来避免死锁: 通过
System.identityHashCode(object)
来返回一个锁对象的Object.hashCode()
的返回值; 通过这个值来定义锁的顺序, 并在整个应用程序中都按照这个顺序来获取锁, 那么就不会形成锁环路了
Object fromAcct = new Object(); // 这里的锁对象也可以是其他
Object toAcct = new Object();
Object tieLock = new Object();
int fromHash = System.identityHashCode(fromAcct);
int toHash = System.identityHashCode(toAcct);
if (fromHash < toHash) {
synchronized (fromAcct) {
synchronized (toAcct) {
// doSomething
}
}
} else if (fromHash > toHash) {
synchronized (toAcct) {
synchronized (fromAcct) {
// doSomething
}
}
} else { // 这种情况主要是避免Hash冲突, 此时需要额外的锁
synchronized (tieLock) {
synchronized (fromAcct) {
synchronized (toAcct) {
// doSomething
}
}
}
}
-
使用开放调用(如果在调用某个方法时, 不需要持有锁, 那么这种调用就被成为开放调用)避免死锁: 这种情况下适用于持有不同锁的方法之间的相互调用(注意锁的级别是在方法上, 而不是代码块上)(相当于形成一个隐式的环); 将锁从方法上转移到需要同步的内部代码块上可以避免死锁
-
Lock.tryLock()
: 可以检测死锁和从死锁中恢复过来; 即指定一个超时时限, 在等待超过该时间后tryLock()
会返回一个失败信息 -
需要注意的几点:
- 对象分配操作的开销比同步的开销更低(实际上, 现在
Java
的分配操作已经比C
语言的malloc
调用更快ConcurrentHashMap
的以前实现中(现在实现不同咯)采用分段锁, 使用了一个包含16
个锁的数组, 每个锁保护散列桶的1/16
, 这样就能够支持多达16
个并发的写入器;ConcurrentHashMap
中的size
将对每个分段进行枚举并将每个分段中的元素数量相加, 而不是维护一个全局计数, 为了避免枚举每个计数,ConcurrentHashMap
为每个分段都维护了一个独立的计数, 并通过每个分段的锁来维护这个值(这个地方有两点需要注意: 一个是size
的计算是在每个add
,remove
操作都会改变一次, 这样虽然增加了add
和remove
的操作, 但是这会使得size
的计算从O(n)
变为O(1)
; 另一个是为每个分段维护一个size
, 而不是使用一个全局的共享size
, 如果使用一个全局的共享size
的话, ,实际上就破坏了分段锁的分段特性, 因为访问共享size
的时候, 需要同步)