volatile是什么?有什么特性?
1、volatile是java虚拟机提供的轻量级的同步机制
三大特性如下:
1、保证可见性
2、不保证原子性
3、禁止指令重排
JMM内存模型是如何实现可见性?
JVM运行程序实体是线程,线程创建时JVM都会为其开辟一个工作内存,工作内存是每个线程私有的数据区域,而java内存模型中规定所有变量都存在主内存中 (主内存:相当于电脑上的内存条,有8G、16G等...)线程对变量的操作(读取和赋值)必须在自己的工作内存中进行,
首先要将变量从主内存中拷贝到自己的工作内存空间,然后对变量进行操作,操作完后再将变量写会主内存。 如下图:
图1 图2
1、由于添加了volatile的缘故,线程A对主内存共享变量的操作线程B是可见的,这就体现了volatile三大特性之一的 “保证可见性”
2、不保证原子性的原因:假如主内存有一个变量值为 age=18, 线程A和线程B分别将主内存的变量拷贝到自己的工作内存中, 线程A将age的值修改成 20,
然后在写回主内存,此时主内存的值为 20;由于各种原因,线程B网络故障导致修改时间比线程A慢了十几秒;而此时线程A又把主内存中的 20 改成 18,这时线程B
通过CAS(ComPareAndSwap):比较并交换)发现主内存中的值还是 18;于是把 18 改成了 22,虽然修改成功了,但并不表示没有问题了;CAS操作引起了ABA的问题。
3、禁止指令重排:多线程环境下线程交替执行,由于编译器优先重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测,用volatile修饰变量
可防止指令重排
验证volatile可见性、解决volatile不保证原子性的case

package com.company; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; class MyData { volatile int number = 0;//没加volatile的时候不保证可见性 public void addTo() { this.number = 60; } //此時已經添加了volatile,不保证原子性 public void addNumber() { number++; } //AtomicInteger解决原子性问题 AtomicInteger atomicInteger = new AtomicInteger();//不传参数,底层源码默认是 0 public void addPlus() { atomicInteger.getAndIncrement();//Atomically increments by one the current value. 源码注释每调一次就会自增 1 } } /** * 1.0 验证 volatile 的可见性 * 1.1 假设int number = 0,number变量不添加volatile关键字修饰,没有可见性 * 1.2 添加了volatile,可以解决可见性问题 * <p> * 2.0 volatile 不保证原子性 */ public class VolatileDemo { public static void main(String[] args) { atomicIntegerSee(); SeeVolatile(); } /** * AtomicInteger保证原子性 */ public static void atomicIntegerSee() { MyData myData = new MyData(); /** * 验证volatile不保证原子性 * number添加了volatile不保证原子性 * 20加到1000,答案是20000;但volatile不保证原子性 * 如何解决volatile不保证原子性问题? * 方法一:在方法上面添加 synchronized 关键字修饰,但每次访问只能有一个线程;导致并发性下降 * 方法二:使用 AtomicInteger 即可以保证原子性,又不会降低并发量 */ for (int i = 1; i <= 20; i++) { new Thread(() -> { for (int j = 1; j <= 1000; j++) { myData.addNumber();//volatile不保证原子性,所以最终结果不会是20000;会比20000小;有可能侥幸是20000 myData.addPlus();// 20个线程怎么加都是20000.能解决原子性的问题 } }, String.valueOf(i)).start(); } while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(Thread.currentThread().getName() + "\t volatile number value is: " + myData.number); System.out.println(Thread.currentThread().getName() + "\t AtomicInteger number value is: " + myData.atomicInteger); } /** * 1.0 验证volatile的可见性 */ public static void SeeVolatile() { MyData myData = new MyData(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }//线程休眠3秒钟 myData.addTo();//这时候number已经变成60,但由于没有添加volition关键字,其他线程并不知道number已经变成60 System.out.println(Thread.currentThread().getName() + "\t updata number: " + myData.number);//此时拿到的number已经是修改完后写回主内存的number }, "AAA").start(); while (myData.number == 0) { //如果取到的number等于0,main线程就会执行里面的内容;但此时main线程已经等于60;所以线程一直处于等待状态,下面的语句无法输出 } System.out.println(Thread.currentThread().getName() + "\t main Thread number is fish,main get number: " + myData.number); } }
解决CAS引起的ABA问题

1 package com.company; 2 3 import java.util.concurrent.TimeUnit; 4 import java.util.concurrent.atomic.AtomicReference; 5 import java.util.concurrent.atomic.AtomicStampedReference; 6 7 /** 8 * 版本号的原子引用 9 * 解决ABA问题:AtomicStampedReference 10 */ 11 public class ABADemo { 12 static AtomicReference<Integer> atomicReference = new AtomicReference<>(100); 13 static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(100, 1); 14 15 public static void main(String[] args) { 16 System.out.println("=================================以下是ABA问题的产生======================================="); 17 new Thread(() -> { 18 atomicReference.compareAndSet(100, 101); 19 atomicReference.compareAndSet(101, 100); 20 }, "t1").start(); 21 new Thread(() -> { 22 try { 23 TimeUnit.SECONDS.sleep(1); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 System.out.println(atomicReference.compareAndSet(100, 2019) + "\t " + atomicReference.get());//修改成功,但t1中间修改了两次又改回100,所以存在ABA问题 28 }, "t2").start(); 29 try { 30 TimeUnit.SECONDS.sleep(2); 31 } catch (InterruptedException e) { 32 e.printStackTrace(); 33 }//暂停两秒钟,保证上面的t1和t2 线程都操作完 34 System.out.println("=================================以下是ABA问题的解决======================================="); 35 new Thread(() -> { 36 int stamp = atomicStampedReference.getStamp();//获取当前版本号 37 try { 38 TimeUnit.SECONDS.sleep(1); 39 } catch (InterruptedException e) { 40 e.printStackTrace(); 41 }//休眠1秒,让t4也获得当前版本号 42 System.out.println(Thread.currentThread().getName() + "\t 第一版本号为:" + stamp); 43 atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); 44 System.out.println(Thread.currentThread().getName() + "\t 第二版本号为:" + atomicStampedReference.getStamp() + "\t 當前主內存中的值為:" + atomicStampedReference.getReference()); 45 atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); 46 System.out.println(Thread.currentThread().getName() + "\t 第三版本号为:" + atomicStampedReference.getStamp() + "\t 當前主內存中的值為:" + atomicStampedReference.getReference()); 47 }, "t3").start(); 48 new Thread(() -> { 49 int stamp = atomicStampedReference.getStamp();//获取当前版本号 50 System.out.println(Thread.currentThread().getName() + "\t 第一版本号:" + stamp); 51 try { 52 TimeUnit.SECONDS.sleep(3); 53 } catch (InterruptedException e) { 54 e.printStackTrace(); 55 }//休眠3秒,让t3完成一次ABA的操作 56 boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);//修改失败,版本号不匹配 57 /*boolean result=atomicStampedReference.compareAndSet(100,2019,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);*/ 58 System.out.println(Thread.currentThread().getName() + "\t 是否修改成功:" + result + "\t 当前最新版本为:" + atomicStampedReference.getStamp()); 59 System.out.println(Thread.currentThread().getName() + "\t 当前主内存的最新值为:" + atomicStampedReference.getReference()); 60 }, "t4").start(); 61 } 62 }
CAS优缺点
优点:
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。
缺点:
1、ABA问题。当第一个线程执行CAS操作,尚未修改为新值之前,内存中的值已经被其他线程连续修改了两次,使得变量值经历 A -> B -> A的过程。
2、循环时间长开销大。如果有很多个线程并发,CAS自旋可能会长时间不成功,会增大CPU的执行开销。
3、只能对一个变量进原子操作。JDK1.5之后,新增AtomicReference类来处理这种情况,可以将多个变量放到一个对象中。