问题
java内存模型带来的三大问题:
1.顺序性
指令重排
2.可见性
内存可见性
3.原子性
咋解决?
1、volatile
只能解决顺序性(禁止指令重排)和可见性。
2、synchronized和显式Lock类
都可以解决。
以上简单描述是volatile和synchronized要解决的问题,具体解释见下文。
volatile和synchronized的本质区别是什么?
volatile和synchronized的唯一区别是,volatile只确保内存可见性(一个线程修改了共享数据,其他线程都可以读最新数据——如果修改共享数据是原子操作,那么volatile就是线程安全的,但是大部分情况修改原子数据都不是原子操作,所以大部分情况volatile都不是线程安全的)。
synchronized也确保内存可见性——并且还确保不同线程互斥,即同一时间,只能有一个线程获取到锁,其他线程阻塞等待获取锁。
synchronzied能确保共享数据同一时间只有一个线程可以更新共享数据。
但是volatile不能,因为volatile只能确保单个读或写操作是原子,但是对一个共享数据的修改,往往很多时候,需要依赖共享数据的旧值,比如在旧值之上加1,即先读共享数据的旧值——然后加1——最后写,这里实际上就包含了3个操作,所以volatile不能保证真正的同步。
volatile应用场景?
那volatitle应用场景是啥样的?对共享数据修改的时候,不依赖共享数据(本质是不使用共享数据的旧值),并且右边的变量也要确保线程安全(本质是因为右边的变量相当于是一个读的操作,那么更新共享数据的时候,就包含了2个操作:1、先读右边变量的值 2、再更新共享数据。更新操作是原子,但是多了一个读右边变量的操作,就不是原子了,因为现在包含了2个原子操作)。
参考:titanwolf.org/Network/Art… //这篇文章的作者就是写java并发编程实战的
示例1
示例2
AtomicInteger的set和get是线程安全的,因为set和get方法只有一步赋值/读取操作,而且操作的数据是int(32位),并且又是vovatile(即内存可见性),所以AtomicInteger的set和get是线程安全的。
但是,AtomicLong的set和get方法根本不是原子的,因为AtomicLong的数据是long(64位),set和get方法是对64位数据进行赋值/读取,所以不是原子的,所以也不是线程安全的。
jdk api也没有说set和get方法是线程安全的,只说compareAndSet(比较然后设置)、getAndIncrement/getAndDecrement(自增/自减)是原子操作,即是线程安全的。
set方法写数据,线程不安全。
compareAndSet才是原子写数据,线程安全。
单个读或写是原子操作
为什么单个读或写是原子操作?这个是java规范保证的。
准确的说,非long/double数据,才是原子。为什么?因为long/double是64位,而java规范只保证32位才是原子操作。
volatile的作用?
使得共享数据被一个线程更新的时候,其他线程可以立即读到最新值(即所谓的可见性)。即,没加volatile,可能读不到最新值;加了,就确保可以读到最新值。
syncronized的作用?
确保互斥,即同一时间,只能有一个线程更新共享数据。具体到代码层面就是,比如锁定一个更新共享数据的方法或者代码块,同一时间,只有一个线程可以执行这个方法或者代码块,这样就确保了其他线程获取不到锁,也就不能执行方法或者代码块,也就不能更新共享数据。
可见性呢?synchronized也可以确保可见性,可见性是指,线程A修改了共享数据,线程B可以看到最新数据。这似乎是一句废话,事实也是一句废话——这里主要是涉及到java内存模型的问题。
java内存模型
对Java内存模型的理解,以及其在并发中的应用。
Java内存模型的主要目标: 定义程序中各个变量的访问规则。
Java线程之间的通信由Java内存模型(本文简称为JMM)控制。
所有变量的存储都在主内存,每条线程还都有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作必须在工作内存完成,而不能直接读取主内存中的变量。不同的线程无法直接访问对方工作内存中的变量,线程间变量的传递均需要通过主内存来完成。
线程间通信:
-
首先,线程A把本地内存A中更新过的共享变量刷新(即写)到主内存中去。 //cpu1(线程1)——缓存1(线程工作内存1)——主内存
-
然后,线程B到主内存中去读取(即读)线程A之前已更新过的共享变量。//cpu2(线程2)——缓存2(线程工作内存2)——主内存
线程只能和线程工作内存通信,线程工作内存然后又和主内存通信。
不同线程的工作内存,不能互相通信,必须通过主内存。比如,线程1写数据到线程工作内存1,然后线程工作内存写数据到主内存;线程工作内存2从主内存读数据,然后线程2就可以从线程工作内存2读数据了。
架构图
cpu维度-架构图
线程维度-架构图
说明:线程维度的架构图可能不好理解,因为为什么会有这个架构图?为什么是这样子?本质是因为cpu维度的架构图。
而cpu维度的架构图的本质是,现在都是多cpu,每个cpu都有自己的高速缓存,为什么每个cpu都要有自己的高速缓存?不是已经有了内存(即架构图里的主内存)吗?因为高速缓存快啊,高速缓存比内存快,而且高速内存比内存要更小,内存越小越快。
各种存储设备的访问速度如下:
寄存器的速度最快,可以在一个时钟周期内访问,
其次是高速缓存,可以在几个时钟周期内访问,
普通内存可以在几十个或几百个时钟周期内访问。
深入理解计算机系统
cpu缓存的架构图
存储的层次结构
苹果电脑macbook pro 2018
一个核可以跑一个线程,所以一个核相当于一个线程,多个核同时跑多个线程,就可能修改了共享数据,但是由于每个核都有自己的独立缓存,所以数据可能不一致————这就是内存可见性的本质。
实际上,苹果电脑是有12个逻辑cpu,因为使用了超线程技术,即一个核可以同时跑2个线程,就相当于12个逻辑cpu了。
几年前,电脑还都是双核的,广告里老是播放intel的双核处理器。现在电脑基本上都是好几核的,一般4核。好一点的,比如苹果电脑,6核。
以后电脑的趋势,就是10几核。
内存可见性
内存模型这个东西很虚,也没什么意义,主要是不了解内存模型,就不能理解线程的内存可见性——因为不知道为什么,线程1修改了共享数据,但是线程2却不一定能读到共享数据的最新值。原因就在于内存模型,即上面的cpu维度的架构图里提到的,在cpu/线程和内存之间,还多了一个中介内存(即高速缓存/线程工作内存)。
多了一个中介,就多了一个数据不一致的问题。
那怎么解决数据不一致的问题?synchronized或者volatile,都能保证线程1修改共享数据之后,线程2就可以读到共享数据的最新值。
而synchronized和volatile的唯一区别就是,synchronized还多了一个功能,就是互斥的功能,即同一时间,只有一个线程能获取对象的锁,其他线程只能等待该线程释放锁——本质是同一时间,只能有一个线程执行方法或者代码块:即只能有一个线程,更新共享数据。