一、原子性
提供了互斥访问,同一时刻只能有一个线程对它进行操作。
保证原子性的操作:
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)进行实现。
compareAndSwapInt()方法是一个native方法,代表这是一个java底层的方法。
该方法的实现原理:
首先看getAndAddInt()函数的参数列表Object var1是当前传入的对象,var2是当前对象的值,var4是要增加的值(自增:var4为1),方法中var5为获取当前对象在主存中的值;
然后看compareAndSwapInt()函数的参数列表,var为当前对象,var2为当前传入的值,var5为当前对象在主存中的值,var5+var4是“底层的值” 和 “要增加的值” 的和;
如果当前对象传入的值var2和底层获取的值var5相等,则将当前对象的值更新为要获取的值也就是var5+var4;否则重新从底层取值var5,重新从var1取值var2,进行判断。通过一直这样循环,保证当我们期望的值和底层的值相同时,才可以操作数据将底层的值刷新为新的值。
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
AtomicReferenceFieldUpdater
3)AtomicStampReference:CAS的ABA问题
什么是ABA问题:在多线程运行环境下CAS操作时,其他线程将变量A改成了B又改成了A,当本线程用期望值和该变量进行比较时,发现A变量的值没有变就进行了数据修改操作,这样是与设计初衷不符的。
解决方法是每次变量更新时,会赋给变量一个版本号并加1递增,这样就避免了ABA问题。
AtomicStampReference类提供了解决这一问题的方法,核心方法是compareAndSet()方法,方法中多了一个对stamp的比较,就是对变量版本号的比较,stamp的值也是在每次变量进行更新时进行维护。
4)AtomicLongArray
AtomicLongArray中维护的是一个数组,可以选择性的更新其中某一个索引对应的值,是原子性的操作。
相比于AtomicInteger和AtomicLong中的方法,AtomicLongArray中的方法,参数列表中多了数组索引的值。
5)AtomicBoolean.compareAndSet()
这个方法在实际应用中有些场景比较实用,可以实现需要保证“程序只执行一次,绝对不会重复“的场景,它可以保证一个boolean值只被改变一次。
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也只是一次同步一个对象。