美文网首页原理收藏-技术篇
慕课网高并发实战(二)-线程安全性

慕课网高并发实战(二)-线程安全性

作者: habit_learning | 来源:发表于2018-04-06 17:00 被阅读153次

    .课程网站

    线程安全性

    原子性-Atomic包

    1、CAS(Compare and Swap)

        CAS:Compare and Swap的意思,比较并操作。很多的cpu直接支持CAS指令。CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS算法:unsafe.compareAndSwapInt(this, valueOffset, expect, update);  如果valueOffset位置(CPU内存)包含的值与expect值相同,则更新valueOffset位置的值为update,并返回true,否则不更新,返回false。

    看一下AtomicInteger.getAndIncrement()的源码:调用了Unsafe类的getAndAddInt(obj, valueOffset, 1)方法。

    getAndIncrement源码

    var1为当前调用对象,即AtomicInteger实例;

    var2为value属性在内存中的位置(volatile修饰的,保证多个线程访问都是与主内存一致的值);

    var4为需要加的值,这里为1;

    var5在1处:为CPU内存中当前value的值;在2处:为期望值;

        在多线程情况下,1->2之间,有可能其他线程修改了CPU内存中的value的值,导致value与预期的var5不一致(compareAndSwapInt()会再次去内存取最新value的值),于是无法执行update操作(var5+var4),需要重新获取当前CPU内存中的value值,继续走compareAndSwapInt()方法进行验证,直到value与预期的var5一致,才执行update操作。

    2、AtomicLong、LongAdder

        当线程竞争很激烈时,AtomicXXX的底层while判断条件中的CAS会连续多次返回false,这样就会造成无用的循环,循环中读取volatile变量的开销本来就是比较高的。因此,在高并发时,AtomicXXX并不是那么理想的计数方式,此时应该使用LongAdder。

        LongAdder是根据ConcurrentHashMap的设计原理-锁分段来实现的。它里面维护一组按需分配的技术单元,并发计数时,  不同的线程可以在不同的计数单元上进行计数,这样减少了多线程竞争,提高了并发效率。取值时,把多个计数单元的值求和就行了。

         LongAdder在统计的时候如果有并发更新,可能会导致结果有些误差。对于需要准确的数值的情况,比如序号生成,不应该使用LongAdder,还是应该采用全局唯一的AtomicLong。

    3.AtomicReference、AtomicReferenceFieldUpdater

    AtomicReference:用法同AtomicInteger一样,但是可以放各种对象。

    AtomicReference代码演示

    AtomicReferenceFieldUpdater:原子性的去更新某一个类的实例指定的某一个字段。

    AtomicReferenceFieldUpdater代码演示

    4、AtomicStampReference:CAS的ABA问题

        ABA问题:在CAS操作的时候,其他线程将变量的值A改成了B,接着由B改成了A,本线程使用期望值A与当前变量进行比较的时候,发现A变量没有变,于是CAS就将A值进行update操作,这个时候实际上A值已经被其他线程改变过,这与设计思想是不符合的。

        解决思路:每次变量更新的时候,把变量的版本号加一,这样只要变量被某一个线程修改过,该变量版本号就会发生递增操作,从而解决了ABA问题。 AtomicStampReference类底层就是这样设计

    5、AtomicBoolean(平时用的比较多)

    使用场景:用于在多线程情况下,保证某一段代码只被执行一次。

    AtomicBoolean代码演示-1 AtomicBoolean代码演示-2

    原子性-锁

    Synchronized:依赖JVM (主要依赖JVM实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程进行操作的)。

    Lock:依赖特殊的CPU指令。

    1、原子性-Synchronized

    1、修饰代码块:大括号括起来的代码,作用于调用的对象

    2、修饰方法:整个方法,作用于调用对象

    3、修饰静态方法:整个静态方法,作用于所有对象

    4、修饰类:括号括起来的部分,作用于所有对象

    1和2的作用效果是一致的;3和4的作用效果是一致的。

    2、原子性-对比

    synchronized:不可中断锁,适合竞争不激烈,可读性好。

    lock:可中断锁(调用unlock()),多样式同步,竞争激烈时能维持常态。

    Atomic:竞争激烈时能维持常态,比lock性能好;但是只能同步一个值。

    可见性

    导致共享变量在线程中不可见的原因:

    1、线程交叉执行

    2、重排序结合线程交叉执行

    3、共享变量更新后的值没有在工作内存与主内存间及时更新

    可见性-synchronized

    JMM关于Synchronized的两条规定:

    1、线程解锁前,必须把共享变量的最新值刷新到主内存。

    2、线程加锁时,将清空工作内存中的共享变量的值, 从而使用共享变量时需要从主内存中重新获取最新的值(注意,加锁与解锁是同一把锁)。

    可见性-volatile

    通过加入内存屏障和禁止重排序优化来实现。

    1、对volatile变量写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新到主内存。

    2、对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。

    volatile写指令 volatile读指令 自增执行步骤

    小结:由于volatile没有原子性,所以volatile不适用于基于当前值的操作,适用于标识操作。

    有序性

        Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

        Happens-before原则,先天有序性,即不需要任何额外的代码控制即可保证有序性,java内存模型一共列出了八种Happens-before规则,如果两个操作的次序不能从这八种规则中推倒出来,则不能保证有序性(本文就不列出8种规则了)。

    相关文章

      网友评论

        本文标题:慕课网高并发实战(二)-线程安全性

        本文链接:https://www.haomeiwen.com/subject/ozcshftx.html