面试的时候问,java有些什么并发的方式解决竞争条件,经常得到的回答是:synchronized, 我承认,这个回答没有问题。但是,能够再深入一点,再全面一点吗?
让人觉得眼前一亮的回答是:按照性能从坏到好,可以分为阻塞式加锁,非阻塞式加锁,无锁几种情况。
阻塞式加锁:即采用synchronized和ReentrantLock(还可以顺便说一下自旋锁,偏向锁等等),他们都是重量级的操作,使用ReentrantLock,常常是为了其他的特性,比如公平锁,条件,可中断等特性。当然,也可能是为了性能的原因。下图就是两种机制的性能测试(注:见后面参考)。
中间的一个小插曲是volatile,volatile可以轻量级的让变量在所有线程立即可见以及禁止指令重排。
举个例子,下面的increase在不加锁的情况下被多个线程调用是否安全的完成我们想要的语义?
private volatile int race = 0;
public void increase(){
race++;
}
非阻塞式加锁:我想,如果你不了解CAS的话肯定out了,没错,非阻塞式加锁就是所谓的乐观锁,它不会尝试先锁后操作,而是尝试先操作,如果没有冲突,就表示成功了,但是如果有冲突,再采取其他的补偿措施(如重试)。非阻塞式加锁是靠硬件指令集的发展才带动起来的。常见的非阻塞式加锁指令有,测试并设置(test-and-set),获取并增加(fetch-and-increment),交换(swap),比较并交换(compare-and-swap,即CAS)。上面的代码,如果换成
private AtomicInteger race = new AtomicInteger(0);
public void increase(){
race++;
}
无锁策略:分三类:1).不可变类。说明该类是类似String的对象,所谓的不可变,即将该类中的状态声明为final,表明此状态不再能够修改,比如String
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
private final char value[];
...
}
2)线程封闭(栈封闭)。这是我比较推崇的一种方式,如果一个方法只依赖于传进来的参数而不依赖于对象的状态,则这个方法就是可重入的,简单的讲就是这个方法不要调用对象中的实例变量,通过这种方式可以减少很多不必要的同步。举个例子,相信你经常听到servlet中尽量不要定义和调用实例变量,因为每个请求都会被一个线程调用,然后去调用同一个servlet的同一个方法,如果该方法用了实例变量,势必造成错误。
3)ThreadLocal。ThreadLocal相当于一个每个线程都有一个变量的保管箱,最好的理解方式就是把它想象成一个以线程为key,你要保管的实例为value的hashmap。事实上也是这样。在ThreadLocal的源码中,你会发现ThreadLocal将this传入作为key来查找变量。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
参考:reentrantLock与synchronized性能测试 http://www.ibm.com/developerworks/library/j-jtp10264/