Java中的多线程安全--volatile,Atomic系列类与锁

多线程情景分析

  1. 随机性

    默认情况下,CPU时间片抢占式调度,无法预测哪个线程会在什么时间拿到执行权

  2. 可见性

    共享数据的修改并不是所有线程都能看见。

    不同线程共享静态成员变量,对于方法区内静态区的变量,并不是直接取用。为了使用共享区数据,Java采用的是1+2的方法:

    1. 如果是第一次访问,在自己线程的栈上创建该变量的副本,只要不修改,就不会再重新获取

    2. 如果修改,就更新自己栈上副本的值,立刻同步到方法区中(共享区域),后续使用修改后的副本

    image-20210424102545770

    以上代码执行结果是无限循环。新线程虽有重新赋值,但是在等待过程中,主线程已经获取到了原值(0),并生成了副本,自己又没有修改,也就不会再重新去拿了,任你新线程如何修改,即便a已经被更新,对主线程而言,a仍旧是0。

  3. 有序性

    JVM翻译Java代码,对于没有上下文依赖的代码,可能会调整代码的顺序,此所谓指令重排。在单线程情况下,不会有影响,但是多线程时,就会影响到执行结果。但说到底,这一问题还是多线程的随机性造成的。

    image-20210424103639344

  4. 原子性

    image-20210424104358166

    以上代码中,a并不一定会被累计到20000,原因就在于t1和t2获取的a并不一定是最新的。何以解决?加上volatile可以吗?

    volatile

    volatile具体做了什么?其实就是改变了使用共享数据的逻辑,由第一次访问获取更新改为:只要使用共享变量,强制获取最新变量,并重写副本。另外的作用就是让JVM不要给涉及这个变量的语句指令重排。volatile意为变动的,不稳定的。正如其字面含义,被volatile修饰的变量,总是处于变化之中,每次获取都要用最新的值。

    但是,注意volatile只是改变了获取共享区数据的方式,但是并不涉及修改,也就是说获取之后还是照旧去修改本地副本并更新共享区数据,随机性问题并没有得以解决。我们真正需要做到的是让使用共享区数据的几个步骤变为一个整体,中间操作过程中,不要有其它线程插入。由此引入了原子类,即一批Atomic开头的Java类。

    原子类

    image-20210424110041090

    原子类累加时,发生了什么?

    image-20210424110232869

    可以看出,累加时先要获取主内存中的值,之后再反复确定获取的值与主内存中的值是否一致,不一致就继续获取,一致后执行累加,返回结果。在比较过程中,不会再有其它线程插入(可以理解为还是有一个悲观锁),确保比较过程不会出错。这就是所谓CAS,这是乐观锁的一种实现方式。

  1. 既然又有乐观锁,又有悲观锁,且乐观锁底层还是要依靠悲观锁的机制,为什么又要有两样东西呢?

    我们来考虑三个窗口买票的问题。三个窗口买票,不加处理,可能会卖重票与卖负票。其原因有三:多线程环境,数据共享,操作共享数据的过程中可能被其它线程抢到。 一二不可避免,能着手解决我呢提的部分只有三,即将操作过程隔离,一次只能有一个线程操作,如同建上围墙,只留一扇小门,一次只是从门放入一人。Lock接口的实现方式也是一样。只是synchronized跟接近于面向过程编程,而Lock接口则是面向对象编程,也就能实现一些更为精细化的操作,二者效率在JDK1.5后已无不同。

    上面已经提到了原子类,如果把票替换为原子类实现(乐观锁),能解决卖重票和负票的问题吗?答案是不能,原子类能保证原子性的范围,只限于修改,即执行getAndIncrement方法期间,操作逻辑的其它部分照样。由此可见,乐观锁与悲观锁的使用范围并不一样。试比较二者:

    1. 乐观锁

      只针对值的修改,只在修改处加校验,操作的其它大断逻辑它不管。

      适用于多查少改。如果改动很多,用乐观锁效率反而变低,因为CAS循环次数变多。

    2. 悲观锁

      针对打大段的上下文关联的逻辑,把大段逻辑变为原子性。

      适用于多改少查

    image-20210424111114383 

 参考资料:

https://www.bilibili.com/video/BV1sE411c7JC?p=8&spm_id_from=pageDriver

posted @ 2021-04-24 11:46  F君君  阅读(245)  评论(0编辑  收藏  举报