Android开发积累

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

2019-04-24  本文已影响41人  前行的乌龟

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

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


1 2 3

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


JVM 工作内存和主内存

JVM 把内存分割为:主内存 | 工作内存 2个部分:

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

JVM规范定义了线程对内存间交互操作:

是不是有点看的眼花缭乱啦,仔细看这些其实都是顺序执行的操作,很好理解,知道就可,同样这些操作有自己的特性:

多线程并发的核心其实就是对于资源的可见性和有序性的处理

通过上面这个例子,就明明白白带出了多线程我们关心什么,多个线程同时对相同资源的使用,只要我们的代码中类似上面要处理相同的资源,那么我们必须要采用合适的多线程测量,否则执行成啥样谁知道


并发添加 volatile 的表现

还是上面的代码,我们给 flag 加上 volatile

    @Volatile
    var flag: Boolean = false

然后我们看看运行情况:


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



并发 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 在多线程中异常危险


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

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


感想

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

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


参考资料:

上一篇下一篇

猜你喜欢

热点阅读