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

.课程网站

线程安全性

原子性-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 有一个根据当前并发状况动态改变的Cell 数组,Cell 对象里面有一个 long类型的 value 用来存储值; 开始没有并发争用的时候或者是 cells 数组正在初始化的时候,会使用 cas 来将值累加到成员变量的 base 上,在并发争用的情况下,LongAdder 会初始化 cells 数组,在 Cell 数组中选定一个 Cell 加锁,数组有多少个 cell,就允许同时有多少线程进行修改,最后将数组中每个 Cell 中的 value 相加,再加上 base 的值,就是最终的值;cell 数组还能根据当前线程争用情况进行扩容,初始长度为2,每次扩容会增长一倍,直到扩容到大于等于 cpu 数量就不再扩容,这也就是为什么 LongAdder 比 cas 和 AtomicLong 效率要高的原因,后面两者都是 volatile+cas 实现的,他们的竞争维度是1,LongAdder的竞争维度为“Cell个数+1”,为什么要+1?因为它还有一个 base,如果竞争不到锁还会尝试将数值加到 base 上; 

     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种规则了)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 定义 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要额外...
    景行lau阅读 717评论 1 0
  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,788评论 0 11
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,418评论 11 349
  • 我觉得应该站在对方的角度去考虑问题, 站在对方的角度就是把自己当做他,用他的思维,他的角度,他的身份,他的理解去想...
    梦夕梦阅读 668评论 2 3
  • 昨天晚上做了一个梦,梦见了
    岚泽阅读 242评论 0 0