android 多线程 — 从一个小例子再次品位多线程

《android 多线程 — 从一个小例子再次品位多线程》

今天回味 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 数据之前会运行多少次

我们多运行几次,看看打印情况

《android 多线程 — 从一个小例子再次品位多线程》 1
《android 多线程 — 从一个小例子再次品位多线程》 2
《android 多线程 — 从一个小例子再次品位多线程》 3

结果完全超出我们认知啊,这运行起来完全没有规律可言,明明我们没用 volatile 标记 flag ,但是为什么图1、图3 这么像 volatile 啊,但是图2缺不是,这怎么理解,这就要盘盘 JVM 工作内存和主内存了

JVM 工作内存和主内存

《android 多线程 — 从一个小例子再次品位多线程》

JVM 把内存分割为:主内存 | 工作内存 2个部分:
  • 主内存 – 堆内存和本地方法区
  • 工作内存 – 每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)

每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成

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

然后我们看看运行情况:

《android 多线程 — 从一个小例子再次品位多线程》

不管点几次都是读取先完事,然后再试写入完事,这样的确是保证了内存可见了,我们在任何地方修改一个 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 输出一样,一直跑停不下来

《android 多线程 — 从一个小例子再次品位多线程》

并发 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()

《android 多线程 — 从一个小例子再次品位多线程》

这里我们还是基于 flag 标记进行逻辑操作,所以 flag 还是要设计成 Volatile 的,然后我们自己加锁,自己阻塞,自己唤醒,阻塞的代码在被唤醒的地方继续执行,这样整个逻辑我们恩那个完全按照自己的思路去做

感想

volatile 好久之前就看过了,这次精研多线程时又看了看当初的文章,于是又看到了这个小例子,看过之后马上反应过来由问题,左想不对,右也不对,谁说线程有自己的工作内存,核心标记也不是 volatile 可见的,但是 Thread2 是循环不挺的执行,不可能内存一直不刷新的,只是执行时间长短的问题,索性我把这个例子好号走走得了

然后连带着想了很多问题,比如线程工作内存何时同步到主内存,多线程的几种手段都是为了达到什么目的,意义,优势,缺陷?我是挨个试了个遍,还真是实践见真章,自己掠过这么一遍之后感觉多线程的手段在脑海里彻底清晰起来,写文章的意义也是在这里

参考资料:

    原文作者:前行的乌龟
    原文地址: https://www.jianshu.com/p/f1a24c4b8d4a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞