volatile和jmm
volatile的理解:
volatile是jvm提供的轻量级的同步机制
1.保证可见性
2.不保证原子性
3.禁止指令重排
为什么叫轻量级同步呢,是因为它基本上遵从了jmm的规范, 但是它又不能保证可见性。所以是乞丐版的同步机制。
JMM的三大特性:
原子性,可见性,有序性
jmm本身是一个抽象的概念并不真实存在。
jmm关于同步的规定,
1.线程加锁前,必须读取主内存中的最新值到工作内存中
2.线程解锁前,必须将工作内存的值刷新到主内存中
3.加锁解锁是用的同一把锁
jmm内存模型的解释:
jvm运行程序的实体是线程,每个线程在jvm都会创建一个工作内存,工作内存是每个线程的私有化数据。java内存模型中规定所有的变量都存储在主内存,主内存所有线程都可以访问。但是线程对数据的操作只能在工作内存中进行,首先要将主内存中的数据拷贝到自己的工作内存,然后对变量进行操作,操作完成后再写回主内存。不同的线程无法访问对方的工作内存,线程之间的通信必须通过主内存来完成。
可见性就是: 在操作完成写回主内存后,会立刻通知其他线程更新数据。
原子性就是:某个线程在做某个具体业务时,不可分割,需要整体完整性。要么同时成功,要么同时失败。
代码验证volatile的可见性:
/** * volatile可见性分析: * 这个程序有两个线程,Thread1线程和main线程, * 让Thread先sleep, main线程先进到while中, * 如果可见性,则修改了number会被main线程获取,则会终止while循环执行,打印最后一句。 * 如果不可见,则修改了number后main也无法获取修改后的number,会一直运行在while语句,无法打印最后一句。 * */ public class volatileVisiableTest { public static void main(String[] args){ Data myData = new Data(); //在这个线程修改值 new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + "更新开始"); //暂停线程一会,让main线程先开始进到while TimeUnit.SECONDS.sleep(3); myData.addNumberTo60(); System.out.println(Thread.currentThread().getName() + "更新完成"); } catch (InterruptedException e) { e.printStackTrace(); } },"Thread1").start(); // main线程判断,如果值不为零,则放行 while(myData.number == 0) { } System.out.println(Thread.currentThread().getName() + "main执行完成, number : " + myData.number ); } } class Data { volatile int number; public void addNumberTo60() { this.number = number + 60; } }
加了volatile实现了可见性:
当不加 volatile修饰number, 程序就会在while一直运行,因为Thread1修改后的数据对main线程不可见:
代码验证volatile无原子性:
/** * 这里创建了20个线程,每个线程增加1000次, * 如果volatile能保证原子性,则最终结果是20000 * 如果不能保证原子性,则最终结果不一定是20000 * */ public class VolatileAtomicTest { public static void main(String[] args) throws Exception { CountDownLatch cdl = new CountDownLatch(20); Add add = new Add(); // 创建20个线程,每个线程对i增加100次 for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 1; j <= 1000; j++) { add.plus(); } cdl.countDown(); }).start(); } // 上面线程全部执行完成再执行主线程 cdl.await(); System.out.println(add.num); } } class Add { volatile int num = 0; public void plus() { num++; } }
多次运行每次的结果都不是20000。所以证明volatile不保证原子性。
如何解决原子性?
1. 使用synchronized在plus方法上
2. 使用AtomicInteger类
class Add { AtomicInteger integer = new AtomicInteger(); public void plus() { integer.getAndIncrement(); } }
指令重排:
计算器在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。指令在重排时必须考虑指令之间的数据依赖性,所以单线程环境里确保程序最终执行结果和代码顺序执行的结果一致。但是在多线程环境中线程交替执行,两个线程中变量能否一致性是无法确定的。
内存屏障(Memory Barrier)又称内存栅栏,是一个cpu指令,有两个作用:
1. 保证特定操作的执行顺序
2. 保证某些变量的可见性
利用该特性实现volatile的内存可见性
通过插入内存屏障禁止在内存屏障前后的执行执行重排序优化,内存屏障的另一个作用是强制刷出各种cpu的缓存数据。
DCL (double control lock)单例模式的双端检锁机制不一定线程, 安全分析为什么不一定线程安全:
如果不加volatile关键字:
1 public class Single { 2 private Single single = null; 3 private Single() { 4 System.out.println("实例化了Single"); 5 } 6 7 public Single getInstance() { 8 if(single == null) { 9 synchronized (Single.class) { 10 if(single == null) { 11 single = new Single(); 12 } 13 } 14 } 15 return single; 16 } 17
11行的single = new Single() 实际上是三个步骤
1.分配内存空间
2.初始化Single对象
3. 将初始化的对象指向single引用
如果能指令重排,会造成2,3 指令重排,也就是先分配了内存空间,再将single指向了这块内存空间,但是还没对对象初始化完成, 这时候single就不是null了。当有一个线程执行到78行之间,判断到single !== null就会直接返回single对象。但是single还没初始化完成,就出问题了。
如果加上volatile关键字,就能防止single = new Single()进行指令重排,就会在对象初始化完成后再指向引用。上面的问题就解决了。所以单例模式是这样的:
/** * 单例模式 */ public class Single { private volatile Single single = null; private Single() { System.out.println("实例化了Single"); } public Single getInstance() { if(single == null) { synchronized (Single.class) { if(single == null) { single = new Single(); } } } return single; } }
CAS:
atomicInteger是怎么实现的:
先看下面的例子:
解释一下这段代码:
第一次预期值与integer对象里的值一致,就更新成功并更新为1024; 第二次对象里的值是1024,但是预期值是5,所以更新失败。
所以这里有个概念,就是比较并且交互
再看看下面这段的代码解析:
public class AtomicTest { public static void main(String[] args) { AtomicInteger integer = new AtomicInteger(5); integer.getAndIncrement(); } }
分析一下getAndIncrement()方法(也就是cas的原理):
源码:
Unsafe类是rt.jar包下的,是CAS核心类,由于java方法无法直接访问底层系统, 需要本地native方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存数据。其内部方法操作可以像C的指针一样操作内存。java中的CAS操作的执行依赖Unsafe类的方法。
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; }
看getAndAddInt这个方法,分析这段意思:
var1是当前对象,var2是偏移量也就是对象的地址值
var5 = this.getIntVolatile(var1, var2);这一步是通过对象的地址拿到主存中的数据作为当前线程的快照var5。
!this.compareAndSwapInt(var1, var2, var5, var5 + var4) 通过var1和var2能获取到主存中的数据,与var5这个快照进行比较,如果不一致就返回false, 继续到do while重新拿到var5作为线程的快照再比较; 如果一致就返回对var5+1返回。
这个就叫自旋
CAS的缺点:
如果CAS长时间不成功,会给CPU带来很大的开销。
对多个共享变量的操作,CAS无法保证操作的原子性,这时候就可以用锁来保证原子性。
AtomicInteger的ABA问题?原子引用?
ABA问题,如果同时有两个线程去修改一个变量A, thread1花费的时间比较长,thread2花费的时间短,当thread2 从A修改成B,再从B修改成A写回主内存。这时候A才运行完成,CAS发现值没变化就操作成功了。
尽管线程1的CAS操作成功,但不代表这个过程没有问题。
这个过程的模拟:
/** * 原子引用 */ public class AtomicReferenceTest { public static void main(String[] args) { AtomicReference<Integer> ref = new AtomicReference(5); // 模拟thread1 new Thread(() -> { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("是否修改成功" + ref.compareAndSet(5, 1024) + " ,修改后的值:" + ref.get()); },"thread2").start(); // 模拟thread2 new Thread(() -> { ref.compareAndSet(5,6); ref.compareAndSet(6,5); },"thread1").start(); } }
ABA问题的解决:
使用AtomicStampedReference。 模拟代码:
public class AtomicStampedReferenceTest { public static void main(String[] args) { // 初始值是5,版本号为1 AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(5, 1); // thread1 new Thread(() -> { int stamp = ref.getStamp(); System.out.println(Thread.currentThread().getName() + "第一次的版本号是:" + stamp); // 等待thread2的ABA操作完成 try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } // 修改thread1的值, 如果不能修改成功则说明ABA解决了 boolean b = ref.compareAndSet(5, 2048, stamp, stamp + 1); // 打印结果验证 System.out.println(Thread.currentThread().getName() + "是否修改成功?" + b + " 当前值为:" + ref.getReference() + "当前的版本号为:" + ref.getStamp()); }, " thread1").start(); // thread2 new Thread(() -> { int stamp = ref.getStamp(); System.out.println(Thread.currentThread().getName() + "第一次的版本号是:" + stamp); //这里睡一秒的目的是为了thread1第一次拿到的版本号是1 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 模拟ABA操作 boolean b2 = ref.compareAndSet(5, 6, ref.getStamp(), ref.getStamp() + 1); System.out.println(Thread.currentThread().getName() + "第二次版本号为:" + ref.getStamp() + ",是否修改成功?" + b2); boolean b3 = ref.compareAndSet(6, 5, ref.getStamp(), ref.getStamp() + 1); System.out.println(Thread.currentThread().getName() + "第三次版本号为:" + ref.getStamp() + ",是否修改成功?" + b3); }, "thread2").start(); } }
这段代码用intellij看更直观:
运行结果: