一、Java中提供的原子类

image.png

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
  • 图示

image.png

2.2 ABA 使用版本号解决

  • ABA问题重现 ``javaAtomicInteger atomicInteger = new AtomicInteger(10);
    1. new Thread(() -> {
    2. atomicInteger.compareAndSet(10, 11);
    3. System.out.println("线程:" + Thread.currentThread().getName() + " 设置值:" + atomicInteger.get());
    4. }).start();
    5. new Thread(() -> {
    6. atomicInteger.compareAndSet(11, 10);
    7. System.out.println("线程:" + Thread.currentThread().getName() + " 设置值:" + atomicInteger.get());
    8. }).start();
    9. new Thread(() -> {
    10. //值已经修改过,但线程没有察觉到,依旧修改为新值12
    11. atomicInteger.compareAndSet(10, 12);
    12. System.out.println("线程:" + Thread.currentThread().getName() + " 设置值:" + atomicInteger.get());
    13. }).start();

输出结果 线程:Thread-0 设置值:11 线程:Thread-1 设置值:10 线程:Thread-2 设置值:12

  1. - 使用AtomicStampe1dReference解决
  2. ```java
  3. AtomicStampedReference atomicStampe1dReference = new AtomicStampedReference(10, 1);
  4. new Thread(() -> {
  5. //获取版本号
  6. int stamp = atomicStampe1dReference.getStamp();
  7. boolean isSuccess = atomicStampe1dReference.compareAndSet(10, 11, stamp, stamp + 1);
  8. System.out.println("线程:" + Thread.currentThread().getName() + " 当前版本:" + stamp + " 是否设置成功:" + isSuccess + " 当前值" + atomicStampe1dReference.getReference());
  9. }).start();
  10. new Thread(() -> {
  11. try {
  12. TimeUnit.SECONDS.sleep(1);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. int stamp = atomicStampe1dReference.getStamp();
  17. boolean isSuccess = atomicStampe1dReference.compareAndSet(11, 10, stamp, stamp + 1);
  18. System.out.println("线程:" + Thread.currentThread().getName() + " 当前版本:" + stamp + " 是否设置成功:" + isSuccess + " 当前值" + atomicStampe1dReference.getReference());
  19. }).start();
  20. new Thread(() -> {
  21. try {
  22. TimeUnit.SECONDS.sleep(2);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. int stamp = atomicStampe1dReference.getStamp();
  27. boolean isSuccess = atomicStampe1dReference.compareAndSet(10, 12, 1, stamp + 1);
  28. System.out.println("线程:" + Thread.currentThread().getName() + " 当前版本:" + stamp + " 是否设置成功:" + isSuccess + " 当前值" + atomicStampe1dReference.getReference());
  29. }).start();
  30. 输出结果
  31. 线程:Thread-0 当前版本:1 是否设置成功:true 当前值11
  32. 线程:Thread-1 当前版本:2 是否设置成功:true 当前值10
  33. 线程:Thread-2 当前版本:3 是否设置成功:false 当前值10

2.3 代码查看

内部主要使用内部类Pair 保存引用和版本号

  1. private static class Pair<T> {
  2. final T reference;//引用
  3. final int stamp;//版本号
  4. private Pair(T reference, int stamp) {
  5. this.reference = reference;
  6. this.stamp = stamp;
  7. }
  8. static <T> Pair<T> of(T reference, int stamp) {
  9. return new Pair<T>(reference, stamp);
  10. }
  11. }

在进行CAS 比较时,分别对引用和版本好进行一起比较, casPair 方法底层使用了Unsafe类

  1. public boolean compareAndSet(V expectedReference,
  2. V newReference,
  3. int expectedStamp,
  4. int newStamp) {
  5. Pair<V> current = pair;
  6. return
  7. expectedReference == current.reference &&
  8. expectedStamp == current.stamp &&
  9. ((newReference == current.reference &&
  10. newStamp == current.stamp) ||
  11. casPair(current, Pair.of(newReference, newStamp)));
  12. }

三、底层实现

3.1 疑问

  1. Unsafe 是什么?
  2. 伪共享概念 “false sharing”,@sun.misc.Contended?

    3.2 Unsafe 类

    详情看 Java魔法类:Unsafe应用解析

image.png

Unsafe 类的获取实例

  1. 利用反射

    1. Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    2. theUnsafe.setAccessible(true);
    3. Unsafe unsafe = (Unsafe) theUnsafe.get(null);
    4. System.out.println(unsafe);
  2. 使用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); } }

  1. - 编译打包<br />
  2. ```shell
  3. #编译
  4. javac ./UnSafeTest.java
  5. #打包, 打一个可执行的jar 包
  6. #-c 创建新档案
  7. #-f 指定档案文件名, 即jar包名称
  8. #-v 在标准输出中生成详细输出, 输出打包日志
  9. #-e 为捆绑到可执行 jar 文件的独立应用程序,指定应用程序入口点
  10. #jar -cvfe jar包名称 运行主类 class文件
  11. jar -cvfe UnSafeTest.jar UnSafeTest UnSafeTest.class
  • 运行
    1. java -Xbootclasspath/a:./UnSafeTest.jar UnSafeTest
    2. #输出
    3. #调用Unsafe.getUnsafe() ==>sun.misc.Unsafe@c387f44

3.3 伪共享(false sharing)

什么是伪共享

缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享

伪共享图示

image.png

Java8 以前解决伪共享的方法— 使用变量填充

  1. public class SomePopularObject {
  2. public volatile long usefulVal;//实际的变量
  3. public volatile long t1, t2, t3, t4, t5, t6, t7 = 1L; //填充的变量
  4. public long preventOptmisation() {
  5. return t1 + t2 + t3 + t4 + t5 + t6 + t7;
  6. }
  7. }

Java8 后引用注解 @sun.misc.Contended 在 JEP 142

该注解可以让你摆脱定义一些死变量。注解使用的策略——对于被该注解修饰的字段,将会被分配128bytes 空间。
常见的缓存行一般只需要64bytes, 而分配128bytes 主要是因为 “instruction prefetcher(指令预取器)”。 根据维基百科,“当处理器在实际需要之前从主存储器请求指令或数据块时,就会发生预取。”,预取可能同时占用两个高速缓存行,因此我们必须分配能填充两个缓存行的大小。

  1. public class SomePopularObject {
  2. @sun.misc.Contended
  3. public volatile long usefulVal;
  4. public volatile long anotherVal;
  5. }

重温Java 对象的内存布局

  • 内存布局

image.png

  • Mark Word 图示
    image.png

    使用JOL 查看内存布局

  • 引入依赖

  1. <dependency>
  2. <groupId>org.openjdk.jol</groupId>
  3. <artifactId>jol-core</artifactId>
  4. <version>0.9</version>
  5. </dependency>
  • 查看(加上虚拟机参数 -XX:-RestrictContended )
  1. public class SomePopularObject {
  2. @sun.misc.Contended
  3. public volatile long usefulVal;
  4. public volatile long anotherVal;
  5. public static void main(String[] args) {
  6. System.out.println(ClassLayout.parseClass(SomePopularObject.class).toPrintable());
  7. }
  8. }
  9. //虚拟机参数:
  10. //-XX:-RestrictContended
  11. //结果
  12. concurrency.progammingArt.chapter7.SomePopularObject object internals:
  13. OFFSET SIZE TYPE DESCRIPTION VALUE
  14. 0 12 (object header) N/A
  15. 12 4 (alignment/padding gap)
  16. 16 8 long SomePopularObject.anotherVal N/A
  17. 24 128 (alignment/padding gap)
  18. 152 8 long SomePopularObject.usefulVal N/A
  19. Instance size: 160 bytes
  20. Space losses: 132 bytes internal + 0 bytes external = 132 bytes total
  21. //虚拟机参数: 
  22. //-XX:-RestrictContended
  23. //-XX:ContendedPaddingWidth=256
  24. //结果
  25. concurrency.progammingArt.chapter7.SomePopularObject object internals:
  26. OFFSET SIZE TYPE DESCRIPTION VALUE
  27. 0 12 (object header) N/A
  28. 12 4 (alignment/padding gap)
  29. 16 8 long SomePopularObject.anotherVal N/A
  30. 24 256 (alignment/padding gap)
  31. 280 8 long SomePopularObject.usefulVal N/A
  32. Instance size: 288 bytes
  33. Space losses: 260 bytes internal + 0 bytes external = 260 bytes total

-XX:ContendedPaddingWidth: 设置填充的大小为256 byte
-XX:-RestrictContended: 开启@sun.misc.Contended,进行填充

使用JMH 测试 @sun.misc.Contended 带来的性能提升

参考