目录
CAS
ABA问题
AtomicStampedReference
AtomicReferenceFieldUpdater
CAS底层原理
LongAdder(无锁+分段)
基本用法
缓存行
源码
Unsafe原理
手写AtomicInteger
在之前的文章中,我们详细讲过了Synchronized三、详解Synchronized-CSDN博客和ReentrantLock五、详解ReentrantLock-CSDN博客的原理,以及他们如何实现线程安全的。
但是在这两种方式种,都是在不满足条件的时候将当前线程阻塞,直到被另一个线程唤醒。
这两种方式都需要切换线程上下文,而线程切换将消耗大量资源。
jdk提供了一种无锁的方式来保证线程之间安全。即循环CAS。
CAS(Compare-and-Swap,比较并交换)是一种用于实现无锁(lock-free)数据结构的原子操作。
它是一种乐观锁策略,通过比较内存中的值和预期值来决定是否更新内存中的数据。
CAS操作通常包含三个参数:
1、内存地址(或变量)、
2、预期值(或者说旧的值)
3、新值。
当内存地址中的值与旧值相等时,将内存地址中的值更新为新值,否则不进行任何操作。(说明地址中的值已经被别的线程修改了)
CAS存在一个著名的问题,被称为ABA问题。
ABA问题是指,在CAS操作中,如果一个值原来为A,变为了B,然后又变回为A,那么在CAS检查时会发现该值没有发生变化,但是实际上却发生了变化。这就是所谓的ABA问题。
举个例子,假设有两个线程A和B,线程A准备用CAS将变量的值从1改为2,线程B同时将变量的值从1改为3,然后又改回1。此时,线程A进行CAS操作,发现变量的值仍然为1,所以CAS操作成功。但实际上,这个变量的值已经被线程B改变过了。
ABA问题的解决方案通常是使用版本号。在每次变量更新时,都让版本号加1。即使A经过变化又回到A,版本号也会有所不同。Java的AtomicStampedReference类就实现了这样的机制,它通过包装[E,Integer]的元组来对整数进行CAS操作,这个元组的第二位是版本号,通过版本号的变化来避免ABA问题。
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
通过AtomicStampedReference的构造函数,可以看到其将数据和版本封装到了pair中。 这个Pair是AtomicStampedReference类中的一个内部类
private static class Pair {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static Pair of(T reference, int stamp) {
return new Pair(reference, stamp);
}
}
在更新的时候需要4个参数:
1、期望值
2、新值
3、期望版本
4、新版本
AtomicReferenceFieldUpdater的主要作用是对某个类的实例的某个volatile字段进行原子操作,这样就可以避免使用昂贵的AtomicReference。在使用AtomicReferenceFieldUpdater时,需要提供目标类的Class对象、要更新的字段的类型的Class对象以及要更新的字段的名称。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class AtomicReferenceFieldUpdaterDemo {
static class User {
volatile String name;
}
private static AtomicReferenceFieldUpdater updater =
AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
public static void main(String[] args) {
User user = new User();
updater.compareAndSet(user, null, "Tom");
System.out.println(user.name); // 输出:Tom
}
}
在这个示例中,我们创建了一个User类,它有一个volatile字段name。然后我们使用AtomicReferenceFieldUpdater创建了一个更新器,用于对User实例的name字段进行原子操作。在main方法中,我们创建了一个User实例,并使用compareAndSet方法将name字段从null更新为"Tom"。
LongAdder是Java 8在java.util.concurrent.atomic包下新增的一个类,它提供了线程安全的加法操作。
LongAdder是为了高并发场景下的性能优化而设计的,相比于AtomicLong,它在高并发场景下有更好的性能,通过累加200w次的实验,LongAdder的效率是AtomicLong的4-5倍。
需要注意的是,LongAdder提供的是最终一致性,而不是即时一致性。也就是说,调用sum()方法获取的值可能不是最新的,因为其他线程可能还没有将自己的局部结果合并到全局结果中。如果需要即时一致性,应该使用AtomicLong。
其核心的思想是无锁+分段。所谓无锁就是cas去更新数据,分段就是利用Cells数组,将不同的线程打散到不同的Cell中。
import java.util.concurrent.atomic.LongAdder;
public class LongAdderExample {
private final LongAdder counter = new LongAdder();
public void increment() {
counter.increment();
}
public long get() {
return counter.sum();
}
}
LongAdder的源码核心变量有以下三个:
1、transient volatile Cell[] cells; //默认2的n次方 用来分段计数
2、transient volatile long base; //基本值,主要在没有争用时使用
3、transient volatile int cellsBusy; //自旋锁(通过CAS锁定)在调整大小和/或创建Cell时使用。当cellsBusy==1的时候,表示加锁了
首先看一下Cell这个类的代码:
@jdk.internal.vm.annotation.Contended static final class Cell {
volatile long value; //存储当前单元累加的变量
}
可以观察到当前class被一个@jdk.internal.vm.annotation.Contended注解注释了,此注解的主要作用是用于减少并发编程中的伪共享(False Sharing)问题。
伪共享是多线程编程中的一个问题,当多个线程修改互相独立的变量,但这些变量共享同一个缓存行时,就会导致性能下降。这是因为当一个线程修改了一个缓存行中的变量,整个缓存行都会被标记为无效,其他线程再访问同一个缓存行中的其他变量时,就需要重新从主内存中加载数据。
需要注意的是,@Contended注解是JDK内部的注解,不建议在普通的应用代码中使用。另外,这个注解的效果可能会因为JVM的具体实现和配置而有所不同。
具体解释一下什么是缓存行,已经Cell在缓存行中是如何存储的:
现代计算机基本上都是多核cpu,那么不同cpu之间共享了同一块内存(主存),但是cpu和主存的计算速度是有差距的,因此在cpu和主存之间增加了多级cpu缓存(包括寄存器)。一般来讲缓存是以行为单位,每行缓存为64个byte。
那么在LongAdder中,按照对象大小的计算方式,一个Cell对象=markword+Klass pointer+实例对象,也就是8(64位机)+8(非压缩klasspointer)+8(long类型value) = 24byte。
由于缓存行为64byte,因此如果Cell数组长度小于为2的时候,Cell[0]和Cell[1]就有放在了同一个缓存行中,那么就有可能被存储在不同cpu的缓存行中。
一旦线程1修改了Cell[0], 那么线程2也必须要刷新自己的缓存行。
如果将Cell[0]和Cell[1]都放到单独的缓存行中,那么线程1修改Cell[0], 不会影响线程2修改Cell[1]。
因此就利用@Contended注解,JVM会尽量将Cell对象与其他Cell隔离在不同的缓存行中,以减少伪共享的影响
LongAdder的主要核心代码就在这个add方法中:
public void add(long x) {
Cell[] cs; long b, v; int m; Cell c;
if ((cs = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[getProbe() & m]) == null ||
!(uncontended = c.cas(v = c.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
尝试分析:
1、一开始cells肯定是空的,因此第一个if不满足
2、开始对base进行cas,累加base的值。这个时候,如果是多线程进来,肯定有的线程成功,有的线程失败
3、如果casBase成功,则返回。如果失败。那就意味着一定是多线程在并发执行。
4、因此,cas失败的线程就执行if里面的逻辑,第二个if里面的逻辑cs肯定为空,那么就去执行longAccumulate方法。这个方法里面会创建cells,一会再说。
5、如果在其他线程已经创建好了cells的时候,有一个线程进来,第一个if一定满足,那么就判断第二个if,此时cells不是null,因此要判断 c = cs[getProbe() & m]) == null 也就是判断当前的线程是否已经有了对应的cell。如果没有,则执行longAccumulate,创建cell。如果不为null,那么就执行c.cas。 这个cas就是针对当前的线程的cell进行处理。
接下来看一下longAccumulate是如何创建累加单元cells的:
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
//1. 首先,它会尝试在一个叫做cells的数组中找到一个位置,然后在这个位置上进行累加操作。用当前线程id
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
//上来就是一个死循环
done: for (;;) {
Cell[] cs; Cell c; int n; long v;
//cells不为空的情况,也就是已经有线程创建过cells了
if ((cs = cells) != null && (n = cs.length) > 0) {
if ((c = cs[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
break done;
}
} finally {
cellsBusy = 0;
}
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (c.cas(v = c.value,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
break;
//如果线程cas失败,意味这个cell已经有线程占用了,就判断是否超过了cpu个数,用来设置collide=false 即不扩容
else if (n >= NCPU || cells != cs)
collide = false; // At max size or stale
else if (!collide)
collide = true;
//cells是空的,且未加锁,并且当前线程加锁成功
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == cs) // 创建扩容
cells = Arrays.copyOf(cs, n << 1);
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = advanceProbe(h);
}
//初始创建cells
else if (cellsBusy == 0 && cells == cs && casCellsBusy()) {
try { // Initialize table
if (cells == cs) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
break done;
}
} finally {
cellsBusy = 0;
}
}
// 线程针对自己的cell失败,则对base进行操作,如果对base失败,那么就直接for循环了
else if (casBase(v = base,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
break done;
}
}
这个方法的主要逻辑是这样的:
1. 首先,它会尝试在一个叫做cells的数组中找到一个位置,然后在这个位置上进行累加操作。
2. 如果这个位置上没有元素,它会尝试创建一个新的元素并放到这个位置上。
3. 如果这个位置上已经有元素,它会尝试对这个元素的值进行累加操作。
4. 如果在进行累加操作时发生了竞争(也就是其他线程也在对这个元素进行操作),它会尝试扩大cells数组的大小,然后再次尝试进行累加操作。
5. 如果cells数组已经达到了最大大小,或者扩大数组的操作失败,它会退回到使用一个叫做base的变量进行累加操作。
1、第一种情况,cells不存在
2、 第二种情况,cells存在,但是当前线程的cell不存在
3、第三种情况 ,cells存在且当前线程的cell存在
unsafe类是jdk中一个非常特殊的类,其内部大量的native方法来直接操作内存,如直接访问系统内存资源、线程调度等。这些操作在Java的安全模型中是被禁止的,因此它被命名为“Unsafe”,且本身unsafe类是一个单例的。其获取方式:
Unsafe unsafe = Unsafe.getUnsafe(); //单例
其主要功能是根据给定的对象,和偏移地址,来更新该地址上的数据且保证线程安全。
例如:
public static void main(String[] args) throws NoSuchFieldException {
Unsafe unsafe = Unsafe.getUnsafe();
//id相对Student类的偏移量
long id = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
long name = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));
Student student = new Student();
unsafe.compareAndSwapInt(student, id, 0, 1);
unsafe.compareAndSwapObject(student, name, null, "二牛");
System.out.println(student.toString());
}
但是上述代码在运行的时候会报错:
Exception in thread "main" java.lang.SecurityException: Unsafe
at jdk.unsupported/sun.misc.Unsafe.getUnsafe(Unsafe.java:99)
从异常上来看,提示一个线程安全的问题,这个主要是因为Unsafe类的设计初衷是为了系统类库的内部使用,所以它有一些访问限制。在JDK中,只有被Bootstrap ClassLoader加载的类才能通过Unsafe.getUnsafe()方法获取Unsafe实例。
具体可以看一下Unsafe.getUnsafe()代码的源码:
public static Unsafe getUnsafe() {
Class> caller = Reflection.getCallerClass();
//安全检查。这里会判断class loader
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
public static boolean isSystemDomainLoader(ClassLoader loader) {
return loader == null || loader == ClassLoader.getPlatformClassLoader();
}
//获取平台class loader
public static ClassLoader getPlatformClassLoader() {
SecurityManager sm = System.getSecurityManager();
ClassLoader loader = getBuiltinPlatformClassLoader();
if (sm != null) {
checkClassLoaderPermission(loader, Reflection.getCallerClass());
}
return loader;
}
PLATFORM_LOADER = new PlatformClassLoader(BOOT_LOADER);
为了绕过这个限制,我们可以不使用unsafe内部的这个方法,而是使用反射的方式:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
//获取内部属性
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
//直接使用反射
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
//id相对Student类的偏移量
long id = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
long name = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));
Student student = new Student();
unsafe.compareAndSwapInt(student, id, 0, 1);
unsafe.compareAndSwapObject(student, name, null, "二牛");
System.out.println(student.toString());
}
由于Unsafe类提供的方法都是非常底层和危险的,一旦使用不当,可能会导致JVM崩溃或者数据不一致等严重问题,因此,对于大多数Java开发者来说,都不建议直接使用Unsafe类。
AtomicInteger本质上就是通过unsafe去操作一个int值,在对int进行修改的时候,循环无锁的方式。
public class AtomicTest {
private Unsafe unsafe;
private int value; //初始值
private long offset; //value字段对应本类的偏移量
public AtomicTest(int value) throws NoSuchFieldException, IllegalAccessException {
//在构造函数中创建unsafe对象
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
this.unsafe = (Unsafe) theUnsafe.get(null);
//设置偏移量
this.offset = unsafe.objectFieldOffset(AtomicTest.class.getDeclaredField("value"));
//设置初始value值
this.value = value;
}
public void increment(){
//value就是旧的值,value+1是新的值
while(!unsafe.compareAndSwapInt(this, offset, value, value + 1)){
}
}
//获取value
public int get(){
return value;
}
}