一、volatile关键字
volatile是java虚拟机提供的轻量级的同步机制
三大特性:
- 保证可见性
- 不保证原子性
- 禁止指令重排
1.1 JMM内存之可见性
JMM(Java内存模型Java memory Model简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规范或规则,通过这组规范定义了程序中各个变量(包含实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存。
- 线程加锁前,必须读取主内存的最新值到自己的工作
- 加锁解锁是同一把锁。
由于jvm运行程序的实体是线程,而每个线程创建时jvm都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量对操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图所示:
可见性代码验证:
![]() | ![]() |
疑问1:
public class interview {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"come in");
try { Thread.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
myData.add();
System.out.println(Thread.currentThread().getName()+"update mydata number:"+myData.number);
},"线程1---").start();
while(myData.number == 0){
}
System.out.println(myData.number+"结束了");
}
}
class MyData{
int number =0;
public void add(){
this.number=60;
}
}
没有加volatile实现了可见性
结果:
线程1---come in
线程1---update mydata number:60
60结束了
Volatile为什么不能保证原子性?如下图:
怎么解决Volatile的原子性?
- 第一种方法:Synchonized这个不好,不建议使用
- 第二种方式:直接使用AtomicInteger atomicInteger = new AtomicInteger();JUC的Atomic的原子类
- 或者加锁
有序性:(volatile 禁止指令重排序)
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排
源代码--》编译器优化的重排--》指令并行的重排---》内存系统的重排--》最终执行的指令
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。(单线不用担心指令重排)
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致是无法确认的,结果无法预测
不禁止重排的情况:
![]() |
禁止指令重排序总结:
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
先了解一个概念,内存屏障(Memory barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
- 1、是保证特定操作的执行顺序
- 2、保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重优化。如果在指令间插入一条Memory barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory barrier指令重排序,也就是说通过插入内存屏障前后禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
线程安全性获得保证
- 工作内存与主内存同步延迟现象导致的可见性问题
- 可以使用synchronized或者volatile关键字解决,他们都是可以使一个线程修改后的变量立即对其他线程可见
- 对于指令重排序导致的可见性问题和有序性问题
- 可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。
volatile使用的场景:
单例模式DCL(double check lock)代码
public class SingleTest {
private static volatile SingleTest singleTest =null;
private SingleTest(){
System.out.println("构造器");
}
public static SingleTest getSingleTest(){
if(singleTest == null){
synchronized (SingleTest.class){
if(singleTest==null){
singleTest = new SingleTest();
}
}
}
return singleTest;
}
}
单例模式volatile分析
原因在于某一个线程执行到第一次检测,读取到到
singleTest不为null时,
singleTest到引用对象可能没有完成初始化。
singleTest = new SingleTest();可以分为一下3步完成(伪代码)
- 步骤1:memory = allocate()//1.分配对象内存空间
- 步骤2:allocate(memory)//2.初始化对象
- 步骤3:singleTest = memory //3.设置singleTest指向刚分配到内存地址,此时singleTest != null
步骤2和步骤3不存在数据依赖关系,而且无论重排序还是重排序后程序到执行结果在单线程中并没有改变,因此这种重排序优化是允许的。memory = allocate()//1.分配对象内存空间 singleTest = memory //3.设置singleTest指向刚分配到内存地址,此时singleTest != null,但是对象还没有初始化完成!allocate(memory)//2.初始化对象
但是指令重排序只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问singleTest不为null时,由于singleTest实例未必已初始化完成,也就造成了线程安全问题。
JMM保证多线程编程的原理:
- 可见性
- 原子性
- 有序性
二、CAS(Compare-And-Swap)算法
那我们以AtomicInteger类来说,
为什么下面在多线程的操作下是线程安全的?
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomicInteger() {
atomicInteger.getAndIncrement();
}
因为一下几点:
- Unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
- 变量valueOffset,表示该变量值在内存中的偏移地址(也就是内存地址),因此Unsafe就是根据内存偏移地址获取数据的。
- 变量value用volatile修饰,保证了多线程之间的内存可见性.
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
- CAS,他是一条CPU并发原语。
- 它的功能是判断内存某个位置的值是否为预期更改为新的值,这个过程是原子的。
- CAS并发原语提现在java语言中就是sun.misc.Unsafe类的各个方法。调用Unsafe类中的CAS方法,JVM会帮助我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由于若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
CAS原理:1、Unsafe 2、自旋锁(直到比较跟新完成)
- CAS比较当前工作内存中的值和主存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。
- CAS应用;CAS有3个操作数,内存V,旧的预期值A,要修改的跟新值B。当切仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做(自旋)
CAS的缺点:
- 循环时间长,开销大
- 我们可以看到getAndAddInt方法执行时,有个do while方法,如果CAS失败,会一致进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
- 只能保证一个共享变量的原子操作
- 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个变量操作时,循环CAS就无法保证操作的原子性,这个时候讲究可以用锁来保证原子性
- ABA问题
三、ABA问题
CAS--》UnSafe---》CAS底层思想---》ABA-----》原子引用跟新---》如何规避ABA问题
- CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
- 比如说线程1从内存为止V取出A,这时候另外一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,然后线程1操作成功。(线程2快于线程1)。
- 尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。
四、AtomicReferece原子引用
AtomicReference底层的原理是UnSafe类
public class AtomicReferenceTest {
public static void main(String[] args) {
User xiaoming = User.builder().age(10).name("xiaoming").build();
User user = User.builder().age(20).name("小王").build();
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(xiaoming);
System.out.println(atomicReference.compareAndSet(xiaoming,user)
+"\t\n"+atomicReference.get());
System.out.println(atomicReference.compareAndSet(xiaoming,user)
+"\t\n"+atomicReference.get());
}
}
@Builder
@Data
class User{
private Integer age;
private String name;
}
结果:
true
User(age=20, name=小王)
false
User(age=20, name=小王)
// 使用 null 初始值创建新的 AtomicReference。
AtomicReference()
// 使用给定的初始值创建新的 AtomicReference。
AtomicReference(V initialValue)
// 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
boolean compareAndSet(V expect, V update)
// 获取当前值。
V get()
// 以原子方式设置为给定值,并返回旧值。
V getAndSet(V newValue)
// 最终设置为给定值。
void lazySet(V newValue)
// 设置为给定值。
void set(V newValue)
// 返回当前值的字符串表示形式。
String toString()
// 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
boolean weakCompareAndSet(V expect, V update)
五、AtomicStampedReference(带时间戳的原子引用)版本引用
ABA问题的解决:
下面是伪代码:
public class ABATest {
/**
* 没有加版本的原子引用
*/
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
/**
* 加版本号的原子引用
*/
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("以下是ABA问题的产生");
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"线程1").start();
new Thread(()->{
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+
"\t"+atomicReference.compareAndSet(100,101)+
"\t结果是"+atomicReference.get());
},"线程2").start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("以下是ABA问题的解决方案");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"获取的版本号是:"+stamp);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);
System.out.println(Thread.currentThread().getName()+"获取的版本号是:"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"获取的版本号是:"+atomicStampedReference.getStamp());
},"线程3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"获取的版本号是:"+stamp);
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---"+atomicStampedReference.compareAndSet(100,101,stamp,stamp+1));
System.out.println(Thread.currentThread().getName()+"获取的版本号是:"+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+"获取的值是:"+atomicStampedReference.getReference());
},"线程4").start();
}
}
运行结果:
以下是ABA问题的产生
线程2 true 结果是101
以下是ABA问题的解决方案
线程3获取的版本号是:1
线程4获取的版本号是:1
线程3获取的版本号是:2
线程3获取的版本号是:3
线程4---false
线程4获取的版本号是:3
线程4获取的值是:100
后续补充