1 volatile 的特性 这篇文章对于 volatile 介绍得很直观,一定要通读。文章链接>>
以下是对 volatile 作的补充。
1.1 volatile 的典型使用场景 1、用来标示状态量。 状态量标示就是通过一个 boolean 类型变量来判断逻辑是否需要执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private static volatile boolean flag = false; private static void refershFlag() throws InterruptedException { Thread threadA = new Thread(new Runnable() { @Override public void run() { while (!flag) { //do something } } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { flag = true; } }); DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); System.out.println("threadA start" + dateFormat.format(new java.util.Date())); threadA.start(); Thread.sleep(100); threadB.start(); threadA.join(); System.out.println("threadA end" + dateFormat.format(new java.util.Date())); } //threadA start2017/02/21 10:48:41 //threadA end2017/02/21 10:48:41
2、double-check 问题应该是 volatile 使用最多的场景了。如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class DoubleCheck { private volatile static DoubleCheck instance = null; private DoubleCheck() { } public static DoubleCheck getInstance() { if (null == instance) { //步骤一 synchronized (DoubleCheck.class) { if (null == instance) { //步骤二 instance = new DoubleCheck(); //步骤三 } } } return instance; } public static void main(String[] args) throws InterruptedException { DoubleCheck doubleCheck = DoubleCheck.getInstance(); } }
代码中步骤三并不是原子性的,可以分为三步: 3.1 为 DoubleCheck 在堆中申请一块内存空间(alloc memory address) 3.2 初始化对象 DoubleCheck(init DoubleCheck) 3.3 将申请的内存空间引用地址赋值给 instance 引用变量(instance > memory address) 在 JVM 看来3.2和3.3并不存在依赖关系,是有可能会重排序的,如果将3.2和3.3重排序:
线程2在步骤一时判断 instance 不为空的情况下,实际上对象并没有初始化,3.2并没有执行。导致接下来使用对象发生错误。此时使用 volatile 修饰 instance 变量就可以防止3.2和3.3重排序,这样就保证了多线程访问时代码的正确性。 3、下面的场景不是经常出现,但是可以作为对 volatile 完整理解的引子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public class VolatileClass { public static void main(String[] args) throws InterruptedException { AAndB aAndB = new AAndB(); for (int i = 0; i < 10000000; i++) { AThread aThread = new AThread(aAndB); BThread bThread = new BThread(aAndB); aThread.start(); bThread.start(); aThread.join(); bThread.join(); if (aAndB.x == 0 && aAndB.y == 0) { System.out.println("resort"); } aAndB.x = aAndB.y = aAndB.a = aAndB.b = 0; } System.out.println("end"); } public static class AAndB { volatile int x = 0; volatile int y = 0; volatile int a = 0; volatile int b = 0; public void awrite() { a = 1; x = b; } public void bwrite() { b = 1; y = a; } } public static class AThread extends Thread { private AAndB aAndB; public AThread(AAndB aAndB) { this.aAndB = aAndB; } @Override public void run() { super.run(); this.aAndB.awrite(); } } public static class BThread extends Thread { private AAndB aAndB; public BThread(AAndB aAndB) { this.aAndB = aAndB; } @Override public void run() { super.run(); this.aAndB.bwrite(); } } } // end
1、x,y 使用 volatile 修饰是为了避免 JVM 的重排序问题,这个在本文开头的链接中有提。 2、a,b 使用 volatile 修饰是为了保证数据的可见性。 3、所以,只有 x,y,a,b 全部都用 volatile 修饰才能保证 x,y 不同时为 0 。
参考链接>> (PS:这里只是习惯性地注明知识的来源。对于文链中的其余部分笔者还存有疑惑,请谨慎参考!而且,对于场景三笔者和文链中的观点是有些许不同的。)
1.2 从 JMM 理解 synchronized、volatile 等关键字 参考链接>>
2 AtomicInteger介绍 原子操作在多线程场景是很有必要的,它可以避免数据的二义性产生。下面的程序中我们新建了两个线程,每个线程对同一个数增加了4次,每次加1,最后的结果正确应该是8。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class JavaAtomic { public static void main(String[] args) throws InterruptedException { ProcessingThread pt = new ProcessingThread(); Thread t1 = new Thread(pt, "t1"); t1.start(); Thread t2 = new Thread(pt, "t2"); t2.start(); t1.join(); t2.join(); System.out.println("Processing count=" + pt.getCount()); } } class ProcessingThread implements Runnable { private int count; @Override public void run() { for (int i = 1; i < 5; i++) { processSomething(i); count++; } } public int getCount() { return this.count; } private void processSomething(int i) { // processing some job try { Thread.sleep(i * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
运行上面的程序后,你会发现最后的运行结果每次都不一样,可能是5,6,7或者8。原因就是 count++ 不具备原子性,当一个线程读取了 count 的值并进行完加1操作时,另外一个线程还是引用的原来旧的 count 值,这样就导致了最后加1后的结果不符合预期的现象。 为了解决这个问题,我们除了可以使用 Synchronized、Lock,还可以使用 Java 5 java.util.concurrent.atomic 提供的 AutomicInteger 实现原子操作。并且在程序逻辑允许的情况下,我们应该优先使用 atomic,它在执行速度、可读性、实用性方面均优于前两者 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class JavaAtomic { public static void main(String[] args) throws InterruptedException { ProcessingThread pt = new ProcessingThread(); Thread t1 = new Thread(pt, "t1"); t1.start(); Thread t2 = new Thread(pt, "t2"); t2.start(); t1.join(); t2.join(); System.out.println("Processing count=" + pt.getCount()); } } class ProcessingThread implements Runnable { private AtomicInteger count = new AtomicInteger(); @Override public void run() { for (int i = 1; i < 5; i++) { processSomething(i); count.incrementAndGet(); } } public int getCount() { return this.count.get(); } private void processSomething(int i) { // processing some job try { Thread.sleep(i * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
还有一个问题是,为什么 AutomicInteger 可以实现原子操作呢?这和它内部实现的 compareAndSwap(CAS) 有关系:
1 2 3 4 5 6 7 8 9 10 11 12 //java 8 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
通过与预期的值做比较,如果当前值和预期值相同,则说明没有被其他线程更改过,就可以放心的设置新增。否则重复检查直到赋值成功。
参考链接>>