JMM、Volatile及单例模式解析

前言

volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块或方法 和 volatile 变量,相比于synchronized(重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

一、并发编程三要素

1.原子性

定义:原子性,即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。

java原子性包括:

  • 基本类型的读取和赋值操作,且赋值必须是值赋给变量,如 a=2。变量之间的相互赋值不是原子性操作,不包括(long,double)变量;
  • 所有引用reference的赋值操作;
  • java.concurrent.Atomic.* 包中所有类的一切操作。
2.可见性

定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后,该变量会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。

当然,synchronizeLock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3.有序性

定义:即程序执行的顺序按照代码的先后顺序执行

Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主内存同步延迟”现象。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。

另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

二、锁的互斥和可见性

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。

互斥,即一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据。抢锁失败后只要锁的持有状态一直没有改变,那就让出 CPU 给别的线程先执行。

可见性,要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即当一个线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

使用volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • 对变量的写操作不依赖于当前值;
  • 该变量没有包含在具有其他变量的不变式中。

因为volatile本身保证了可见性与有序性,还需变量的操作满足原子性,才能保证并发安全性。实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上就是保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

三、CPU缓存架构

在了解JMM内存模型之前,先了解一下CPU缓存架构。

CPU是计算机的心脏,所有运算和程序最终都要由它来执行。

主内存RAM是数据存在的地方,CPU和主内存之间有好几级缓存,因为即使直接访问主内存相对来说也是非常慢的。

如果对一块数据做相同的运算多次,那么在执行运算的时候把它加载到离CPU很近的地方就有意义了,比如一个循环计数,你不想每次循环都到主内存中去取这个数据来增长它吧。

在这里插入图片描述
越靠近CPU的缓存越快也越小。

所以L1缓存很小但很快,并且紧靠着在使用它的CPU内核。L2大一些,但也慢一些,并且仍然只能被一个单独的CPU核使用。L3在现代多核机器中更普遍,仍然更大,更慢,并且被单个插槽上的所有CPU核共享。最后,主内存保存着程序运行的所有数据,它更大,更慢,由全部插槽上的所有CPU核共享。

当CPU执行运算的时候,它先去L1查找所需的数据,再去L2,然后L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。

走得越远,运算耗费的时间就越长。所以如果进行一些很频繁的运算,要确保数据在L1缓存中。

四、Java内存模型(JMM)以及共享变量的可见性

JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中(堆中),每个线程都有一个私有的本地内存(Local Memory)(栈中),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

1. JMM内存模型

请添加图片描述

对共享变量进行修改,不是直接操作该变量,而是通过将共享变量复制到各个线程的工作内存中,操作完成后再刷回主内存当中。不加volatile修饰变量的话,不是主线程,其他线程修改值后,不会影响主线程变量的值(不会修改)

<
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值