JAVA高并发-线程安全性(原子性、可见性、有序性)

一、原子性

提供了互斥访问,同一时刻只能有一个线程对它进行操作。
保证原子性的操作:
1.Atomic

    1)Atomic:CAS(Unsafe.compareAndSwapInt)

    Atomic包下提供的类利用CAS保证操作的原子性,如和int/integer相对应的AtomicInteger类提供的incrementAndGet()函数实现一个整数自增的操作count++,通过查看源码发现AtomicInteger下的自增操作incrementAndGet(),使用了unsefe的类提供的unsefe.getAndAddInt()方法。unfefe.getAndAddInt()是通过do-while语句做主体函数,其中使用compareAndSwapInt(var1, var2, var5, var5+var4)进行实现。
JAVA高并发-线程安全性(原子性、可见性、有序性)
    compareAndSwapInt()方法是一个native方法,代表这是一个java底层的方法。
    该方法的实现原理:
    首先看getAndAddInt()函数的参数列表Object var1是当前传入的对象,var2是当前对象的值,var4是要增加的值(自增:var4为1),方法中var5为获取当前对象在主存中的值;
    然后看compareAndSwapInt()函数的参数列表,var为当前对象,var2为当前传入的值,var5为当前对象在主存中的值,var5+var4是“底层的值” 和 “要增加的值” 的和;
    如果当前对象传入的值var2和底层获取的值var5相等,则将当前对象的值更新为要获取的值也就是var5+var4;否则重新从底层取值var5,重新从var1取值var2,进行判断。通过一直这样循环,保证当我们期望的值和底层的值相同时,才可以操作数据将底层的值刷新为新的值。
JAVA高并发-线程安全性(原子性、可见性、有序性)
JAVA高并发-线程安全性(原子性、可见性、有序性)
    compareAndSwap就是CAS的核心,上面介绍的是compareAndSwapInt(),相应的还有compareAndSwapLong()等针对其他类型变量的方法,AtomicBoolean、AtomicLong等针对其他类型变量的类。
    2)LongAdder、AtomicReference、AtomicReferenceFieldUpdater
   1)jdk8中新增了一个LongAdder类和AtomicLong类非常类似,在Longadder类中将AtomicLong中的incrementAndGet()方法改为了increment()方法。
    为什么新增一个LongAdder类的,是因为在AtomicLong类的CAS算法是在一个死循环内不断尝试修改目标的值,当竞争激烈时修改失败的几率很高也就导致了一直在循环这些原子性操作,性能会受到影响。LongAdder类的核心思想是将数据的热点数据分离。
    比如将AtomicLong的内部核心数据Value分离成一个数组,每个线程访问时通过hash()等算法映射到其中一个元素进行运算,最后的结果为这些运算结果的求和累加,当前value的实际值也有Value分离出的所有元素累计合成。这样做相当与将AtomicLong的单点更新压力分散到各个节点上,在高并发是通过分散提高了性能。
    但LongAdder也有其缺点,当统计时有并发更新可能会导致统计出的数据有误差,比如***生成等需要准确且全局唯一数据时,还是应该使用AtomicLong
   (2)原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子的更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了AtomicReference:原子更新引用类型、AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
    AtomicReference
JAVA高并发-线程安全性(原子性、可见性、有序性)

   AtomicReferenceFieldUpdater

JAVA高并发-线程安全性(原子性、可见性、有序性)

    3)AtomicStampReference:CAS的ABA问题

    什么是ABA问题:在多线程运行环境下CAS操作时,其他线程将变量A改成了B又改成了A,当本线程用期望值和该变量进行比较时,发现A变量的值没有变就进行了数据修改操作,这样是与设计初衷不符的。
    解决方法是每次变量更新时,会赋给变量一个版本号并加1递增,这样就避免了ABA问题。
     AtomicStampReference类提供了解决这一问题的方法,核心方法是compareAndSet()方法,方法中多了一个对stamp的比较,就是对变量版本号的比较,stamp的值也是在每次变量进行更新时进行维护。JAVA高并发-线程安全性(原子性、可见性、有序性)

    4)AtomicLongArray

    AtomicLongArray中维护的是一个数组,可以选择性的更新其中某一个索引对应的值,是原子性的操作。
    相比于AtomicInteger和AtomicLong中的方法,AtomicLongArray中的方法,参数列表中多了数组索引的值。

    5)AtomicBoolean.compareAndSet()

    这个方法在实际应用中有些场景比较实用,可以实现需要保证“程序只执行一次,绝对不会重复“的场景,它可以保证一个boolean值只被改变一次。
JAVA高并发-线程安全性(原子性、可见性、有序性)
2.原子性-锁(Synchronized)
实现锁的两种方式:
    1)synchronized:在作用对象的作用范围内,依赖JVM实现操作的原子性。
    2)Lock:依赖特殊的CPU指令,代码实现,如ReentrantLock(本章不做说明)。
Synchronized关键字使用方式:
    1)修饰代码块:大括号括起来的代码,作用于调用的对象,也就是当前对象,不同对象之间互不影响,交叉执行。
    2)修饰方法:整个方法,作用于调用的对象,也就是当前对象,不同对象之间互不影响,交叉执行。
    3)修饰静态方法:整个静态方法,作用于所有对象,不同对象调用,不会出现交叉执行的现象。
    4)修饰类:括号括起来的部分,作用于所有对象,不同对象调用,不会出现交叉执行的现象。
注:子类继承父类时,如果父类中有syncronized修饰的方法,syncronized关键字是不会继承过去的,因为syncronized关键字不属于方法声明的一部分。
3.Atomic、Synchronized、Lock对比
syncronized:不可中断锁,适合竞争不激烈场景,可读性好。
Lock:可中断锁,多样化同步,竞争激烈时能维持常态,需要大量代码实现。
Atomic:竞争激烈时能维持常态,比Lock性能好;缺点是一次只能同步一个值,虽然提供了AtomicReference、AtomicReferenceFieldUpdater也只是一次同步一个对象。

二、可见性

    一个线程对主内存的修改可以及时的被其他线程观察到。
    导致共享变量在线程间不可见的原因:
    ·线程交叉执行。
    ·代码重排序结合线程交叉执行。
    ·共享变量更新后的值没有在工作内存与主内存之间及时更新。
1.可见性-syncronized
    JMM关于syncronized的两条规定:
    1)线程解锁前,必须把共享变量的最新值刷新到主内存中。
    2)线程加锁时,将清空工作内存*享变量的值,从而使得使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)
    由于syncronized可以保证原子性及可见性,变量只要被syncronized修饰,就可以放心的使用
2.可见性-volatile
    通过加入内存屏障和禁止重排序优化来实现可见性。
    具体实现过程:
    1)对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存。
    2)对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
注:volatile不能保证操作的原子性,也就是不能保证线程安全性。
    如果需要使用volatile必须满足以下两个条件:
    1)对变量的写操作不依赖与变量当前的值。
    2)该变量没有包含在具有其他变量的不变的式子中。
    所以volatile修饰的变量适合作为状态标记量。

JAVA高并发-线程安全性(原子性、可见性、有序性)

JAVA高并发-线程安全性(原子性、可见性、有序性)

三、有序性

    Java内存模型中,允许编译器和处理器对指令进行重排序,但重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
    volatile、syncronized、Lock都可保证有序性。
    一系列操作如果无法保证happens-before原则,就说这段操作无法保证有序性。
1.happens-before原则
    1)程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
    2)锁定规则:一个unLock操作先行发生于后面对同一个锁的Lock()操作。
    3)volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
    4)传递规则:如果操作A先行发生与操作B,而操作B先行发生于操作C,则操作A先行发生于操作C。
    5)线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
    6)线程终端规则:对线程interrupt()方法的调用先行发生与被中断线程的代码检测到中断事件的发生(只有执行了interrupt()方法才可以检测到中断事件的发生)。
    7)线程终结规则:线程中所有操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行。
    8)对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

四、总结

1.原子性:Atomic包、CAS算法、synchronized、Lock
2.可见性:synchronized、volatile
3.有序性:happens-before原则