之前我们学到volatile关键字,无论是基本数据类型还是引用数据类型,只要被volatile关键字修饰,从JMM的角度分析,该变量就具备了有序性和可见性这两个语义特质,但其无法保证原子性。
原子性是指某个操作或者一些列操作要么都成功要么都失败,不允许出现因终端而导致的部分成功或部分失败的情况出现。
比如,对int类型的加法操作就是原子性的,如x+1,但是我们在使用的过程中往往会将x+1的结果赋予另一个变量甚至x变量本身,即x=x+1或x++这样的操作,而这样的语句事实上是由若干个原子性的操作组合而来的,因此它们不具备原子性。
具体实现步骤如下:
long类型的加法x+1的操作不是原子性的,一个64位写操作实际上将会被拆分为2个32位的操作,这一行为的直接后果将会导致最终的结果是不确定的并且缺少原子性的保证。
首先我们对比一下synchronized关键字、显示锁Lock修饰的int以及AtomicInteger类型在多线程场景下的性能表现。
SynchronizedVsLockVsAtomicInteger.java
package com.myf.concurrent.wwj2.jmh;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.profile.StackProfiler;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.TimeValue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zgx
*/
@Measurement(iterations = 100, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@Warmup(iterations = 100, time = 100, timeUnit = TimeUnit.MILLISECONDS)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class SynchronizedVsLockVsAtomicInteger {
/**
* 状态对象为线程组共享
* 使用显示锁Lock进行共享资源同步
* 使用synchronized关键字进行共享资源同步
*/
@State(Scope.Group)
public static class IntMonitor {
private int x;
private final Lock lock = new ReentrantLock();
// 使用显式锁Lock进行共享资源同步
public void lockInc() {
lock.lock();
try {
x++;
} finally {
lock.unlock();
}
}
// 使用synchronized关键字进行共享资源同步
public void synInc() {
synchronized (this) {
x++;
}
}
}
/**
* 状态对象为线程组共享
* 直接使用AtomicInteger
* 使用AtomicInteger进行共享资源同步
*/
@State(Scope.Group)
public static class AtomicIntegerMonitor {
private AtomicInteger x = new AtomicInteger();
public void inc() {
x.incrementAndGet();
}
}
/**
* 线程组sync,10个线程
* @param monitor IntMonitor状态对象
*/
@GroupThreads(10)
@Group("sync")
@Benchmark
public void syncInc(IntMonitor monitor) {
monitor.synInc();
}
/**
*
* 线程组lock,10个线程
* @param monitor IntMonitor状态对象
*/
@GroupThreads(10)
@Group("lock")
@Benchmark
public void lockInc(IntMonitor monitor) {
monitor.lockInc();
}
/**
*
* 线程组atomic,10个线程
* @param monitor AtomicIntegerMonitor状态对象
*/
@GroupThreads(10)
@Group("atomic")
@Benchmark
public void atomicIntegerInc(AtomicIntegerMonitor monitor) {
monitor.inc();
}
public static void main(String[] args) throws RunnerException {
Options opts = new OptionsBuilder()
.include(SynchronizedVsLockVsAtomicInteger.class.getSimpleName())
.forks(1)
.timeout(TimeValue.seconds(10))
// .addProfiler(StackProfiler.class)
.build();
new Runner(opts).run();
}
}
基准测试输出结果如下
Benchmark Mode Cnt Score Error Units
SynchronizedVsLockVsAtomicInteger.atomic avgt 100 0.416 ± 0.007 us/op
SynchronizedVsLockVsAtomicInteger.lock avgt 100 0.209 ± 0.029 us/op
SynchronizedVsLockVsAtomicInteger.sync avgt 100 0.990 ± 0.006 us/op
我们可以看出AtomicInteger > 显示锁Lock > synchronized关键字
我们打开StackProfile的设置
....AtomicInteger..........................................................
73.2% RUNNABLE
19.0% WAITING
7.7% TIMED_WAITING
....Lock....................................................................
68.7% WAITING
23.6% RUNNABLE
7.7% TIMED_WAITING
....synchronized............................................................
48.7% BLOCKED
29.5% RUNNABLE
14.1% WAITING
7.7% TIMED_WAITING
AtomicInteger的RUNNABLE状态达到73.2%,且没有BLOCKED状态
synchronized则相反BLOCKED高达48.7%
显而易见AtomicInteger的性能是最优的。
与int的引用类型Integer继承Number类一样,AtomicInteger也是Number类的一个子类,此外,AtomicInteger还提供了很多原子性的操作。
在AtomicInteger的内部有一个被volatile关键字修饰的成员变量value,实际上AtomicInteger所提供的所有方法主要都是针对该变量value进行的操作。
public AtomicInteger(): 创建AtomicInteger的初始值为0。public AtomicInteger(int initalValue): 创建AtomicInteger并且指定初始值,无参等同于初始值为0。Int getAndIncrement(): 返回当前int类型的value值,然后对value进行自增运算,该方法能够保证value的原子性增量操作。int incrementAndGet(): 直接返回自增后的结果。该方法能够保证value的原子性增量操作。int getAndDecrement(): 返回当前int类型的value值,然后对value进行自减运算,该操作方法能够保证value的原子性减量操作。intdecrementAndGet(): 直接返回自减后的结果。该操作方法能够保证value的原子性减量操作。boolean compareAndSet(int expectedValue, int newValue): 原子性的更新AtomicInteger的值,其中expect代表当前当前的AtomicInteger数值,update则是需要设置的新值,该方法会返回一个Boolean的结果:当expect和AtomicInteger的当前值不相等时,修改会失败,返回值为false;修改成功则会返回true。boolean weakCompareAndSetPlain(int expectedValue, int newValue):含义同上,底层没看懂int getAndAdd(int delta):原子性的更新value值,更新后的value为value和delta之和,方法的返回值为value的前一个值,该方法是基于自旋+CAS算法实现的(Compare And Swap)原子性操作。int addAndGet(int delta):原子性的更新value值,更新后的value为value和delta之和,方法的返回值为value的当前值,自JDK1.8增加了函数式接口之后,AtomicInteger也提供了函数式接口的支持。
int getAndUpdate(IntUnaryOperator updateFunction):原子性的更新value值,方法入参为IntUnaryOperator接口,返回值为value更新之前的值。
int updateAndGet(IntUnaryOperator updateFunction):原子性地更新AtomicInteger的值,方法入参为IntUnaryOperator接口,该方法返回更新后的value值。
int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction):原子性地更新AtomicInteger的值,方法入参为IntBinaryOperator接口和delta值x,返回值为更新之前的值。
@FunctionalInterface
public interface IntBinaryOperator {
/**
* 该接口在getAndAccumulate方法中,left为AtomicInteger value的当前值。
* right为delta值,返回值被用于更新AtomicInteger的value值
*/
int applyAsInt(int left, int right);
}
int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction):该方法与getAndAccumulate类似,不过该方法返回更新之后的值。
void set(int newValue):为AtomicInteger的value设置一个新值,AtomicInteger中有一个被volatile关键字修饰的value成员属性,因此调用Set方法为value设置新值后其他线程就会立即看到。void lazySet(int newValue):set方法修改被volatile关键字修饰的value值会被强制刷新到主内存中,从而立即被其他线程看到,这一切都要归功于volatile关键字底层的内存屏障。内存屏障虽然足够轻量,但毕竟还是会带来性能上的开销,比如在单线程中对AtomicInteger的value进行修改时,没有必要保留内存屏障,而value被volatile关键字修饰,这似乎是不可调和的矛盾。幸好追求性能机制的JVM开发者们早就考虑到了这一点,lazySet方法的作用正在于此。❓❓❓CAS包括3个操作数:内存值V、旧的预期值A、要修改的新值B。当且仅当预期值A与内存值V相等时,将内存值V修改为B,否则什么都不需要做。
compareAndSetInt方法是一个native方法,提供了CAS(Compare And Set)算法的实现AtomicInteger类中的原子性方法几乎都借助于该方法实现。
/**
*
* @param o 该入参是宿主偏移量所在的宿主对象
* @param offset 该入参是o对象某属性的地址偏移量,是由Unsafe对象获得的
* @param expected 该值是我们期望value当前的值,如果expected的值和当前的值不一样,
* 说明该值已经被其他线程修改,则修改失败,返回false
* @param x 新值
* @return 是否修改成功
*/
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
public final boolean compareAndSet(int expectedValue, int newValue) {
/**
* 这里this是当前对象,
* VALUE给的注释为:报告具有给定名称的字段在其类的存储分配中的位置
*/
return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}
通过compareAndSet我们大概了解了其字段含义。
可是,expected一般都是获取的当前对象的值,为什么会存在expected和当前值不相等的情况?
AtomicInteger ai = new AtomicInteger(2);
ai.compareAndSet(ai.get(),10);
原因是相对于synchronized关键字、显示锁Lock、AtomicInteger提供的方法不具备排他性,当A线程通过get()方法获取了AtomicInteger value当前值后,B线程对value的修改已经完成,A线程试图修改的时候就会出现expected和当前值不一致的情况,因此会修改失败,这种情况也被称为乐观锁。
由于compareAndSetInt方法的乐观锁特性,会存在对value修改失败的情况,但有些时候对value的更新必须要成功,比如addAndGet方法
public final int getAndAdd(int delta) {
return U.getAndAddInt(this, VALUE, delta);
}
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// 1
v = getIntVolatile(o, offset);
// 2
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
在getAndAddInt方法中有一个do…while循环控制语句,首先在注释1处获取当前被volatile关键字修饰的value值(通过内存偏移量的方式读取内存)。
在注释2处执行compareAndSetInt方法,执行成功则返回,不成功执行下一轮的处理。
通过上面源码的分析,incrementAndGet 的执行结果有可能是11也有可能是比11更大的值。
AtomicInteger ai = new AtomicInteger(10);
//这句断言在多线程的情况下未必会成功
assert ai.incrementAndGet() == 11;
AtomicBoolean提供了一种原子性地读写布尔类型变量的解决方案,通常情况下,该类将被用于原子性地更新状态标识位,比如flag。
public AtomicBoolean():无参构造,等价于public AtomicBoolean(false)。boolean compareAndSet(boolean expectedValue, boolean newValue):期望值与Atomic Boolean的当前值一致时执行新值的设置动作,若设置成功则返回true,否则直接返回false。boolean weakCompareAndSet(boolean expectedValue, boolean newValue):同上void set(boolean newValue):设置AtomicBoolean最新的value值,该新值的更新对其他线程立即可见。boolean getAndSet(boolean newValue):返回AtomicBoolean的前一个布尔值,并且设置新的值。void lazySet(boolean newValue):同上。get():获取AtomicBoolean的当前布尔值。AtomicBoolean的实现方式比较类似于AtomicInteger类,实际上AtomicBoolean内部的value本身就是一个volatile关键字修饰的int类型的成员属性。
private volatile int value;
与AtomicInteger非常类似,AtomicLong提供了原子性操作long类型数据的解决方案,AtomicLong同样也继承自Number类,AtomicLong所提供的原子性方法在使用习惯上也与AtomicInteger非常一致。
AtomicInteger类中最为关键的方法为compareAndSetInt ,对于该方法,2.3节的第1小节中已经进行了非常详细的分析,同样,在AtomicLong类中也提供了类似的方法compareAndSetLong
AtomicReference类提供了对象引用的非阻塞原子性读写操作,并且提供了其他一些高级的用法。众所周知,对象的引用其实是一个4字节的数字,代表着在JVM堆内存中的引用地址,对一个4字节数字的读取操作和写入操作本身就是原子性的,通常情况下,我们对对象引用的操作一般都是获取该引用或者重新赋值(写入操作),我们也没有办法对对象引用的4字节数字进行加减乘除运算,那么为什么JDK要提供AtomicReference类用于支持引用类型的原子性操作呢?
举一个有意思的小例子:
10个线程抢购商品,只有一个商品存在,只有旧商品被购买,新商品才创建
/**
* 10个线程抢购商品,只有一个商品存在,只有旧商品被购买,新商品才创建
* @author myf
*/
public class AtomicReferenceExample {
public static void main(String[] args) {
AtomicReference<Product> reference = new AtomicReference<>();
// 创建第一个商品
reference.set(new Product("iphone", 5000, new Date()));
// 10个线程抢购商品
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Product product;
do {
// 拿到商品链接
product = reference.get();
// 抢购失败,从新获取商品链接
}while (!reference.compareAndSet(product, product.createNewProduct()));
// 抢购成功,购买商品
product.buy();
}).start();
}
}
}
class Product {
String name;
int price;
/**
* 过期时间
*/
Date expirationTime;
public Product(String name, int price, Date expirationTime) {
this.name = name;
this.price = price;
this.expirationTime = expirationTime;
}
public Product createNewProduct() {
return new Product(this.name + 1, this.price, new Date(System.currentTimeMillis() + 1000));
}
public void buy() {
System.out.println(Thread.currentThread().getId() + "购买商品" + this.name);
}
}
ABA问题,即数据A被其他线程修改为B后又修改为A,CAS会认为值未发生变化。
我们通常采用增加版本号的方式来避免CAS算法中的ABA问题。
在Java原子包中也提供了这样的实现AtomicStampedReference<。
AtomicStampedReference在构建的时候需要一个类似于版本号的int类型变量stamped,每一次针对共享数据的变化都会导致该stamped的增加(stamped的自增维护需要应用程序自身去负责,AtomicStampedReference并不提供),因此就可以避免ABA问题的出现,AtomicStampedReference的使用也是极其简单的,创建时我们不仅需要指定初始值,还需要设定stamped的初始值,在AtomicStampedReference的内部会将这两个变量封装成Pair对象,代码如下所示。
public class AtomicStampedReference<V> {
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);
}
}
private volatile Pair<V> pair;
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
}
public AtomicStampedReference(V initialRef, int initialStamp):在创建AtomicStampedReference时除了指定引用值的初始值之外还要给定初始的stamp。
getReference():获取当前引用值,等同于其他原子类型的get方法。
getStamp():获取当前引用值的stamp数值。
public V get(int[] stampHolder) :看源码理解吧,源码也有不优雅的操作。
public V get(int[] stampHolder) {
Pair<V> pair = this.pair;
stampHolder[0] = pair.stamp;
return pair.reference;
}
Pair被定义为私有的,该方法返回了当前引用的值,并将stamp数值放入入参数组的0位置。
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp):对比并且设置当前的引用值,这与其他的原子类型CAS算法类似,只不过多了expectedStamp和newStamp,只有当expectedReference与当前的Reference相等,且expectedStamp与当前引用值的stamp相等时才会发生设置,否则set动作将会直接失败。
weakCompareAndSet (V expectedReference, V newReference, int expectedStamp, int newStamp):同上。
set(V newReference, int newStamp):设置新的引用值以及stamp。
attemptStamp(V expectedReference, int newStamp):该方法的主要作用是为当前的引用值设置一个新的stamp,该方法为原子性方法。
在Java原子包中提供了相应的原子性操作数组元素相关的类。
Demo
// 定义int类型的数组并且初始化
int[] intArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 创建AtomicIntegerArray 并且传入int类型的数组
AtomicIntegerArray intAtomicArr = new AtomicIntegerArray(intArray);
// 原子性地为intAtomicArr的第二个元素加10
assert intAtomicArr.addAndGet(1, 10) == 12;
// 第二个元素更新后值为12
assert intAtomicArr.get(1) == 12;
截止目前我们已经知道,要想使得共享数据的操作具备原子性,目前有两种方案:
synchronized提供了互斥的机制来保证在同一时刻只能有一个线程对共享数据进行操作。原子类型采用CAS算法提供的LockFree方式,允许多个线程同时进行共享数据的操作,原子类型提供了乐观的同步解决方案。
但,如果你即不想使用synchronized,又不想将数据类型声明为原子类型的,那么这个时候应该如何进行操作呢。
在Java的原子包中提供了原子性操作对象属性的解决方案。即AtomicFieldUpdater
在Java的原子包中提供了三种原子性更新对象属性的类,分别如下所示。
Demo
// 定义一个简单的类
static class Alex{
// int类型的salary,并不具备原子性的操作
volatile int salary;
public int getSalary(){
return this.salary;
}
}
public static void main(String[] args){
// ① 定义AtomicIntegerFieldUpdater,通过newUpdater方法创建,传入class对象和需要原子更新的属性名。
AtomicIntegerFieldUpdater<Alex> updater =
AtomicIntegerFieldUpdater.newUpdater(Alex.class, "salary");
// ② 实例化Alex
Alex alex = new Alex();
// ③ 原子性操作Alex类中的salary属性
int result = updater.addAndGet(alex, 1);
assert result == 1;
}
在AtomicIntegerFieldUpdater通过静态方法newUpdater 成功创建之后,就可以使用AtomicIntegerFieldUpdater的实例来实现对应class属性的原子性操作了,就像我们直接使用原子类型一样。
AtomicFieldUpdater在使用上非常简单,其内部实现原理也是很容易理解的,但是并不是所有的成员属性都适合被原子性地更新。
使用的第三方类库某个属性不是被原子性修饰
的,在多线程的环境中若不想通过加锁的方式则可以采用这种方式(当然这对第三方类库的成员属性要求是比较苛刻的,最起码得满足可被原子性更新的所有条件),另外,AtomicFieldUpdater的方式相比较直接使用原子类型更加节省应用程序的内存。