![](https://img.haomeiwen.com/i1785445/582590ddfa115ebf.jpg)
今天回味 volatile 时看到了别人的一个 Demo:
class VolatileDemo() {
var flag: Boolean = false
fun read() {
while (!flag) {
Log.d("AA", "读取任务ing...")
// Thread.sleep(100)
}
Log.d("AA", "读取任务结速")
}
fun write() {
flag = true
Log.d("AA", "写入任务完成")
}
}
var volatileDemo = VolatileDemo()
var thread1 = Thread(Runnable { volatileDemo.write() })
var thread2 = Thread(Runnable { volatileDemo.read() })
//我们让线程2的读操作先执行
thread2.start()
//睡30毫秒,为了保证线程2比线程1先执行
Thread.sleep(30)
//再让线程1的写操作执行
thread1.start()
并发没有 volatile 的表现
读取和写入操作中的 Flag 没用 volatile 标记,这时大家猜猜线程会怎么运行,这个例子当初有人 用来解释 volatile 的内存可见性,说 thread2 栈帧中的内存副本不会同步更新,即便 thread1 修改了 flag 的值,thread2 也会一直卡在这个循环里出不来。但是...重点是但是,这是不对的,thread2 还是能结束的,只是每次 thread2 每次表现都不一样,谁也不知道 thread2 在刷新 flag 数据之前会运行多少次
我们多运行几次,看看打印情况
![](https://img.haomeiwen.com/i1785445/5ce3b732ca835947.png)
![](https://img.haomeiwen.com/i1785445/4a77444efbe529ae.png)
![](https://img.haomeiwen.com/i1785445/cba0b4ef49bba23b.png)
结果完全超出我们认知啊,这运行起来完全没有规律可言,明明我们没用 volatile 标记 flag ,但是为什么图1、图3 这么像 volatile 啊,但是图2缺不是,这怎么理解,这就要盘盘 JVM 工作内存和主内存了
JVM 工作内存和主内存
![](https://img.haomeiwen.com/i1785445/8a95f26df85f142a.png)
JVM 把内存分割为:主内存 | 工作内存 2个部分:
每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成
JVM规范定义了线程对内存间交互操作:
- Lock(锁定) - 作用于主内存中的变量,把一个变量标识为一条线程独占的状态。
- Read(读取) - 作用于主内存中的变量,把一个变量的值从主内存传输到线程的工作内存中。
- Load(加载) - 作用于工作内存中的变量,把read操作从主内存中得到的变量的值放入工作内存的变量副本中
- Use(使用) - 作用于工作内存中的变量,把工作内存中一个变量的值传递给执行引擎。
- Assign(赋值) - 作用于工作内存中的变量,把一个从执行引擎接收到的值赋值给工作内存中的变量。
- Store(存储) - 作用于工作内存中的变量,把工作内存中的一个变量的值传送到主内存中。
- Write(写入) - 作用于主内存中的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。
- Unlock(解锁) - 作用于主内存中的变量,把一个处于锁定状态的变量释放出来,之后可被其它线程锁定。
是不是有点看的眼花缭乱啦,仔细看这些其实都是顺序执行的操作,很好理解,知道就可,同样这些操作有自己的特性:
- read - load,store - write 都是成对进行的,不允许单一出现使用
- 不允许线程丢弃它最近的一个 assign 操作,即变量在工作内存被更改后必须同步改更改回主内存
- 工作内存中的变量在没有执行过 assign 操作时,不允许无意义的同步回主内存
多线程并发的核心其实就是对于资源的可见性和有序性的处理
- 可见性 - 对于可见性来说,什么时候把线程工作内存中的变量副本同步到主内存中完全是 JVM 自己实现系统决定的,我找了好多资料也没有明确说明的,更具上面例子的测试,我发现有时候对数据的修改会马上同步到主内存,有时候要等到线程上下文切换时才会更新数据。另外再说一点,使用 volatile 同样也会由工作内存的问题,区别是工作内存中的修改会马上立即同步到主内存
- 有序性 - 有序性这个大家应该都门清,就是严格保证代码按照我们的逻辑执行,上面的例子就是个反面典型,执行成啥样我们完全控制不了
通过上面这个例子,就明明白白带出了多线程我们关心什么,多个线程同时对相同资源的使用,只要我们的代码中类似上面要处理相同的资源,那么我们必须要采用合适的多线程测量,否则执行成啥样谁知道
并发添加 volatile 的表现
还是上面的代码,我们给 flag 加上 volatile
@Volatile
var flag: Boolean = false
然后我们看看运行情况:
![](https://img.haomeiwen.com/i1785445/9b2492edc29e6a9b.png)
不管点几次都是读取先完事,然后再试写入完事,这样的确是保证了内存可见了,我们在任何地方修改一个 Volatile 的变量,所有改变量的副本都会立马相应,可以看到影响的速度是很快的,快的写入都来不急执行下面的任务,读取那边就完成同步了
但是从结果上看光是有 Volatile 还是不行的,逻辑上读取操作结束应该在写入完成之后执行的,这样看来 Volatile 并不能解决根本问题,还是得 Synchronized
很多人都说用 Volatile 做多线程同步必须小心再小心,通过这一个小小的例子就很明显了,Volatile 的缺陷太大,无法保证连续性和逻辑性,Volatile 最适合的场景就是赋值操作了,典型的就是单例了对吧,这个大家都知道
并发添加 Synchronized 的表现
那么写到这就完事了吗,还没有,最常用的多线程同步手段 Synchronized 我们还没用呢,既然上面 volatile 保证不了连续性逻辑性,那么我们来看看 Synchronized ,我们给写入和读取方法都改成 Synchronized 的
class VolatileDemo(var index: Int) {
var flag: Boolean = false
@Synchronized
fun read() {
while (!flag) {
Log.d("AA", "读取任务ing... - 第:$index 点击")
// Thread.sleep(100)
}
Log.d("AA", "读取任务结速 - 第:$index 点击")
}
@Synchronized
fun write() {
flag = true
Log.d("AA", "写入任务完成 - 第:$index 点击")
}
}
但是结果呢,thread2 真的卡在这里了,thread2 拿到锁一直运行不释放锁,thread1 怎么由机会执行呢,就会想下面 log 输出一样,一直跑停不下来
![](https://img.haomeiwen.com/i1785445/03bdfa647513ec6f.png)
并发 volatile + Synchronized 的表现
我们继续修改代码
class VolatileDemo(var index: Int) {
@Volatile
var flag: Boolean = false
@Synchronized
fun read() {
while (!flag) {
Log.d("AA", "读取任务ing... - 第:$index 点击")
// Thread.sleep(100)
}
Log.d("AA", "读取任务结速 - 第:$index 点击")
}
@Synchronized
fun write() {
flag = true
Log.d("AA", "写入任务完成 - 第:$index 点击")
}
}
是不是有人对此很期待啊,肯定有人听说过多线程使用 volatile + Synchronized 来做,但是结果吧和上面单独使用 Synchronized 一样,thread2 一直运行,thread1 没有执行的机会,可见多线程设计的复杂性,你这边的逻辑说不准就会这样。不要迷信网上有人说的 volatile + Synchronized 万能论调,存扯淡
那我们应该怎么办,显然这种单单依靠 flag 在多线程中异常危险
- 常规方式 - 我们可以放弃这个 flag 标记,完全使用 Synchronized 来实现同步,但是 Synchronized 由局限性,Synchronized 修饰的是整个方法,只能同步整个方法的执行,而不能在方法执行的过程中进行操作
- 自由加锁 - 若是我们需要在方法中根据情况不筒进行不同的同步操作,那么就剩下自己加锁这种选择了,这样可以实现更精细的操作
并发 ReentrantLock+ Condition的表现
没啥说的直接看代码
class VolatileDemo {
@Volatile
var flag: Boolean = false
var reentrantLock = ReentrantLock()
var condition = reentrantLock.newCondition()
fun read() {
try {
reentrantLock.lock()
Log.d("AA", "开始读取任务")
if (!flag) {
Log.d("AA", "没有数据,进入待机状态,释放锁")
condition.await()
Log.d("AA", "没有数据,被唤醒再进入")
}
Log.d("AA", "读取任务结速")
condition.signalAll()
} finally {
reentrantLock.unlock()
}
}
fun write() {
try {
reentrantLock.lock()
flag = true
Log.d("AA", "写入任务完成")
condition.signalAll()
} finally {
reentrantLock.unlock()
}
}
}
var volatileDemo = VolatileDemo()
var thread1 = Thread(Runnable { volatileDemo.write() })
var thread2 = Thread(Runnable { volatileDemo.read() })
//我们让线程2的读操作先执行
thread2.start()
//睡1毫秒,为了保证线程2比线程1先执行
Thread.sleep(30)
//再让线程1的写操作执行
thread1.start()
![](https://img.haomeiwen.com/i1785445/b79f90b031b3df65.png)
这里我们还是基于 flag 标记进行逻辑操作,所以 flag 还是要设计成 Volatile 的,然后我们自己加锁,自己阻塞,自己唤醒,阻塞的代码在被唤醒的地方继续执行,这样整个逻辑我们恩那个完全按照自己的思路去做
感想
volatile 好久之前就看过了,这次精研多线程时又看了看当初的文章,于是又看到了这个小例子,看过之后马上反应过来由问题,左想不对,右也不对,谁说线程有自己的工作内存,核心标记也不是 volatile 可见的,但是 Thread2 是循环不挺的执行,不可能内存一直不刷新的,只是执行时间长短的问题,索性我把这个例子好号走走得了
然后连带着想了很多问题,比如线程工作内存何时同步到主内存,多线程的几种手段都是为了达到什么目的,意义,优势,缺陷?我是挨个试了个遍,还真是实践见真章,自己掠过这么一遍之后感觉多线程的手段在脑海里彻底清晰起来,写文章的意义也是在这里
网友评论