1.谈谈你对volatile的理解
volatile是Java虚拟机提供的轻量级的同步机制[就是乞丐版的synchonized]
保证可见性,不保证原子性,静止指令重排 [能保证JMM两个]
2.JMM(JAVA 内存模型)
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,(12生肖里的龙并不存在)它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.
JMM关于同步规定:
1.线程解锁前,必须把共享变量的值刷新回主内存
2.线程加锁前,必须读取主内存的最新值到自己的工作内存
3.加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:
只要有一个线程修改了主内存中的变量值,且把它写回到了主内存,就一定要通知其它线程,俗称可见性
2.1可见性
通过前面对JMM的介绍,我们知道
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的.
这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题.
2.2原子性 number++在多线程下是非线程安全的,如何不加synchronized解决?
why不能保证原子性?(方法加了synchonized可以解决,但是杀鸡焉用宰牛刀)
主内存空间的值为0,一个线程number++,没有synchonized,线程可以哄抢
虽然有volatile通知修改,但是太快了,正要通知就被挂起了,然后被其它线程的数据覆盖了,出现了丢失数据
2.3VolatileDemo代码演示可见性+原子性代码
class Mydata{ //MyData.java==>MyData.class==>JAVA字节码
// int number=0;
volatile int number=0;
public void addTo60(){
this.number=60;
}
//请注意,此时number是加了volatile修饰的
public void addPlusPlus(){
number++;//被分成了3个指令 执行getfield拿到了原始number;执行iadd进行加1操作;执行putfield写把累加后的值写回
}
AtomicInteger atomicInteger=new AtomicInteger();
public void addMyAutomic(){
atomicInteger.getAndIncrement();
}
}
/**
* 验证volatile的可见性
* 1.1假如int number=0;number变量之前根本没有添加volatile关键字修饰,没有可见性
* 1.2假如volatile int number=0,有可见性
*
* 验证volatile不保证原子性
* 2.1原子性指的是什么意思?不可分割,完整性,也即某个线程正在做某个业务时,中间不可以被加塞或者分割,需要整体完整
* 要么同时成功,要么同时失败.课堂签到,张三写了张字,被李四抢走,在给张三写了三
* 我的操作不应该被打断
* 2.2volatile不保证原子性
*
* 2.3why不能保证原子性?(方法加了synchonized可以解决,但是杀鸡焉用宰牛刀)
* 主内存空间的值为0,一个线程number++,没有synchonized,线程可以哄抢
* 虽然有volatile通知修改,但是太快了,正要通知就被挂起了,然后被其它线程的数据覆盖了,出现了丢失数据
* 2.4如何解决原子性?
* 2.4.1加sync-->杀鸡别用宰牛刀
* 2.4.2使用JUC下的AtomicInteger
*/
public class VolatileDemo {
public static void main(String[] args) {
volatileAutomic();
}
//加了volatile关键字不保证原子性
private static void volatileAutomic() {
Mydata mydata=new Mydata();
for(int i=0;i<20;i++){
new Thread(()->{
for(int j=0;j<1000;j++){
mydata.addPlusPlus();
mydata.addMyAutomic();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程计算完成后,再用main线程取得最终的计算结果
while (Thread.activeCount()>2){
Thread.yield();//礼让线程
}
//只要不是20000,volatile就是不保证原子性
System.out.println("计算结果 int类型"+mydata.number);
//保证原子性
System.out.println("计算结果 atomicInteger类型"+mydata.atomicInteger);
}
//volatile可以保证可见性,及时通知其它线程,主物理内存的值已经被修改
private static void seeokByVolatile() {
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();}
mydata.addTo60();
System.out.println(Thread.currentThread().getName()+"\t update"+mydata.number);
},"AAA").start();
//如果一直等待,说明main线程没有感知到值的变化,不会输出misson is over
while (mydata.number==0){
//main线程就一直在这里等待循环,直到number值不在等于0
}
System.out.println(Thread.currentThread().getName()+"\t misson is over"+mydata.number);
}
}
2.4有序性
计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下3种
源代码->编译器优化的重排->指令并行的重排->内存系统的重排->最终执行的指令
public void mySort(){
int x=11;//语句1 int y=12;//语句2 x=x+5;//语句3 y=x*x;//语句4 } 1234 2134 1324 问题: 请问语句4 可以重排后变成第一条码? 存在数据的依赖性 没办法排到第一个 |
|
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致.
处理器在进行重新排序是必须要考虑指令之间的数据依赖性[先有你爹才能有你]
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测
加了volatile就是禁止指令重排
3.你在哪些地方用到过volatile?
3.1单例模式DCL代码
public class SingletoDemo {
private static SingletoDemo instance=null;
private SingletoDemo(){
System.out.println(Thread.currentThread().getName()+"我是构造方法");
}
public static SingletoDemo getInstance(){
if(instance==null){
instance=new SingletoDemo();
}
return instance;
}
public static void main(String[] args) {
System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());
System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());
System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());
}
}
3.2单例模式volatile分析
public class SingletoDemo {
private static volatile SingletoDemo instance=null;
private SingletoDemo(){
System.out.println(Thread.currentThread().getName()+"我是构造方法");
}
//DCL(Double Check Lock 双端检锁机制,比如上厕所,上厕所之前看一下有没有人,进厕所把门锁上,再看看有没有人
public static SingletoDemo getInstance(){
if(instance==null){
synchronized (SingletoDemo.class){
if(instance==null){
instance=new SingletoDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//单线程(main线程的操作动作)
/* System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());
System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());
System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());*/
//单例模式在多线程环境下可能存在安全问题
for(int i=1;i<=10;i++){
new Thread(()->{
SingletoDemo.getInstance();
},String.valueOf(i)).start();
}
//1.解决一:在getInstance方法上添加sync,但是太重了
//2.解决二:DCL模式双重检测,可能出现指令重排,所以要加volatile
}
}
DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排
原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化.
instance=new SingletonDem(); 可以分为以下步骤(伪代码)
memory=allocate();//1.分配对象内存空间
instance(memory);//2.初始化对象
instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null
步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的.
memory=allocate();//1.分配对象内存空间
instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.
instance(memory);//2.初始化对象
但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题.
1.解决一:在getInstance方法上添加sync,但是太重了
2.解决二:DCL模式双重检测,可能出现指令重排,所以要加volatile
4.CAS是什么
//this指的是当前对象
unsafe类是什么? |
![]() |
1.Unsafe 是CAS的核心类 由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个后面,基于该类可以直接操作特额定的内存数据.UnSafe类在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作的助兴依赖于UNSafe类的方法. 3.变量value和volatile修饰,保证了多线程之间的可见性. |
CAS是什么? CAS的全称为Compare-And-Swap ,它是一条CPU并发原语. |
|
this 就是var1,valueoffset 就是var2,var5就是获得var1变量的var2位置的值, 进入while循环,看var1变量的var2位置的值是否等于var5的值,如果相等,就改变值为var5+var4 如果修改成功,跳出循环
var1 AtomicInteger对象本身。 |
没有加锁,既解决了一致性,又提高了并发性 |
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上): 1.AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存. 2.线程A通过getIntVolatile(var1,var2) 拿到value值3,这是线程A被挂起. 3.线程B也通过getIntVolatile(var1,var2) 拿到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存中的值也是3 成功修改内存的值为4 线程B打完收工 一切OK. 4.这是线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数字4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了. 5.线程A重新获取value值,因为变量value是volatile修饰,所以其他线程对他的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功. |
CAS (CompareAndSwap) 比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作, 否则继续比较直到主内存和工作内存中的值一致为止. CAS应用 CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。 当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。 |
CAS缺点: 1.循环时间长开销很大 2.只能保证一个共享变量的原子操作.对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性 3.引出来ABA问题? |
Automic CAS-->Unsafe-->CAS底层思想-->ABA-->原子引用更新-->如何规避ABA问题 |
5.ABA问题
原子类Automic的ABA问题谈谈?原子更新引用知道吗? |
一句话:狸猫换太子 |
CAS会导致ABA问题 CAS算法实现一个重要前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差里会导致数据地变化 比如说一个线程one从内存V中取出A,这时候另一个线程two也从内存中取出A,并且线程B进行了一系列操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作时发现内存中仍然是A,然后线程one操作成功. 尽管线程one的CAS操作成功,但是并不代表这个过程是没有问题的 |
如何解决ABA问题? --> 原子引用-->时间戳原子引用 |
AtomicReference类是用来解决自己定义的类的CAS |
ABA问题的解决-->AtomicStampedReference |
/**
* ABA问题的解决 AtomicStampedReference
*/
public class ABADemo {
static AtomicReference<Integer> atomicReference =new AtomicReference<>(100);
static AtomicStampedReference 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);
},"t1").start();
new Thread(()->{
//暂停1秒钟t2线程,保证上面的t1线程完成了ABA操作
try{
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e){e.printStackTrace();}
System.out.println(atomicReference.compareAndSet(100,2020)+"\t"+atomicReference.get());
},"t2").start();
try{TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e){e.printStackTrace();}
System.out.println("***********以下是ABA问题的解决****************");
new Thread(()->{
int stamp=atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第一次版本号"+stamp);
//暂停1秒钟t3线程
try{TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e){e.printStackTrace();}
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t第二次版本号"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t第三次版本号"+atomicStampedReference.getStamp());
},"t3").start();
new Thread(()->{
int stamp=atomicStampedReference.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=atomicStampedReference.compareAndSet(100,2020,stamp,stamp+1);
System.out.println(Thread.currentThread().getName()+"\t修改成功否"+result+&