欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

同步数据结构之原子类序章 博客分类: Java 线程

程序员文章站 2024-03-16 18:19:58
...

概述

关于Java并发包的原子相关类其实可以在Unsafe后面紧接着进行介绍,因为这些原子类其实只是建立在对Unsafe的调用基础上并没有利用到Java并发包的其他类,出于前面介绍的同步组件在Java并发包的重要性,我才将它们放在了原子类的前面。

Java从JDK 1.5开始提供的java.util.concurrent.atomic原子包给开发人员提供了一组用法简单、性能高效、线程安全的更新单个变量的方法。并且相对于使用传统的synchronized关键字的方式,原子包从使用者层面上看,是一种更加轻量级、细粒度、无锁的保证线程安全的策略。

因为原子包其实采用的是CAS + Volatile关键字的方式实现的这些无锁高效的线程安全机制,而CAS机制实际上是直接借助了当前平台的CPU的相关高效的原子指令来完成的,而在某些平台的CPU架构上原子包在实现的时候在内部还是借助了某种形式的内部锁的思想,所以原子包中的那些看似无锁的线程安全的方法还是具有一定程度的平台相关性,在某些平台上不能绝对保证线程不被阻塞。

 

原子包中的类可以分为5类:

  1. 原子更新标量类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
  2. 原子更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  3. 原子更新字段类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
  4. 原子更新复合类:AtomicMarkableReference,将一个布尔值与引用变量关联起来,可用于标记数据结构中的节点等;AtomicStampedReference,将一个整数值与引用变量关联起来,可用于添加更新版本号解决ABA问题
  5. JDK8新增的高竞争下的累加器及其扩展类:Striped64,LongAccumulator,LongAdder,DoubleAccumulator,DoubleAdder

原子包中的大部分类及其实现方法都包含或利用如下几个重要方法:

  1. get:由于操作的变量被volatile修饰,所以具有读取 volatile 变量的内存效果,总是能够获取到最新的值。
  2. set由于操作的变量被volatile修饰,所以具有写入volatile 变量的内存效果,立即将更新刷新到主存对其他线程可见。 
  3. lazySet即“延迟可见写”,直接使用的Unsafe的putOrderedXXX(Object o, long offset, long x)方法实现,该方法在Unsafe章节已经详细分析过,它能够执行高效的写入,但不保证更改立即对其他线程可见,而且由于在前面加上了store-store内存屏障,所以能够保证前面的写操作不会被重排序到lazySet的后面去。
  4. compareAndSet、getAndIncrement等读写方法compareAndSet是CAS机制的实现方法,保证了操作的原子性,当操作的变量是被volatile修饰的时候,它具备了volatile变量读写操作的内存效果,原子包中操作的属性都是被volatile修饰的,所以原子包中的CAS操作都具有读写volatile变量的内存效果,也就说同时满足了可见性、有序性以及原子性。除此之外,getAndIncrement等其他读写方法都具有相同的内存效果。
  5. weakCompareAndSet我们知道compareAndSet同时具有volatile变量的内存效果,确切是说,它除了能保证对自身变量的读写操作的原子性与有序性、可见性外,还能因为对volatile变量的写操作会在其前后加入相应的内存屏障达到使其前面的其他写和后面的读操作不但不能重排序而且还会跟着变得立即可见,也就是说compareAndSet + volatile同时保证了在该操作前后的其他读写操作的有序性和可见性,即它们满足happen-before规则。而原子包提供的该weakCompareAndSet方法则被称之为不保证有序性的compareAndSet方法,也就是说weakCompareAndSet只保证对自身操作的原子性和可见性,但不保证在该操作前面和后面的其他读写操作的有序性和可见性,即不附带任何的happen-before规则,更通俗的说就是,weakCompareAndSet只保证自身的原子性和可见性,在它前面和后面的其他读写操作可能会被重排序,所以当其他线程能够看到weakCompareAndSet操作的结果时,并不保证一定也能看到在weakCompareAndSet操作前面或者后面的操作的结果。一般来说,weakCompareAndSet都比compareAndSet更加高效,常常用于在程序运行时更新计数器或者统计数据这类无关于其他happens-before的程序中非常有用。另外,weakCompareAndSet操作可能会返回一个虚假的失败结果false,这种失败可能是由于内存冲突导致的,而和预期值(expectedValue)是否和当前的值相等无关。最后,通过查看原子包的源码,你会发现weakCompareAndSet和compareAndSet方法的实现是一模一样的,这是因为weakCompareAndSet是由JVM在运行时通过JIT即时编译的平台相关的手写汇编程序替换执行的,所以只有在运行时通过hsdis插件和JVM参数-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly打印出真正执行的汇编指令才会发现他们的差异(奇怪的是我在测试的时候,发现该测试程序需要循环多次调用才能看到相应的汇编指令)。值得一提的是,在JDK9中,(weak)compareAndSet这两个方法已经被加上了一个@HotSpotIntrinsicCandidate注解,更加明确的表示这两个方法可能( 但不保证 )通过HotSpot VM自己来写汇编或IR编译器来实现该方法以提供性能。

原子类主要被设计为用于实现非阻塞数据结构和相关基础结构类。compareAndSet方法也不是锁的常规替换方法,仅当对象的重要更新限定于单个变量时才使用它。

原子类也不是java.lang.Integer 和相关类的常规替换。原子类没有定义诸如 equals、hashCode 和 compareTo 之类的方法。(因为原子变量是期望可变的,所以对于哈希表的键值来说,它们不是好的选择。)另外,原子包仅为一些常用的类型提供了原子类。例如,并没有表示 byte 的原子类。如果要实现这种不常见类型的原子类,可以使用 AtomicInteger 来保存 byte 值,并进行适当的强制转换。也可以使用 Float.floatToRawIntBits(float) 和 Float.intBitsToFloat(int) 转换来保存 float 类型的值,使用 Double.doubleToRawLongBits(double) 和 Double.longBitsToDouble(long) 转换来保存 double 类型的值。

 

本章节从整体到局部重要方法的形式阐述了Java并发包的atomic原子类的设计思想以及原子类家族的5种成员结构,接下来的章节我将分别对这5种原子类进行分析研究。