本文基于jdk8
参考:
黑马程序员深入学习Java并发编程,JUC并发编程全套教程_哔哩哔哩_bilibili
CAS原理
CAS:比较和交换(设置) Compare And Swap(Set)。当A的值为5的时候,给A设置值为10。这里涉及到的比较和设置 值的操作是原子的。
CAS的操作系统层实现
操作系统层面的CAS是一条CPU的原子指令 (cmpxchg指令),正是由于该指令具备了原子性,因此使用CAS操作数据时不会造成数据不一致的问题。
CAS的Java层实现
Java是把底层C++代码调用的这个原子指令封装到native方法中了。不过还提供了Unsafe类用来操作相关线程等。但这个类无法直接调用,只能通过反射来使用。该类不建议使用。
获取Unsafe
java
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeAccessor {
static Unsafe unsafe;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
static Unsafe getUnsafe() {
return unsafe;
}
}
使用Unsafe
java
import sun.misc.Unsafe;
import java.lang.reflect.Field;
class Student {
volatile int id;
volatile String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public static void main(String[] args) throws NoSuchFieldException {
Unsafe unsafe = UnsafeAccessor.getUnsafe();
Field id = Student.class.getDeclaredField("id");
Field name = Student.class.getDeclaredField("name");
// 获得成员变量的偏移量
long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);
long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);
Student student = new Student();
// 使用 cas 方法替换成员变量的值
UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true
UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
System.out.println(student);
}
}
CAS中的ABA问题
某个共享变量在线程1中刚获取到值为A,后面要对这个值进行CAS操作,把它变成C。
但是线程1在CAS之前,线程2也读到了共享变量的值,并且它先于线程1对改共享变量的CAS操作,把这个共享变量变成B,最后在线程1在CAS操作之前又把这个变量改成A。
当线程1要执行CAS操作时,它一看还是原来的A,就直接执行了,但实际上并不是原来的A了
java
import java.util.concurrent.atomic.AtomicReference;
import static java.lang.Thread.sleep;
public class CasAba {
static volatile AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程获取A");
// 获取值 A
// 这个共享变量被它线程修改过?
String prev = ref.get();
other();
sleep(1000);
// 尝试改为 C
if (ref.compareAndSet(prev, "C")) {
System.out.println("主线程修改成功:" + prev + "->" + ref.get());
}
}
private static void other() throws InterruptedException {
new Thread(() -> {
System.out.println("t1线程获取A");
String prev = ref.get();
if (ref.compareAndSet(ref.get(), "B")) {
System.out.println("t1修改成功:" + prev + "->" + ref.get());
}
}, "t1").start();
sleep(500);
new Thread(() -> {
System.out.println("t2线程获取B");
String prev = ref.get();
if (ref.compareAndSet(ref.get(), "A")) {
System.out.println("t2修改成功:" + prev + "->" + ref.get());
}
}, "t2").start();
}
}
解决方案
如果比较值不够,那么就在值的基础上在加个版本号。每次读取该值,版本就加一。比较的时候也要看看版本号是不是之前的版本。
AtomicStampedReference
该类就是带版本号的实现。
java
import java.util.concurrent.atomic.AtomicStampedReference;
import static java.lang.Thread.sleep;
public class AtomicStampedReferenceTest {
static volatile AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
System.out.println("主线程获取初始值:" + prev + " 版本号:" + stamp);
other();
sleep(1000);
// 尝试改为 C + 版本号加一
if (ref.compareAndSet(prev, "C", stamp, stamp + 1)) {
System.out.println("主线程修改成功:" + prev + "->" + ref.getReference() + " 版本号:" + ref.getStamp());
} else {
System.out.println("主线程修改失败:" + prev + "->" + ref.getReference() + " 版本号:" + ref.getStamp());
}
}
private static void other() throws InterruptedException {
new Thread(() -> {
String prev = ref.getReference();
int stamp = ref.getStamp();
System.out.println("t1线程获取初始值" + prev + " 版本号:" + stamp);
if (ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1)) {
System.out.println("t1修改成功:" + prev + "->" + ref.getReference() + " 版本号:" + ref.getStamp());
}
}, "t1").start();
sleep(500);
new Thread(() -> {
String prev = ref.getReference();
int stamp = ref.getStamp();
System.out.println("t2线程获取初始值" + prev + " 版本号:" + stamp);
if (ref.compareAndSet(prev, "A", stamp, stamp + 1)) {
System.out.println("t2修改成功:" + prev + "->" + ref.getReference());
}
}, "t2").start();
}
}
CAS特点
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,多重试几次
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
常见应用
以下这些类或者关键字都使用了CAS。大部分都在juc下
synchronized
synchronized在进行锁升级的时候,基本在每次升级的使用都用到了CAS来设置值。
比如在偏向锁时,把线程ID记录到对象头中,这里的记录用的CAS来记录的;
在轻量级锁时,要把对象头中的Mark Word和锁相关的值记录到锁记录中。
上面这些都是单次的设置值的操作,而在轻量级锁在变成重量级锁之前,会自旋等待,这里就一直是CAS操作。
原子基础类型
对于一些基础类型,juc提供了原子类来方便使用。
java
import java.util.concurrent.atomic.AtomicInteger;
public class TestAtomicInteger {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));
}
}
原子引用类型
可以使用这个类型来把对象的引用给变成原子的。
AtomicReference AtomicMarkableReference AtomicStampedReference
比如上面CAS中的ABA问题中的实例,String的使用方法。
原子数组
对于数组的包装:AtomicIntegerArray AtomicLongArray AtomicReferenceArray
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class TestAtomicArray {
/**
* 参数1,提供数组、可以是线程不安全数组或线程安全数组
* 参数2,获取数组长度的方法
* 参数3,自增方法,回传 array, index
* 参数4,打印数组的方法
*/
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
private static <T> void demo(
Supplier<T> arraySupplier,
Function<T, Integer> lengthFun,
BiConsumer<T, Integer> putConsumer, Consumer<T> printConsumer) {
List<Thread> ts = new ArrayList<>();
T array = arraySupplier.get();
int length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j % length);
}
}));
}
ts.forEach(t -> t.start()); // 启动所有线程
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 等所有线程结束
printConsumer.accept(array);
}
public static void main(String[] args) {
demo( () -> new int[10],
(array) -> array.length,
(array, index) -> array[index]++,
array -> System.out.println(Arrays.toString(array)));
demo( () -> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array));
}
}
原子字段(属性)
对于类中的字段的包装,必须配合volatile使用,不然报异常
AtomicReferenceFieldUpdater AtomicIntegerFieldUpdater AtomicLongFieldUpdater
java
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class TestFieldAtomic {
private volatile int field;
public static void main(String[] args) {
AtomicIntegerFieldUpdater fieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(TestFieldAtomic.class, "field");
TestFieldAtomic testFieldAtomic = new TestFieldAtomic();
fieldUpdater.compareAndSet(testFieldAtomic, 0, 10);
// 修改成功 field = 10
System.out.println(testFieldAtomic.field);
// 修改成功 field = 20
fieldUpdater.compareAndSet(testFieldAtomic, 10, 20);
System.out.println(testFieldAtomic.field);
// 修改失败 field = 20
fieldUpdater.compareAndSet(testFieldAtomic, 10, 30);
System.out.println(testFieldAtomic.field);
}
}
原子累加器
原子累加器有两类。其中LongAdder这一类的性能比AtomicLong要高。
java
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 TestAccumulator {
private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
T adder = adderSupplier.get();
long start = System.nanoTime();
List<Thread> ts = new ArrayList<>();
// 4 个线程,每个累加 50 万
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();
System.out.println(adder + " cost:" + (end - start)/1000_000);
}
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());
}
}
}
性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。另外在设置累加单元时需要注意缓存行伪共享的问题。
java
// 累加单元数组, 懒惰初始化
// transient 表示这个域不会被序列化
// 在并发编程中,序列化包含当前运行状态的字段可能会导致问题,比如反序列化后状态与预期不符。
// 例如,cells数组中的Cell对象保存着临时的累加值,这些值在不同的时间点可能完全不同。将这种类型的数据序列化并在其他时间或环境中恢复,很可能会失去其初衷和准确性。
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;
// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;
防止缓存行伪共享
java
// 该注解是为了防止缓存行的伪共享
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
// 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
final boolean cas(long prev, long next) {
return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
}
// 省略不重要代码
}
缓存行
因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte ( 8 个 long )
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
伪共享
如果多个CPU读取的是同一个缓存行,那么一个CPU更新了这里面的某个值(并同步到内存),另外一个CPU还得从内存中读取。这种现象就是伪共享。如下图:
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 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就能防止这种现象