前面的两篇总结简单的说明了同步的一些问题,在使用基础的同步机制中还有两个可以分享的技术:volatile关键字和ThreadLocal。合 理的根据场景利用这些技术,可以有效的提高并发的性能,下面尝试结合自己的理解叙述这部分的内容,应该会有理解的偏差,我也会尽量 的在完善自己理解的同时同步更新文章的错误。
或许在知道synchronized配和对象内部锁的机制以后,可以提高写出正确同步的并发程序成功率,但是这时候会遇到另一个大问题:性 能!是的,对于 synchronized带来的可能庞大的性能成本,开发者们总结出不同的优秀的优化方案:常见的是锁的分解和锁持有时间的最 小化。有效的降低锁持有的时间对竞争线程激烈的调用会大大的提高性能,所以不要轻易的在方法上声明synchronized,应该在需要保护的 代码块上添加 synchronized。另一个方案是拆分锁的竞争颗粒的大小,与其几百个线程竞争一个对象的锁,不如几个或者几十个线程竞争 多个对象的锁,常见的应用是ConcurrentHashMap的实现,其内部有类似的锁对象数组维护每段表内的线程竞争,默认16个对象锁,当然提 供参数可调。这对于存储了成千上万个实例的map性能提升不言而喻,线程的竞争被分散到多段的小竞争,再也不用全部的堆在门口傻等了 。
但是synchronized同步和类似的机制带来的性能成本,还是使得开发者不能不研究无锁和低成本的同步机制来保证并发的性能。 volatile就是被认为“轻量级的synchronized”,但是使用其虽然可以简化同步的编码,并且运行开销相对于JVM没有优化的竞争线程同步 低,但是滥用将不能保证程序的正确性。锁的两个特性是:互斥和可见。互斥保证了同时只有一个线程持有对象锁进行共享数据的操作,从 而保证了数据操作的原子性,而可见则保证共享数据的修改在下一个线程获得锁后看到更新后的数据。volatile仅仅保证了无锁的可见性, 但是不提供原子性操作的保证!这是因为volatile关键字作用的设计是JVM阻止volatile变量的值放入处理器的寄存器,在写入值以后会被 从处理器的cache中flush掉,写到内存中去。这样读的时候限制处理器的cache是无效的,只能从内存读取值,保证了可见性。从这个实现 可以看出volatile的使用场景:多线程大量的读取,极少量或者一次性的写入,并且还有其他限制。
由于其无法保证“读-修改-写”这样操作的原子性(当然java.util.concurrent.atomic包内的实现满足这些操作,主要是通过 CAS-- 比较交换的机制,后续会尝试写写。),所以像++,--,+=,-=这样的变量操作,即使声明volatile也不会保证正确性。围绕这个原理的主题 ,我们可以大致的整理一下volatile代替synchronized的条件:对变量的写操作不依赖自身的状态。所以除了刚刚介绍的操作外,例如:
Java代码
private volatile boolean flag;
if(!flag) {
flag == true;
}
类似这样的操作也是违反volatile使用条件的,很可能造成程序的问题。所以使用volatile的简单场景是一次性的写入之后,大量线程 的读取并且不再改变变量的值(如果这样的话,都不是并发了)。这个关键字的优势还是在于多线程的读取,既保证了读取的低开销(与单 线程程序变量差不多),又能保证读到的是最新的值。所以利用这个优势我们可以结合synchronized使用实现低开销读写锁:
Java代码
/**
* User: yanxuxin
* Date: Dec 12, 2009
* Time: 8:28:29 PM
*/
public class AnotherSyncSample {
private volatile int counter;
public int getCounter() {
return counter;
}
public synchronized void add() {
counter++;
}
}