首先我们来看一下 什么事volatile,和volatile能解决jmm内存模型中的什么问题
jmm模型其实是一个思想,规则,在多线层的情况下,我们必须满足“可见性” “原子性” '有序性'
可见性:因为传统的n++在单线程的环境下是没有其他线程和他抢占Cpu的资源的,但是在多线程的环境下,就不一定了。他是先把Cpu主内存的值num读到线程自己的内存空间此时主内存和num内存的值都是1,当其中一个线程修改了工作内存中的值以后,并且把它写入到主内存,同时告诉其他线程我主内存中的num值变了,但是没有volatile这种可见性是看到不的,volatile就是解决了这个问题。
“有序性“:从字面意思可以解释为,程序运行时按照允许执行的,但是我们的程序在计算机眼里可不一定是按照我么写代码的顺序去执行的。举个例子
public Class RedorSeqDemo{
int a=0;
boolean flag=false
public void method01(){a =1 flag =true}
public void method2(){
if(flag){a=a+5 sout("a=" ,a)}
}
}
在单线程的情况下我们是不需要考虑有序性的,这时候他的值就是0,调用method1方法 将a改成1
此时调用方法method2就会变成 a的值 6
但是在多线程的情况下,由于计算机的指令重排,是无法预测的 那么俩个线程使用的变量能否保证一致性是无法确定的,结果无法预测
此时A线程可能定义了flag=true,然后就执行了method2方法,以判断 初始化的值是0 那么a的值就变成了5,此时加上了Volatile就解决了这个指令重排的问题(有序性的问题)
“原子性“:
所谓的原子性,就是有点像我们事务的原子性很像没要么都成功,要么都失败,但是在jmm内存中原子性的,也是这样在我们再程序运行的时候不被打扰,从而保持结果的整性,由于我们的volatile并不能保证我们原子性那么再多线程的情况下我们程序在运行过程中导致结果的错误。
volatile 不保证原子性的演示:
class Mydate {
// 加volatile 验证原子性
volatile int num =0 ;
public void add(){
this.num = 60 ;
}
public void addPlus(){
num++;
}
}
class VolatileDemo{
public static void main(String[] args) {
// 现在我们需要20 线程 做100次
Mydate mydate = new Mydate();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j <1000 ; j++) {
mydate.addPlus();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(mydate.num);
}
}
按道理来讲运行结果是20000他就会保证原子性 ,但是运行结果看来并不是20000所以volatile并不保证原子性
总结:
1:为什么volatile在多线程的情况下不保证原子性呢 因为我们线程在工作的时候线程在操作共享变量的时候
解决原子性的问题
1:只需要synchronized就可以解决了对于我们来说synchronized解决就有点像杀鸡我们用牛刀,这样不是最好的方法
2:可以用Cas来解决,cas其实是一种自旋锁的思想,在我们java.util.concurrent包下面的 AotumicInteger 中的 getAndAddInteger来解决,他其实跟 i++ 是一样的思想,他是怎么解决的呢
在这个方法中,我们有2个值,一个是 当前期望值,和修改后的值,在多线程的环境下,我们其中一个线程堆主内存的 值进行修改 ,但是其他线程在想对其修改的时候它就要去判断一下自己线程中的值和主内存的值是不是一样的。如果不一样他会在哪里一直自旋,直到 一样为止。
Cas 为什么可以解决呢:是因为他底层源码使用Unsafe类,这个类是用Native修饰的,在jdk一出生的时候就有了。(也就是打娘胎来的时候就有了),先比较在修改
var 1 表示当前这个对象, var2 表示这个对象的地址值
var5 = this.getIntVolatile(var1, var2) 表示去拿到当前对象所对印的那个地址取出VALUE
这里面也就是说通过拿到这个主内存中的值 ,拿到以后我们去比较,如果一样我们就进行就该,如果不一样那么就一直在那里自旋着。直到对象一样为
总结: 上面我们可以看出,对于CAS我们可以解决原子性的问题,但是也会存在这样一个问题AB
ABA问题:
ABA问题解决:
1:利用AtomicReferenceDemo 原子引用,下面请看代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.atomic.AtomicReference;
@Data
@NoArgsConstructor
@AllArgsConstructor
class Mydate {
// 加volatile 验证原子性
String userName;
int age;
}
class VolatileDemo
{
public static void main(String[] args)
{
Mydate z3 = new Mydate("z3", 24);
Mydate li4 = new Mydate("li4", 25);
AtomicReference<Mydate> atomicReference = new AtomicReference<>();
// 这里表示的是 当线程将我们的张三读到工作线程,但是我像把主内存的值的值是lisi
atomicReference.compareAndSet(z3, lisi);
System.out.println(atomicReference.compareAndSet(z3, li4) + "-" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, li4) + "-" + atomicReference.get().toString());
}
}
总结:说明存在ABA问题,并且没有修改成功。
只需要在上面加上版本号就行了下面请看代码如何解决的
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
static AtomicReference<Integer> atomicReference =new AtomicReference<>(100);
static AtomicStampedReference<Integer> integerAtomicStampedReference =new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
// // 线程 t1
// new Thread(() -> {
// atomicReference.compareAndSet(100, 101);
// atomicReference.compareAndSet(101, 100);
// },"t1").start();
// // 线程t2
// new Thread(() -> {
// try{
// TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){e.printStackTrace();}
// System.out.println(atomicReference.compareAndSet(100,2019)+"\t"+atomicReference.get().toString());
// // true 2019
// },"t2").start();
// 暂停一会线程
try{
TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){e.printStackTrace();
}
System.out.println("===========================解决ABA问题");
// 线程t4
new Thread(() -> {
int stamp = integerAtomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第一次版本号:"+stamp);
// 暂停一秒
try{ TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){e.printStackTrace();}
integerAtomicStampedReference.compareAndSet(100,101,integerAtomicStampedReference.getStamp(),integerAtomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t第二次版本号:"+integerAtomicStampedReference.getStamp());
integerAtomicStampedReference.compareAndSet(101,100,integerAtomicStampedReference.getStamp(),integerAtomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t第三次版本号:"+integerAtomicStampedReference.getStamp());
},"t3").start();
// 线程4
new Thread(() -> {
int stamp = integerAtomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第一次版本号:"+stamp);
// 暂停3秒 t4线程 保证t3 线程完成一次ABA操作
try{ TimeUnit.SECONDS.sleep(3); }catch (InterruptedException e){e.printStackTrace();}
boolean result = integerAtomicStampedReference.compareAndSet(100, 2019, stamp, stamp+1);
System.out.println(Thread.currentThread().getName()+"\t修改成功否:"+result+"\t"+"当前最新版本号:"+integerAtomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+"\t当前实际最新值:"+integerAtomicStampedReference.getReference());
},"t4").start();
}
}
总结:上面的代码就是解决ABA问题的实现方式应用到AtomicStampedReference这个类表示,在每次进行修改的时候记录一下版本号,如果你修改到最后版本号跟后面的线程版本号不一样的话,那就说明你这存在Aba的问题。此时只需判断当前版本号和你修改之后的版本是否一至就可以解决。