一、Java中提供的原子类
1.1 基本类型
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
1.2 数组类型
- AtomicIntegerArray:整型数组原子类
- AtomicLongArray:长整型数组原子类
- AtomicReferenceArray :引用类型数组原子类
1.3 引用类型
- AtomicReference:引用类型原子类
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段
- AtomicMarkableReference :原子更新带有标记位的引用类型
1.4 对象的属性修改类型
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- AtomicStampe1dReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原1子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
1.5 Java8新增的原子类
Striped64 以下类的父类
- DoubleAccumulator:double类型 累加器
- LongAccumulator :long类型累加器
- DoubleAdder :double类型 加法器
- LongAdder : long类型加法器
具体使用请查看api 文档
二、ABA问题的解决
对于CAS 算法中存在的ABA问题, 可以采用版本号的方式解决,这里看看AtomicStampe1dReference是怎么解决ABA问题的?
2.1 什么是ABA问题
- ABA问题:一个线程将内存值从A改为B,另一个线程又从B改回到A
- 图示
2.2 ABA 使用版本号解决
- ABA问题重现
``java
AtomicInteger atomicInteger = new AtomicInteger(10);new Thread(() -> {
atomicInteger.compareAndSet(10, 11);
System.out.println("线程:" + Thread.currentThread().getName() + " 设置值:" + atomicInteger.get());
}).start();
new Thread(() -> {
atomicInteger.compareAndSet(11, 10);
System.out.println("线程:" + Thread.currentThread().getName() + " 设置值:" + atomicInteger.get());
}).start();
new Thread(() -> {
//值已经修改过,但线程没有察觉到,依旧修改为新值12
atomicInteger.compareAndSet(10, 12);
System.out.println("线程:" + Thread.currentThread().getName() + " 设置值:" + atomicInteger.get());
}).start();
输出结果 线程:Thread-0 设置值:11 线程:Thread-1 设置值:10 线程:Thread-2 设置值:12
- 使用AtomicStampe1dReference解决
```java
AtomicStampedReference atomicStampe1dReference = new AtomicStampedReference(10, 1);
new Thread(() -> {
//获取版本号
int stamp = atomicStampe1dReference.getStamp();
boolean isSuccess = atomicStampe1dReference.compareAndSet(10, 11, stamp, stamp + 1);
System.out.println("线程:" + Thread.currentThread().getName() + " 当前版本:" + stamp + " 是否设置成功:" + isSuccess + " 当前值" + atomicStampe1dReference.getReference());
}).start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
int stamp = atomicStampe1dReference.getStamp();
boolean isSuccess = atomicStampe1dReference.compareAndSet(11, 10, stamp, stamp + 1);
System.out.println("线程:" + Thread.currentThread().getName() + " 当前版本:" + stamp + " 是否设置成功:" + isSuccess + " 当前值" + atomicStampe1dReference.getReference());
}).start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
int stamp = atomicStampe1dReference.getStamp();
boolean isSuccess = atomicStampe1dReference.compareAndSet(10, 12, 1, stamp + 1);
System.out.println("线程:" + Thread.currentThread().getName() + " 当前版本:" + stamp + " 是否设置成功:" + isSuccess + " 当前值" + atomicStampe1dReference.getReference());
}).start();
输出结果
线程:Thread-0 当前版本:1 是否设置成功:true 当前值11
线程:Thread-1 当前版本:2 是否设置成功:true 当前值10
线程:Thread-2 当前版本:3 是否设置成功:false 当前值10
2.3 代码查看
内部主要使用内部类Pair 保存引用和版本号
private static class Pair<T> {
final T reference;//引用
final int stamp;//版本号
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
在进行CAS 比较时,分别对引用和版本好进行一起比较, casPair 方法底层使用了Unsafe类
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
三、底层实现
3.1 疑问
- Unsafe 是什么?
- 伪共享概念 “false sharing”,@sun.misc.Contended?
3.2 Unsafe 类
详情看 Java魔法类:Unsafe应用解析
Unsafe 类的获取实例
利用反射
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
System.out.println(unsafe);
使用jvm 参数-Xbootclasspath ,使得被引导类加载器加载,从而通过
Unsafe.getUnsafe
方法安全的获取Unsafe实例。
- 编写一个类 ```java
public class UnSafeTest { public static void main(String[] args) { Unsafe unsafe = Unsafe.getUnsafe(); System.out.println(“调用Unsafe.getUnsafe() ==>” + unsafe); } }
- 编译打包<br />
```shell
#编译
javac ./UnSafeTest.java
#打包, 打一个可执行的jar 包
#-c 创建新档案
#-f 指定档案文件名, 即jar包名称
#-v 在标准输出中生成详细输出, 输出打包日志
#-e 为捆绑到可执行 jar 文件的独立应用程序,指定应用程序入口点
#jar -cvfe jar包名称 运行主类 class文件
jar -cvfe UnSafeTest.jar UnSafeTest UnSafeTest.class
- 运行
java -Xbootclasspath/a:./UnSafeTest.jar UnSafeTest
#输出
#调用Unsafe.getUnsafe() ==>sun.misc.Unsafe@c387f44
3.3 伪共享(false sharing)
什么是伪共享
缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享
伪共享图示
Java8 以前解决伪共享的方法— 使用变量填充
- 具体例子 PaddedAtomicLong
public class SomePopularObject {
public volatile long usefulVal;//实际的变量
public volatile long t1, t2, t3, t4, t5, t6, t7 = 1L; //填充的变量
public long preventOptmisation() {
return t1 + t2 + t3 + t4 + t5 + t6 + t7;
}
}
Java8 后引用注解 @sun.misc.Contended 在 JEP 142
该注解可以让你摆脱定义一些死变量。注解使用的策略——对于被该注解修饰的字段,将会被分配128bytes 空间。
常见的缓存行一般只需要64bytes, 而分配128bytes 主要是因为 “instruction prefetcher(指令预取器)”。 根据维基百科,“当处理器在实际需要之前从主存储器请求指令或数据块时,就会发生预取。”,预取可能同时占用两个高速缓存行,因此我们必须分配能填充两个缓存行的大小。
public class SomePopularObject {
@sun.misc.Contended
public volatile long usefulVal;
public volatile long anotherVal;
}
重温Java 对象的内存布局
- 内存布局
Mark Word 图示
使用JOL 查看内存布局
引入依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
- 查看(加上虚拟机参数 -XX:-RestrictContended )
public class SomePopularObject {
@sun.misc.Contended
public volatile long usefulVal;
public volatile long anotherVal;
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(SomePopularObject.class).toPrintable());
}
}
//虚拟机参数:
//-XX:-RestrictContended
//结果
concurrency.progammingArt.chapter7.SomePopularObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap)
16 8 long SomePopularObject.anotherVal N/A
24 128 (alignment/padding gap)
152 8 long SomePopularObject.usefulVal N/A
Instance size: 160 bytes
Space losses: 132 bytes internal + 0 bytes external = 132 bytes total
//虚拟机参数:
//-XX:-RestrictContended
//-XX:ContendedPaddingWidth=256
//结果
concurrency.progammingArt.chapter7.SomePopularObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap)
16 8 long SomePopularObject.anotherVal N/A
24 256 (alignment/padding gap)
280 8 long SomePopularObject.usefulVal N/A
Instance size: 288 bytes
Space losses: 260 bytes internal + 0 bytes external = 260 bytes total
-XX:ContendedPaddingWidth: 设置填充的大小为256 byte
-XX:-RestrictContended: 开启@sun.misc.Contended,进行填充
使用JMH 测试 @sun.misc.Contended 带来的性能提升