Java多线程基础总结三:volatile

前面的两篇总结简单的说明了同步的一些问题,在使用基础的同步机制中还有两个可以分享的技术: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++;   }}

这个简单的例子在读的方法上没有使用synchronized关键字,所以读的操作几乎没有等待;而由于写的操作是原子性的违反了使用条件 ,不能得到保证,所以使用synchronized同步得到写的正确性保证,这个模型在多读取少写入的实际场景中应该要比都用synchronized的性 能有不小的提升。

另外还有一个使用volatile的好处,得自于其原理:内部禁止改变两个volatile变量的赋值或者初始化顺序,并且严格限制volatile变 量和其周围非volatile变量的赋值或者初始化顺序。

Java代码

/**  * User: yanxuxin  * Date: Dec 12, 2009  * Time: 8:34:07 PM  */public class VolatileTest {   public static void main(String[] args) {     final VolatileSample sample = new VolatileSample();     new Thread(new Runnable(){       public void run() {         sample.finish();       }     }).start();      new Thread(new Runnable(){       public void run() {         sample.doSomething();       }     }).start();   }}class VolatileSample {   private volatile boolean finished;   private int lucky;   public void doSomething() {     if(finished) {       System.out.println("lucky: " + lucky);     }   }   public void finish() {     lucky = 7;     finished = true;   }}

这里首先线程A执行finish(),完成finished变量的赋值后,线程B进入方法doSomething()读到了finish的值为 true,打印lucky的值, 预想状态下为7,这样完美的执行结束了。但是,事实是如果finished变量不是声明了volatile的话,过程就有可能是这样的:线程A执行 finish()先对finished赋值,与此同时线程B进入doSomething()得到finished的值为 true,打印lucky的值为0,镜头切回线程A,接着给 lucky赋值为7,可怜的是这个幸运数字不幸杯具了。因为这里发生了扯淡的事情:JVM或许为了优化执行把两者的赋值顺序调换了。这个结 果在单线程的程序中简直绝对一定肯定就是不可能,遗憾的是多线程存在这个隐患。

所以不说其它的知识,想用Java实现正确,高性能的并发程序是需要处处小心的。后面想说的ThreadLocal就是看惯了线程为了共享数据 而屡屡发生惨剧后,想把数据与线程死死绑定不共享的另一个技术。当然还想尝试写写对atomic包的理解,对并发集合的理解,对线程池的 理解。所有的这些基础有个清晰的认识,才能有自信写写正确的,性能稍好的并发程序。

失败是什么?没有什么.只是更走近成功一步,

Java多线程基础总结三:volatile

相关文章:

你感兴趣的文章:

标签云: