一、线程安全问题
并发编程(多线程编程)首先要解决的问题就是线程安全问题,我们常用的一些Java基本类操作在单线程执行过程中是没有问题,但是一旦到多线程环境中就会出现失常,比如:
int++存在线程安全问题,即多线程执行int++后结果会与预期有出入,不具备原子性。
public class AtomicIntegerTest {
static int num=0;
static AtomicInteger ai = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
//计数器
CountDownLatch countDownLatch = new CountDownLatch(1000);
ExecutorService executorService = Executors.newFixedThreadPool(200);
for(int i=0; i < 1000; i++){
executorService.execute(()->{
num++;
ai.getAndIncrement();
countDownLatch.countDown();
});
}
//阻塞,等待所有线程都执行完成
countDownLatch.await();
executorService.shutdown();
System.out.println("AtomicInteger result:"+ai.get());
System.out.println("Integer result:"+num);
}
}
//输出结果
AtomicInteger result:1000 (始终输出1000, 线程安全)
Integer result:998 (int输出的结果偶尔是1000,说明非线程安全)
以上例子说明全局变量int会存在线程安全问题,可以使用synchronized、Lock或AtomicInteger来解决这个问题,通过基准测试(JMH)性能结果AtomicInteger>显式锁Lock>synchronized关键字。
二、AtomicInteger源码分析
以下是AtomicInteger的大致结构,核心就是借助Unsafe的各种CAS native方法实现线程安全。
// Unsafe是由C++实现的,其内部存在着大量的汇编 CPU指令等代码,JDK实现的
// Lock Free几乎完全依赖于该类
private static final Unsafe unsafe = Unsafe.getUnsafe();
// valueOffset将用于存放value的内存地址偏移量
private static final long valueOffset;
static {
try {
// 获取value的内存地址偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 在AtomicInteger的内部有一个volatile修饰的int类型成员属性value
private volatile int value;
// AtomicInteger中的原子性方法都借助于unsafe.compareAndSwapInt方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
1. compareAndSwapInt源码分析——CAS算法
/*
* 该方法是一个native方法,需查看jdk源码(C++实现)
* 打开openjdk-jdk8u/hotspot/src/share/vm/prims/unsafe.cpp文件我们会找到相关的C++代码
* object:该入参是地址偏移量所在的宿主对象
* valueOffSet:该入参是object对象某属性的地址偏移量,是由Unsafe对象获得的
* expectValue:该值是我们期望value当前的值,如果expectValue与实际的当前
* 值不相等,那么对value的修改将会失败,方法的返回值也会变为false
* newValue:新值
*/
public final native boolean compareAndSwapInt(Object object, long valueOffSet, int expectValue, int newValue);
CAS包含3个操作数:内存值V、旧的预期值A、要修改的新值B。当且仅当预期值A与内存值V相等时,将内存值V修改为B,否则什么都不需要做,这种方式也被称为乐观锁。
2. 自旋方法addAndGet源码分析
由于compareAndSwapInt方法的乐观锁特性,会存在对value修改失败的情况,但是有些时候对value的更新必须要成功,比如调用incrementAndGet、addAndGet等方法,分析一下addAndGet方法的实现:
// AtomicInteger类中的addAndGet方法
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the updated value
*/
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
// Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object object, long valueOffset, int delta) {
int currentValue;
do {
//获取当前被volatile关键字修饰的value值(通过内存偏移量的方式读取内存)
currentValue= this.getIntVolatile(object, valueOffset);
//执行compareAndSwapInt方法,如果执行成功则直接返回,如果执行失败则再次执行下一轮的compareAndSwapInt方法
} while(!this.compareAndSwapInt(object, valueOffset, currentValue, currentValue+ delta));
return currentValue;
}
可以看到本质还是调用compareAndSwapInt方法,通过循环比对直到成功,形成自旋。
通过上面源码的分析,以下代码incrementAndGet的执行结果有可能是11也有可能是比11更大的值
AtomicInteger ai = new AtomicInteger(10);
//这句断言在多线程的情况下未必会成功
assert ai.incrementAndGet() == 11;
AtomicLong、AtomicBoolean类似。
三、AtomicReference详解
上述的AtomicInteger、AtomicLong、AtomicBoolean都是针对封装类型的原子类型,在实际业务场景中不能完全满足,比如个人的资金账户变化,就需要针对自定义对象Object的原子类型,即AtomicReference。
public class User {
private String account;
private int amount;
public User(String account, int amount) {
this.account = account;
this.amount = amount;
}
//... getter and setter
}
public class AtomicReferenceTest {
static ExecutorService executorService = Executors.newFixedThreadPool(50);
final static int loopCount = 100;
// 使用volatile线程共享
private static volatile User user = new User("Kevin", 0);
// 金额非同步增加10
public static void inc(){
user.setAmount(user.getAmount()+10);
}
private static volatile User syncUser=new User("Kevin", 0);
// 金额同步增加10
public static void syncInc(){
synchronized (AtomicReferenceTest.class){
final User u = syncUser;
final User newUser = new User(u.getAccount(), u.getAmount()+10);
syncUser = newUser;
}
}
// 使用AtomicReference具有原子性,此处使用的compareAndSet,可能会失败,所以循环处理
private static AtomicReference<User> ref=new AtomicReference<>(new User("Kevin", 0));
public static void casInc(){
boolean isOk = false;
while (!isOk){
final User u = ref.get();
final User newUser = new User(u.getAccount(), u.getAmount()+10);
isOk=ref.compareAndSet(u, newUser);
if(!isOk){
System.out.println(Thread.currentThread().getName()+",continue:"+newUser);
}
}
}
public static void testAll() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(loopCount);
for(int i=0; i < loopCount; i++){
executorService.execute(()->{
inc();
syncInc();
casInc();
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("Integer result:"+user.getAmount());
System.out.println("Sync result:"+syncUser.getAmount());
System.out.println("Atomic result:"+ref.get().getAmount());
}
public static void main(String[] args) throws InterruptedException {
testAll();
executorService.shutdown();
}
}
// 运行结果
pool-1-thread-1,continue:User{account='Kevin', amount=10} (偶尔会出现,说明cas失败了,通过循环自旋处理)
Integer result:990 (说明非线程安全)
Sync result:1000
Atomic result:1000
以上通过三种方式进行测试,普通方式进行多线程加减存在线程安全问题,通过synchronized同步锁和AtomicReference都可以实现线程安全操作。
四、AtomicStampedReference详解(解决ABA问题)
原子类型用自旋+CAS的无锁操作保证了共享变量的线程安全性和原子性,绝大多数情况下,CAS算法并没有什么问题,但是在需要关心变化值的操作中会存在ABA的问题,比如一个值原来是A,变成了B,后来又变成了A,那么CAS检查时会发现它的值没有发生变化,但是实际上却是发生了变化的。
比如账户有10元,给小于20元的账户赠送100元,只能一次:
A线程增加100元账户变成了110,B线程刚好消费减少100元账户又变成了10元,C线程无法知道账户是否有过变化又会重复赠送
如何避免CAS算法带来的ABA问题呢?针对乐观锁在并发情况下的操作,我们通常会增加版本号,在Java原子包中也提供了这样的实现AtomicStampedReference。
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 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)));
}
}
使用AtomicStampedReference进行测试
// 使用AtomicStampedReference具有原子性
private static AtomicStampedReference<User> stampRef=new AtomicStampedReference<>(new User("Kevin", 0), 1);
public static void casStampInc(){
boolean isOk = false;
while (!isOk){
final User u = stampRef.getReference();
final User newUser = new User(u.getAccount(), u.getAmount()+10);
final int stamp = stampRef.getStamp();
isOk=stampRef.compareAndSet(u, newUser, stamp, stamp+1);
if(!isOk){
System.out.println(Thread.currentThread().getName()+",stamp continue:"+newUser);
}
}
}
public static void testAll() throws InterruptedException {
System.out.println("Atomic Stamp result:"+stampRef.getReference().getAmount()+","+stampRef.getStamp());
}
//运行结果
pool-1-thread-32,stamp continue:User{account='Kevin', amount=330}
Atomic Stamp result:1000,101
如上stamp可以用来标记值的变化版本。
五、AtomicArray数组原子类型
针对数组的原子类型如下:
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
// 定义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;
六、AtomicFieldUpdater原子性更新
既不想使用synchronized对共享数据的操作进行同步,又不想将数据类型声明成原子类型的,可以使用AtomicFieldUpdater原子性操作对象属性,如AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
// 定义一个简单的类
static class Alex{
// int类型的salary,并不具备原子性的操作
volatile int salary;
public int getSalary()
{
return this.salary;
}
}
public static void main(String[] args){
// 定义AtomicIntegerFieldUpdater,通过newUpdater方法创建
AtomicIntegerFieldUpdater<Alex> updater = AtomicIntegerFieldUpdater.newUpdater(Alex.class, "salary");
// 实例化Alex
Alex alex = new Alex();
// 原子性操作Alex类中的salary属性
int result = updater.addAndGet(alex, 1);
assert result == 1;
}
AtomicFieldUpdater在使用上非常简单,但是并不是所有的成员属性都适合被原子性地更新:
(1)未被volatile关键字修饰的成员属性无法被原子性地更新
(2)类变量(static修饰)无法被原子性地更新
(3)无法直接访问的成员属性不支持原子性地更新
(4)final修饰的成员属性无法被原子性地更新
(5)父类的成员属性无法被原子性地更新
七、总结
原子类型包为我们提供了一种无锁的原子性操作共享数据的方式,无锁的操作方式可以减少线程的阻塞,减少CPU上下文的切换,提高程序的运行效率,但是这并不是一条放之四海皆准的规律,比如,同样被synchronized关键字同步的共享数据和原子类型的数据在单线程运行的情况下,synchronized关键字的效率却要高很多,究其原因是synchronized关键字是由JVM提供的相关指令所保证的,因此在Java程序运行期优化时可以将同步擦除,而原子类是由本地方法和汇编指令来提供保障的,在Java程序运行期间是没有办法被优化的。