字段更新器,主要是用来更新自定义类的字段。Java 提供以下三种字段更新器:
注意的是:字段更新器要操作(原子操作)哪个字段,哪个字段必须被 volatile 修饰,否则会出现异常。
Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
package com.example.test.java.juc;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class AtomicIntegerFieldUpdaterTest {
private volatile int field;
public static void main(String[] args) {
AtomicIntegerFieldUpdater fieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(AtomicIntegerFieldUpdaterTest.class, "field");
AtomicIntegerFieldUpdaterTest test5 = new AtomicIntegerFieldUpdaterTest();
fieldUpdater.compareAndSet(test5, 0, 10);
// 修改成功 field = 10
System.out.println(test5.field);
// 修改成功 field = 20
fieldUpdater.compareAndSet(test5, 10, 20);
System.out.println(test5.field);
// 修改失败 field = 20
fieldUpdater.compareAndSet(test5, 10, 30);
System.out.println(test5.field);
}
}
运行结果:
10
20
20
原子累加器主要解决多线程情况下i++线程不安全的问题。
jdk1.8后出现了DoubleAdder、DoubleAccumulator、 LongAdder、LongAccumulator,专门用于做累加操作的。
package com.example.test.java.juc;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class LongAdderTest {
/**
参数1:提供累加对象
参数2: 操作方法
supplier 提供者 无中生有 ()->结果
consumer 消费者 一个参数没结果 (参数)->void,
*/
private static void demo(Supplier adderSupplier, Consumer action) {
T adder = adderSupplier.get();
// 纳秒
long start = System.nanoTime();
List ts = new ArrayList<>();
// 40 个线程,对入参变量 每人累加 50 万 : 40*50万 = 2000万(原子操作的结果)
for (int i = 0; i < 40; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(adder);
}
}));
}
ts.forEach(t -> t.start());
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 纳秒
long end = System.nanoTime();
// 从 Java SE 7 的版本开始,程序中的数字可以使用下划线来进行分割(_)以便于为程序提供更好的可读性。
// 你可以对一个比较长的数字,使用下划线来进行分隔,以便于你不会数错 0
System.out.println(adder + " cost:" + (end - start)/1000_000 + "ms");
}
/**
* @param args
*
* LongAdder性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]…
* 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。
*
*
*/
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
demo(() -> new LongAdder(), adder -> adder.increment());
}
for (int i = 0; i < 5; i++) {
demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
}
}
}
运行结果:
20000000 cost:105ms
20000000 cost:73ms
20000000 cost:79ms
20000000 cost:83ms
20000000 cost:94ms
20000000 cost:752ms
20000000 cost:735ms
20000000 cost:734ms
20000000 cost:748ms
20000000 cost:756ms
LongAdder类有几个关键域:
// 累加单元数据,惰性初始化 。Cell进行初始化时,默认容量为2
transient volatile Cell[] cells;
// 基础值,如果没有竞争,则用cas累加这个域,
transient vloatile long base;
// 在cells创建或扩容时,置为1,表示cas加锁。
transient volatile int cellsBusy;
如果对个线程同时对 LongAdder进行累加,cell会扩容,最大扩容到cpu的核数。每个线程对自己拿到的槽位上的数据进行累加。如果没有线程竞争,只是base进行累加,cell不会进行初始化。
transient关键字:
(1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法被访问。
(2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
(3)一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。
package com.example.test.thread.cas;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
/**
* cas锁示例,自己代码中不建议这样写。
*/
@Slf4j(topic = "com.test.LockCas")
public class LockCas {
// 0-没加锁,1-已加锁
private AtomicInteger state = new AtomicInteger(0);
// 加锁
public void lock(){
while (true) {
if (state.compareAndSet(0,1)) {
break;
}
}
}
// 解锁
public void unlock() {
log.debug("unkock...");
state.set(0);
}
public static void main(String[] args) {
LockCas lock = new LockCas();
new Thread(() -> {
log.debug("begin...");
// 加锁
lock.lock();
try {
log.debug("lock...");
// 执行代码
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 代码执行完成后,解锁
lock.unlock();
}
},"线程1").start();
new Thread(() -> {
log.debug("begin...");
// 加锁
lock.lock();
try {
log.debug("lock...");
// 执行代码
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 代码执行完成后,解锁
lock.unlock();
}
},"线程2").start();
}
}
运行结果:
10:35:32.273 [线程2] DEBUG com.test.LockCas - begin...
10:35:32.273 [线程1] DEBUG com.test.LockCas - begin...
10:35:32.281 [线程2] DEBUG com.test.LockCas - lock...
10:35:33.283 [线程2] DEBUG com.test.LockCas - unkock...
10:35:33.283 [线程1] DEBUG com.test.LockCas - lock...
10:35:34.284 [线程1] DEBUG com.test.LockCas - unkock...
Cell :累加单元 。
源码如下:
// 该注解的作用:防止缓存行的伪共享。
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
// 最重要的方法:用cas方式进行累加,prev 表示旧值,next表示新值。
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);
}
}
}
以上原理说明得从缓存说起:
1、缓存与内存的速度比较:
下图为cpu的内存结构
寄存器 | 1 cyle(4GHz的cpu约为0.25ns) |
---|---|
L1 : 一级缓存 | 3~4 cyle |
L2 : 二级缓存 | 10~20 cyle |
L3:三级缓存 | 40~45 cyle |
内存 | 120~240 cyle |
因为cpu与内存的速度差异很大,需要靠预读数据到缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应一块内存,一般是64bytes(8个long).
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心(cpu)的缓存行中,cpu要保证数据的一致性,如果某个cpu核心更改了数据,其他cpu核心对应的真个缓存行必须失效。
因为Cell是数组形式,在内存中是连续存储的,一个Cell为24字节(16字节的对象头和8字节long类型的value),因此魂村行可以存下2个Cell对象。这样问题就来了:
core-0 要修改Cell[0]
core-1 要修改Cell[1]
无论谁修改成功,都会导致对方core的缓存行失效,比如core-0中Cell[0]=6000,Cell[1]=8000, 要累加Cell[0]=6001,Cell[1]=8000,这时会让core-1的缓存行失效。
@sun.misc.Contended用来解决这个问题,它的原理是在使用该注解的对象或字段的前后各增加128字节大小的padding,从而让cpu将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。如下图: