重入锁死 Reentrance Lockout
重入锁死可能会发生在如果一个线程重入一个锁中,ReadWriteLock或者一些其他的同步器不是可重入的。可重入的意味着已经持有一个锁的线程可以再一次持有它。Java的同步块是可重入的,因此下面的代码将会没有问题的工作:
public class Reentrant{
public synchronized outer(){
inner();
}
public synchronized inner(){
//do something
}
}
下面的这个Lock的实现不是可重入的:
public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
如果一个线程调用lock方法两次,在期间没有调用unlock方法,对于lock方法的第二次调用将会锁住。一个可重入的锁死就会发生了。
对于重入锁死你有两个选择:
- 避免写重入锁的代码。
- 使用重入锁。
Compare and Swap
CAS(Compare And Swap)比较和替换常用于设计并发算法.通常的做法是将预期值和一个确切的变量相比较,如果相等,则将变量替换为新的值.
什么样的场景中可以用到CAS
在并发算法和程序中“先检查后执行”是一个很常见的模式.即先检查一个变量值然后基于这个变量值执行.如下所示:
class MyLock {
private boolean locked = false;
public boolean lock() {
if(!locked) {
locked = true;
return true;
}
return false;
}
}
这段代码在并发环境中会有线程安全问题,在这里我们先忽略。
我们可以看到,在lock()方法中,首先去检查变量locked是否为false(先检查),如果是的话则将它设置为true(后执行)。
为了确保在多线程应用中是线程安全的,“先检查后执行”操作必须是原子的。原子的意思是检查和执行必须在同一个原子代码块中执行,而不能分开。任何一个线程在执行原子代码块的过程中都不会受到其他线程的干扰直到结束为止。没有线程能在同一时间执行原子代码块。
下面将上文提及的实例更改为使用synchronized
关键字实现的原子代码块.
class MyLock {
private boolean locked = false;
public synchronized boolean lock() {
if(!locked) {
locked = true;
return true;
}
return false;
}
}
复制代码
现在lock()方式已经是同步的了,所以同一时间只能有一个线程能够在Mylock实例上执行.lock()方法同样已经是原子的了.
原子版的lock()方法已经是一个活生生的“比较和替换”的例子了。lock()方法先比较变量locked是否为预期值false,如果是的话,则将变量替换为true.
CAS即原子操作
现代CPU大部分已经内建支持原子的比较和替换操作。在Java5中,你可以使用java.util.concurrent.atomic
包中的原子对象来使用CPU中的这些功能.
下面示例了上文lock()方法如何使用AtomicBoolean对象.
public static class MyLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public boolean lock() {
return locked.compareAndSet(false, true);
}
}
我们可以注意到locked已经不是基础布尔类型,而是AtomicBoolean类型.这个对象有一个compareAndSet()方法用于比较AtomicBoolean实例值,若实例值等于一个预期值,则将它替换为新的值.在例子中,我们将比较locked值是否为false,如果是则将它替换为true.
compareAndSet()方法如果成功替换值则返回true,否则返回false.
使用Java5+中所提供原子对象的CAS功能,比起你自己实现的CAS带来的优势是能够使用CPU中的比较和替换特性.这比自己实现的CAS性能要优越得多.