文章目录
- 一、概述与结构:
- 二、:原子更新基本类型
- 三、:原子更新引用类型
- 四、:原子更新数组
- 五、:原子更新字段类型
- 六、:JDK1.8原子类型
- ①新增内容简介
- ②LongAdder分析
- 1、add()
- 2、add()其他补充说明
- 3、sum()
- 4、reset()
- 5、sumThenReset()
- ③待添加
- 知识补充:Striped64类
- 知识补充:CAS
- 系列文章传送门:
一、概述与结构:
①原子类
在并发编程中很容易出现线程安全问题,一个常见的例子就是多线程更新变量i。比如多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。
实际上,在J.U.C下的atomic包提供了一系列的操作简单、性能高效,并能保证线程安全的类去更新基本类型变量、数组元素、引用类型以及更新对象中的字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。
②类型分类
整体类结构可以参考JUC探险-1、初识概貌中的图。
现在大致将原子类分为五类:
●基本类型:AtomicBoolean、AtomicInteger、AtomicLong
●引用类型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference
●数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
●对象的属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
●JDK1.8新增类:DoubleAdder、LongAdder、DoubleAccumulator、LongAccumulator
二、:原子更新基本类型
①基本作用
●AtomicBoolean:以原子更新的方式更新Boolean。
●AtomicInteger:以原子更新的方式更新Integer。
●AtomicLong:以原子更新的方式更新Long。
②常用API
这几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法。
●addAndGet(int delta):以原子方式将输入的数值与实例中原本的数值相加,并返回相加后的结果。
●incrementAndGet():以原子的方式将实例中的原数值进行加1操作,并返回相加后的结果。
●getAndSet(int newValue):将实例中的数值更新为新数值,并返回旧数值。
●getAndIncrement():以原子的方式将实例中的原数值加1,返回的是自增前的旧数值。
③源码分析
1、getAndIncrement()
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
可以看出,该方法实际上是调用了unsafe实例的getAndAddInt()方法,unsafe实例的获取时通过UnSafe类的静态方法getUnsafe()获取:
private static final Unsafe unsafe = Unsafe.getUnsafe();
Unsafe类在sun.misc包下,Unsafer类提供了一些底层操作,atomic包下的原子操作类的也主要是通过Unsafe类提供的compareAndSwapInt(),compareAndSwapLong()等一系列提供CAS操作的方法来进行实现。
2、compareAndSet()
AtomicBoolean类实现更新的核心方法是compareAndSet()方法,其源码如下:
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
可以看出,compareAndSet()方法的实际上也是先转换成0、1的整型变量,然后是通过针对int型变量的原子更新方法compareAndSwapInt()来实现的。
AtomicInteger类主要利用CAS(compare and swap)+ volatile 和 native 方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。
public class AtomicInteger extends Number implements java.io.Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
}
UnSafe类的objectFieldOffset()方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外value是一个volatile变量,在内存中可见,因此JVM可以保证任何时刻任何线程总能拿到该变量的最新值。
atomic包中只提供了对boolean、int、long这三种基本类型的原子更新的方法。那么参考这些更新的思想、方式,原子更新char、double、float也可以采用类似的实现。
三、:原子更新引用类型
①基本作用
●AtomicReference:原子更新引用类型。
●AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号。(带版本号是为了解决CAS的ABA问题)
●AtomicMarkableReference:原子更新带有标记位的引用类型。
②常用API
这几个类的用法基本一致,这里以AtomicReference为例总结常用的方法。
●compareAndSet(V expect, V update):以原子更新的方式将对象进行更新。
●getAndSet(V newValue):将实例中的对象更新为新对象,并返回旧对象。
③源码分析
1、compareAndSet()
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
可以看出,该方法实际上是调用了unsafe实例的compareAndSwapObject()方法,与原子基本类型情况类似。不同之处在于比较的是对象。
四、:原子更新数组
①基本作用
●AtomicIntegerArray:原子更新Integer数组中的元素。
●AtomicLongArray:原子更新Long数组中的元素。
●AtomicReferenceArray:原子更新引用类型数组中的元素。
②常用API
这几个类的用法基本一致,这里以AtomicIntegerArray为例总结常用的方法。
●addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加。
●getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1。
●compareAndSet(int i, int expect, int update):以原子更新的方式将数组中索引为i的元素进行更新。
③源码分析
1、addAndGet()
public final int addAndGet(int i, int delta) {
return getAndAdd(i, delta) + delta;
}
public final int getAndAdd(int i, int delta) {
return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}
可以看出,该方法实际上是调用了unsafe实例的getAndAddInt()方法,与原子基本类型情况类似。不同之处在于会多一个指定数组索引位i,从数组中取值。
五、:原子更新字段类型
①基本作用
●AtomicIntegerFieldUpdater:原子更新整型字段类。
●AtomicLongFieldUpdater:原子更新长整型字段类。
●AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
②常用API
这几个类的用法基本一致,这里以AtomicIntegerFieldUpdater为例总结常用的方法。
●compareAndSet(T obj, int expect, int update):以原子更新的方式将对象中的字段进行更新。
●getAndIncrement(T obj):以原子更新的方式将对象中的字段自增加1。
③源码分析
1、compareAndSet()
public final boolean compareAndSet(T obj, int expect, int update) {
accessCheck(obj);
return U.compareAndSwapInt(obj, offset, expect, update);
}
可以看出,该方法实际上是调用了unsafe实例的compareAndSwapInt()方法,与原子基本类型情况类似。不同之处在于在操作之前检查了对象类型。
要想使用原子更新字段需要两步操作:
1、原子更新字段类都是抽象类,只能通过静态方法newUpdater来创建一个更新器,并且需要设置想要更新的类和属性,例如:
private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(类名.class,"字段名");
2、更新类的属性必须使用public volatile进行修饰。
六、:JDK1.8原子类型
①新增内容简介
DoubleAdder、LongAdder、DoubleAccumulator、LongAccumulator是JDK1.8新增的部分,是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。
②LongAdder分析
AtomicLong的实现方式是内部有个value变量,当多线程并发自增、自减时,均通过CAS指令从机器指令级别操作保证并发的原子性。
LongAdder中包含了一个Cell数组,Cell是Striped64的一个内部类,顾名思义,Cell代表了一个最小单元。定义如下:
// 为提高性能,使用注解@sun.misc.Contended,用来避免伪共享
@sun.misc.Contended static final class Cell {
// 用来保存要累加的值
volatile long value;
Cell(long x) { value = x; }
// cas更新value值
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
// 这个静态方法用于获取偏移量
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
Cell内部有一个非常重要的value变量,并且提供了一个CAS更新其值的方法。
1、add()
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
// uncontended标识在cells中,当前线程要做cas累加操作的某个元素是否存在争用。(true代表不存在争用,false代表存在争用)
boolean uncontended = true;
// 判断一、判断二
// 此处两个判断都代表cells数组没有被初始化成功(初始化成功的cells数组长度为2)
if (as == null || (m = as.length - 1) < 0 ||
// 判断三
// 通过getProbe方法获取当前线程Thread的threadLocalRandomProbe变量的值,初始为0。
// getProbe() & m相当于getProbe() % cells.length
// 如果cells数组对应下标处的值为null,说明这个位置从来没有线程做过累加,继续向下执行,此时创建一个新的Cell对象a
(a = as[getProbe() & m]) == null ||
// 判断四
// 尝试对cells数组对应下标处的值做累加操作,并返回操作结果;如果失败继续向下执行
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
ⅰ、运行进入if判断,cells此时为null,进入casBase()方法:
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
此处执行CAS累加操作,并返回成功与否。
ⅱ、如果casBase()方法执行成功,则不会进入if内部。但是当在高并发的情况下,casBase()方法可能执行失败,此时AtomicLong和LongAdder处理方式就有了差异。
●AtomicLong会自旋直到原子操作成功。
●LongAdder会进入if内部执行操作。(具体操作比较复杂,请往下看)
ⅲ、在if内部,先设置uncontended标识为未争用,然后进行判断、操作。通过所有判断到达代码内部有三种情况:
●cells没有初始化。(通过前两个判断)
●当前线程hash到的cells数组中的位置还没有其它线程做过累加操作。(通过第三个判断)
●产生了冲突。(通过第四个判断)
ⅳ、此时如果通过了各项判断,最终会来到longAccumulate()方法:
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
int h;
// 获取当前线程的threadLocalRandomProbe值作为h
// 如果h == 0,代表新的线程开始参与cells争用,可能的场景有两种(凡是参与了cells争用操作的线程threadLocalRandomProbe都不为0)
// 1、当前线程之前还没有参与过cells争用(或者cells数组还没初始化,进到当前方法来就是为了初始化cells数组后争用的),在外层cas操作失败
// 2、在外层执行add()方法时,对cells某个位置的Cell的cas操作失败,将wasUncontended设置为false
if ((h = getProbe()) == 0) {
// 如果h为0,说明当前线程是第一次进入该方法,则强制设置线程的threadLocalRandomProbe为ThreadLocalRandom类的成员静态私有变量probeGenerator的值,后文在介绍hash值的生成时会详细说明
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
// cas冲突标志,表示当前线程hash到的Cells数组的位置。(collide=true代表有冲突,collide=false代表无冲突)
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
// 分支一:cells已经初始化
if ((as = cells) != null && (n = as.length) > 0) {
// 内部分支1:如果被hash到的位置为null,说明没有线程在这个位置设置过值,没有竞争,可以直接使用
if ((a = as[(n - 1) & h]) == null) {
// cellsBusy == 0,代表当前没有线程对cells数组做修改
if (cellsBusy == 0) { // Try to attach new Cell
// 将要累加的x值作为初始值创建一个新的Cell对象
Cell r = new Cell(x); // Optimistically create
// cellsBusy == 0,尝试获取cellsBusy锁
if (cellsBusy == 0 && casCellsBusy()) {
// 标记Cell是否创建成功并放入到cells数组被hash的位置上
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
// 再次检查cells数组不为null,且长度大于0,且hash到的位置的Cell为null
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
// 将新的cell设置到该位置
rs[j] = r;
created = true;
}
} finally {
// 解除cellsBusy锁
cellsBusy = 0;
}
// 生成成功,跳出循环
if (created)
break;
// 如果created为false,说明上面指定的cells数组的位置cells[m % cells.length]已经有其它线程设置了cell了,继续执行循环
continue; // Slot is now non-empty
}
}
// 如果执行到此处,说明cellsBusy不为0,有线程正在更改cells数组,代表产生了冲突,于是将collide设置为false
collide = false;
}
// 内部分支2:被hash到的位置不为null,存在竞争
else if (!wasUncontended) // CAS already known to fail
// 设置未竞争标志位true,继续执行(后面会重新计算prob的值,然后重新执行循环)
wasUncontended = true; // Continue after rehash
// 内部分支3:新的争用线程参与争用的情况(外层cas竞争失败,进入后未竞争标志位被设置为true)
// 尝试将x值加到cells[m % cells.length]的value,如果成功直接退出
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
// 内部分支4:内部分支3处理新的线程争用执行失败了
// 这时如果cells数组的长度已经到了最大值(大于等于cup数量),或者是当前cells已经做了扩容,则将collide设置为false(后面会重新计算prob的值,然后重新执行循环)
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
// 内部分支5:cas冲突标志为false(无冲突)
else if (!collide)
// 设置cas冲突标志为true,表示发生了冲突,需要再次生成hash(再次循环走到此处时会进入内部分支6)
collide = true;
// 内部分支6:至少两次竞争失败,考虑开始cells扩容
else if (cellsBusy == 0 && casCellsBusy()) {
try {
// 检查cells是否已经被扩容
if (cells == as) { // Expand table unless stale
// cells扩容
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
// 为当前线程重新计算hash值
h = advanceProbe(h);
}
// 分支二:cells尚未初始化或长度为0
// 尝试获取cellsBusy锁
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
// 初始化cells数组,初始容量为2,并将要累加的x值通过hash&1,放到0个或第1个位置上
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
// 解除cellsBusy锁
cellsBusy = 0;
}
// 如果init = true,说明初始化成功,跳出循环
if (init)
break;
}
// 分支三:cells尚未初始化或长度为0,并且尝试获取cellsBusy锁失败
// 尝试将值累加到base上
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
㈠获取当前线程的threadLocalRandomProbe值,判断是否为0。
如果threadLocalRandomProbe为0,代表新的线程开始参与cells争用,可能的场景有两种:
●当前线程之前还没有参与过cells争用(或者cells数组还没初始化,进到当前方法来就是为了初始化cells数组后争用的),在外层cas操作失败。
●在外层执行add()方法时,对cells某个位置的Cell的cas操作失败,将wasUncontended设置为false。
㈡进入一个死循环,其中包含外层三个分支路线,第一个分支路线又包含内部6个子分支路线。
●外层分支一:cells已经初始化时进入(处理外层判断三、判断四)
▶内层分支1:被hash到的位置为null,说明没有线程在这个位置设置过值,没有竞争,可以直接使用。此时用x值作为初始值创建一个新的Cell对象,对cells数组使用cellsBusy加锁,然后将这个Cell对象放到cells[m % cells.length]位置上。
▶内层分支2:被hash到的位置不为null,存在竞争。此时设置未竞争标志位true,继续执行。(后面会重新计算prob的值,然后重新执行循环)
▶内层分支3:新的争用线程参与争用的情况(外层cas竞争失败,进入后未竞争标志位被设置为true)。尝试将x值加到cells[m % cells.length]的value,如果成功直接退出。
▶内层分支4:内部分支3处理新的线程争用执行失败了。这时如果cells数组的长度已经到了最大值(大于等于cup数量),或者是当前cells已经做了扩容,则将collide设置为false。(后面会重新计算prob的值,然后重新执行循环)
▶内层分支5:cas冲突标志为false(无冲突)。设置cas冲突标志为true,表示发生了冲突,需要再次生成hash。(再次循环走到此处时会进入内部分支6)
▶内层分支6:至少两次竞争失败,考虑开始cells扩容。扩容前先检查是否已经扩容。
为当前线程重新计算hash值。
●外层分支二:cells尚未初始化或长度为0时进入(处理外层判断一、判断二)
▶先尝试获取cellsBusy锁。
▶初始化cells数组,初始容量为2,并将要累加的x值通过hash&1,放到0个或第1个位置上。
▶解除cellsBusy锁,初始化成功后跳出循环。
●外层分支三:cells尚未初始化或长度为0,且获取cellsBusy锁失败时进入
▶尝试将要累加的x值累加到base上。
ⅴ、回头看最外层if的判断,其实当不存在线程竞争的时候,cells将为null。只有当casBase()失败后,才会初始化cells。
2、add()其他补充说明
ⅰ、hash值的生成
这个hash值是LongAdder定位当前线程应该将值累加到cells数组哪个下标的。
首先看下Thread类中的一个成员变量threadLocalRandomProbe:
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
threadLocalRandomProbe这个变量的值就是LongAdder用来hash定位Cell在cells数组位置的,平时线程的这个变量一般用不到,它的值一直都是0。
在Striped64中通过getProbe()方法获取当前线程threadLocalRandomProbe的值:
static final int getProbe() {
// PROBE是threadLocalRandomProbe变量在Thread类里面的偏移量
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
ⅱ、threadLocalRandomProbe的初始化
在add()方法中,threadLocalRandomProbe的值保持为0,如果进入longAccumulate()方法,其值才会发生改变。
进入longAccumulate()方法后,第一件事就是判断threadLocalRandomProbe的值是否为0,为0时会将其初始化,核心逻辑在ThreadLocalRandom.current()中:
private static final AtomicInteger probeGenerator = new AtomicInteger();
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}
probeGenerator是static类型的AtomicInteger类,每执行一次localInit()方法,都会将probeGenerator累加一次。如果取得的值为0,会使用1提代。最后把值更新到threadLocalRandomProbe。
ⅲ、threadLocalRandomProbe重新生成(重新计算hash)
在longAccumulate()方法中,有一个重新生成hash的调用:
static final int advanceProbe(int probe) {
probe ^= probe << 13; // xorshift
probe ^= probe >>> 17;
probe ^= probe << 5;
UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
return probe;
}
对传入的值进行了一系列的左右移位 、异或操作,生成一个新值。
3、sum()
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
返回累加的和,但在高并发情况下不是一个准确的数值,只是近似准确的计数值。(没有处理线程安全问题)只能适用于偏差允许的情况下。
4、reset()
public void reset() {
Cell[] as = cells; Cell a;
base = 0L;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
a.value = 0L;
}
}
}
重置计数器,只应该在明确没有并发的情况下调用,可以用来避免重新new一个LongAdder。
5、sumThenReset()
public long sumThenReset() {
Cell[] as = cells; Cell a;
long sum = base;
base = 0L;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null) {
sum += a.value;
a.value = 0L;
}
}
}
return sum;
}
相当于sum()后再调用reset()。
③待添加
知识补充:Striped64类
①整体介绍
Striped64是在java8中新增的用来支持高并发累加器的并发组件,它的设计思路是在竞争激烈的时候尽量分散竞争。
Striped64的内部有三个重要的成员变量:
/**
* 存放Cell的hash表,大小为2的幂。
*/
transient volatile Cell[] cells;
/**
* 基础值,在没有竞争时会更新这个值;在cells不可用时,也会尝试累加这个值。
*/
transient volatile long base;
/**
* 自旋锁,通过CAS操作加锁,用于保护创建或者扩展cells。
*/
transient volatile int cellsBusy;
②具体内容
此处结合上面的LongAdder来举例说明:
1、cells
cells是并发高性能的关键:
●AtomicInteger只有一个value,所有线程累加都要通过cas竞争value这一个变量,高并发下线程竞争非常严重。
●LongAdder则有两个值用于累加,一个是base,它的作用类似于AtomicInteger里面的value,在没有竞争的情况不会用到cells数组。当有了竞争后cells数组就上场了,第一次初始化长度为2,以后每次扩容都是变为原来的两倍,直到cells数组的长度大于等于当前服务器cpu的数量为止就不在扩容(超过cpu数量的话效率不会更好,还有可能增加cpu切换的情况);每个线程会通过线程对cells[threadLocalRandomProbe % cells.length]位置的Cell对象中的value做累加,这样相当于将线程绑定到了cells中的某个cell对象上。
2、base
●在没有竞争的情况下,将累加值累加到base。
●在cells初始化的过程中,cells不可用,这时会尝试将值累加到base上。
3、cellsBusy
cellsBusy的作用是当要修改cells数组时加锁,防止多线程同时修改cells数组,0为无锁,1为加锁,加锁的状况有三种:
●cells数组初始化的时候。
●cells数组扩容的时候。
●如果cells数组中某个元素为null,给这个位置创建新的Cell对象的时候。
4、注解@sun.misc.Contended
此注解用于消除伪共享。
伪共享
cpu的缓存系统中是以缓存行(cache line)为单位存储的,缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节,cache line是cache和memory之间数据传输的最小单元。
大多数现代cpu都包含了L1和L2cache。对于L1 cache,大多是write though的,L2 cache则是write back的,不会立即写回memory,这就会导致cache和memory的内容的不一致;另外,对于多处理器环境,由于cache是cpu私有的,不同cpu的cache的内容也存在不一致的问题,因此很多的计算架构,不论是ccNUMA还是SMP都实现了cache coherence的机制(即不同cpu的cache一致性机制)。
Write-through(直写模式):在数据更新时,同时写入缓存Cache和后端存储。此模式的优点是操作简单。缺点是因为数据修改需要同时写入存储,数据写入速度较慢。
Write-back(回写模式):在数据更新时只写入缓存Cache。只在数据被替换出缓存时,被修改的缓存数据才会被写到后端存储。此模式的优点是数据写入速度快,因为不需要写存储。缺点是一旦更新后的数据未被写入存储时出现系统掉电的情况,数据将无法找回。
cache coherence的一种实现是通过cache-snooping协议,每个cpu通过对bus的snoop实现对其它cpu读写cache的监控:
●当cpu1要写cache时,其它cpu就会检查自己cache中对应的cache line。如果是dirty的,就write back到memory,并且会将cpu1的相关cache line刷新;如果不是dirty的,就废弃该cache line。
●当cpu1要读cache时,其它cpu就会将自己cache中对应的cache line中标记为dirty的部分write back到memory,并且会将cpu1的相关cache line刷新。
所以,提高cpu的缓存命中率,减少cache和memory之间的数据传输,将会提高系统的性能。
因此,在程序和二进制对象的内存分配中保持cache line一致就十分重要,如果不保证cache line对齐,出现多个cpu中并行运行的进程或者线程同时读写同一个cache line的情况的概率就会很大。这时cpu的cache和memory之间会反复出现write back和refresh情况,这种情形就叫做cache thrashing。
为了有效的避免cache thrashing,通常有以下两种途径:
●对于heap的分配,很多系统在malloc(动态内存分配)调用中实现了强制的一致性。
●对于stack的分配,很多编译器提供了stack aligned的选项。
当然,如果在编译器指定了stack aligned,程序的尺寸将会变大,会占用更多的内存。因此,这中间的取舍需要仔细考虑。
为了解决这个问题在jdk1.6采用了long padding的方式,jdk1.7的某个版本后优化了long padding,然后在jdk1.8中加入了@sun.misc.Contended。
除了LongAdder,Striped64在ConcurrentHashMap中也有使用,此处学习之后可以触类旁通。
知识补充:CAS
①整体介绍
CAS全称Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数:
●需要读写的内存值 V。
●进行比较的值 A。
●要写入的新值 B。
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
②代码学习
上面提到java.util.concurrent包中的原子类,就是通过CAS来实现了乐观锁,那么我们进入原子类AtomicInteger的源码,看一下AtomicInteger的定义:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
}
根据定义我们可以看出各属性的作用:
●unsafe: 获取并操作内存的数据。
●valueOffset: 存储value在AtomicInteger中的偏移量。
●value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。
接下来,我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通过class文件中的参数名,并不能很好的了解方法的作用,所以我们通过OpenJDK 8 来查看Unsafe的源码:
// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
根据OpenJDK 8的源码我们可以看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。
后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。
③问题与不足
CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:
●ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
●循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
●只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
系列文章传送门:
JUC探险-1、初识概貌
JUC探险-2、synchronized
JUC探险-3、volatile
JUC探险-4、final
JUC探险-5、原子类
JUC探险-6、Lock & AQS
JUC探险-7、ReentrantLock
JUC探险-8、ReentrantReadWriteLock
JUC探险-9、Condition
JUC探险-10、常见工具、数据结构
JUC探险-11、ConcurrentHashMap
JUC探险-12、CopyOnWriteArrayList
JUC探险-13、ConcurrentLinkedQueue
JUC探险-14、ConcurrentSkipListMap
JUC探险-15、BlockingQueue
JUC探险-16、ThreadLocal
JUC探险-17、线程池