对volatile的理解--从JMM以及单例模式剖析

MyTest myTest = new MyTest();

//第一个线程

new Thread(() -> {

try {

System.out.println(Thread.currentThread().getName() + “\t come in”);

Thread.sleep(3000);

myTest.numTo60();

System.out.println(Thread.currentThread().getName() + “\t update value:” + myTest.num);

} catch (InterruptedException e) {

e.printStackTrace();

}

} ,“thread1”).start();;

//主线程判断num值

while (myTest.num == 0){

//如果myData的num一直为零,main线程一直在这里循环

}

System.out.println(Thread.currentThread().getName() + "\t mission is over, num value is " + myTest.num);

}

}

如上代码是没有保证可见性的,可见性存在于JMM当中即java内存模型当中的,可见性主要是指当一个线程改变其内部的工作内存当中的变量后,其他线程是否可以观察到,因为不同的线程件无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,因为此处没有添加volatile指令,导致其中thread1对num值变量进行更改时,main线程无法感知到num值发生更改,导致在while处无限循环,读不到新的num值,会发生死循环

de822c8e7f2f8642fb41f0f73702f36d.png

此时修改类中代码为

/**

* volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改

*/

public static class MyTest{

//类的内部成员变量num

public volatile int num = 0;

//numTo60 方法,让num值为60

public void numTo60(){

num = 60;

}

}

此时volatile就可以保证内存的可见性,此时运行代码就可以发现

4995a459d1755113f4452a35e9701d32.png

1.2 不保证原子性

原子性概念:不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败

类代码为:

//自定义的类

public static class MyTest {

//类的内部成员变量num

public volatile int num = 0;

public void numPlusPlus() {

num++;

}

}

主方法为

public static void main(String[] args) {

MyTest myTest = new MyTest();

/**

* 10个线程创建出来,每个线程执行2000次num++操作

* 我们知道,在字节码及底层,i++被抽象为三个操作

* 即先取值,再自加,再赋值操作

*/

for (int i = 1; i <= 10; i++) {

new Thread(() -> {

for (int j = 0; j < 2000; j++) {

myTest.numPlusPlus();

}

}, “Thread” + i).start();

}

//这里规定线程数大于2,一般有GC线程以及main主线程

while (Thread.activeCount() > 2) {

Thread.yield();

}

System.out.println(Thread.currentThread().getName() + "\t finally num value is " + myTest.num);

}

代码如上所示,如果volatile保证原子性,那么10个线程分别执行自加2000次操作,那么最终结果一定是20000,但是执行三次结果如下

//第一次

main  finally num value is 19003

//第二次

main  finally num value is 18694

//第三次

main  finally num value is 19552

可以发现,我们num的值每次都不相同,且最后的值都没有达到20000,这是为什么呢?

为什么会出现这种情况?

首先,我们要考虑到这种情况,假如线程A执行到第11行即myTest.numPlusPlus();方法时

线程进入方法执行numPlusPlus方法后,num的值不管是多少,线程A将num的值首先初始化为0(假如主存中num的值为0),之后num的值自增为1,之后线程A挂起,线程B此时也将主存中的num值读到自己的工作内存中值为0,之后num的值自增1,之后线程B挂起,线程A继续运行将num的值写回主存,但是因为volatile关键字保证可见性,但是在很短的时间内,线程B也将num的值写回主存,此时num的值就少加了一次,所以最后总数基本上少于20000

如何解决?

但是JUC有线程的原子类为AtomicInteger类,此时,将类代码更改为

public static class MyTest {

//类的内部成员变量num

public volatile int num = 0;

AtomicInteger atomicInteger = new AtomicInteger();

//numTo60 方法,让num值为60

public void numTo60() {

num = 60;

}

public void numPlusPlus() {

num++;

}

public void myAtomPlus(){

atomicInteger.getAndIncrement();

}

}

共同测试num和atomicInteger,此时执行主函数,三次结果为

//第一次

main  finally num value is 19217

main  finally atomicInteger value is 20000

//第二次

main  finally num value is 19605

main  finally atomicInteger value is 20000

//第三次

main  finally num value is 18614

main  finally atomicInteger value is 20000

我们发现volatile关键字并没有保证我们的变量的原子性,但是JUC内部的AtomicInteger类保证了我们变量相关的原子性,AtomicInteger底层用到了CAS。

1.3 禁止指令重排

有序性的概念:在计算机执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分以下三种

2a5e7b5a1e3533ee948dc51ad403fbdf.png

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排顺序是必须要考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性时无法确定的,结果无法预测

重排代码实例:声明变量:int a,b,x,y=0

e6d3789e41745f1f7e10efd3ffb0d154.png

如果编译器对这段程序代码执行重排优化后,可能出现如下情况:

15ffc01613fcc29e90865312728625b1.png

这个结果说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的

volatile实现禁止指令重排,从而避免了多线程环境下程序出现乱序执行的现象

内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,他的作用有两个:

  • 保证特定操作的执行顺序

  • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排优化。如果在之间插入一条Memory Barrier则会告诉编译器和CPU, 不管什么指令都不能和这条Memory Barrier指令重排顺序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读 取到这些数据的最新版本

71a2cb9b298476987096669210b0aaeb.png

2.JMM(java内存模型)

为什么提到JMM?JMM当中规定了可见性、原子性、以及有序性的问题,在多线程中只要保证了以上问题的正确性,那么基本上不会发生多线程当中存在数据安全问题

JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在,他描述的时一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:

  • 线程解锁前,必须把共享变量的值刷新回主内存

  • 线程加锁前,必须读取主内存的最新值到自己的工作内存

  • 加锁解锁时同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有的成为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是贡献内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先概要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存的变量副本拷贝,因此不同的线程件无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,期间要访问过程如下图:

b2c43ace4f3faba5d1ac7a779d1d23c2.png

JMM的三大特性
  • 可见性

  • 原子性

  • 有序性

所以JMM当中的2.1和2.3在volatile当中都有很好的体现,volatile关键字并不能保证多线程当中的原子性,但是volatile是轻量级的同步机制,不想synchronized锁一样粒度太大

3.你在那些地方用过volatile?结合实际谈论一下?

当普通单例模式在多线程情况下:

/**

* 普通单例模式

* */

public class SingletonDemo {

private static SingletonDemo instance = null;

private SingletonDemo() {

System.out.println(Thread.currentThread().getName() + “\t 构造方法 SingletonDemo()”);

}

public static SingletonDemo getInstance() {

if (instance == null) {

instance = new SingletonDemo();

}

return instance;

}

public static void main(String[] args) {

//构造方法只会被执行一次

// System.out.println(getInstance() == getInstance());

// System.out.println(getInstance() == getInstance());

// System.out.println(getInstance() == getInstance());
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

更多:Java进阶核心知识集

包含:JVM,JAVA集合,网络,JAVA多线程并发,JAVA基础,Spring原理,微服务,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存等等

image

高效学习视频

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

更多:Java进阶核心知识集

包含:JVM,JAVA集合,网络,JAVA多线程并发,JAVA基础,Spring原理,微服务,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存等等

[外链图片转存中…(img-fX9w0Naq-1713644927365)]

高效学习视频

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 11
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值