关于java并发的三大特性,原子性、可见性、有序性关乎线程安全问题的基本原理,JDK提供java.util.concurrent.atomic包来对数据类型进行包装,以实现各数据类型的原子性操作。
关于三大特性与volatile关键字可参考文章深入理解volatile关键字。
接下来就通过一道简单的题目层层解读AtomicInteger的底层原理。
demo1:i++的原子性问题
public class AtomicIntegerDemo1 {
private int value = 0;
public void add() {
value++;
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo1 atomicIntegerDemo1 = new AtomicIntegerDemo1();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicIntegerDemo1.add();
}
}).start();
}
Thread.sleep(1000);//等待所有线程执行完毕再输出
System.out.println(atomicIntegerDemo1.value);
}
}
这是一道简单的题目,创建20个线程,每个线程对共享变量value进行1000次自加操作。不论运行多少次,结果都不是我们期望的20000:
究其原因,假设现在value值为1,线程1将变量value的值从堆空间(线程共享)读取到虚拟机栈(线程私有)中进行自加操作value值变为2,再将新值2刷新到堆内存。这个过程中可能线程1还没来得及刷新到堆内存,线程2也读取value值到自己的栈内存,然后将新值2刷新到堆内存。这样两个线程对变量value一共自加两次,value值应当是3,而实际value的值仍然是2。这就是线程安全问题。本质原因还是因为value++操作是非原子操作。
要实现线程安全方法有两种:
- 同步阻塞:通过sychronized关键字或者Lock对象加锁,
public void sychronized add() { value++; }
,保证add()方法的原子性。同步阻塞方式优点是安全,实现简单,缺点是并发效率低。 - 非阻塞并发:通过CAS(比较并交换)机制实现乐观锁,只有当当前值与期望值一样时,才将旧值替换为当前值,否则重新读取当前值,这种循环重试机制也叫自旋锁。异步并发的优点是性能高,CPU利用率高,线程持续运行,省去了CPU线程调度的性能消耗,缺点是如果当前值一直不与期望值一样,将会一直循环重试。乐观锁还可能出现ABA问题。
以上代码的运行结果并不会是我们期望的20000。将add()方法设置为同步方法可以解决问题,那再来看看AtomicInteger如何解决原子性问题的。
注意:以上代码中给value属性添加volatile关键字并不能解决问题,因为volatile关键字只有保证可见性和有序性的语义,而不能保证原子性。
demo2:使用AtomicInteger类
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerDemo2 {
private AtomicInteger value = new AtomicInteger();
public void add() {
value.getAndAdd(1);
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo2 atomicIntegerDemo2 = new AtomicIntegerDemo2();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicIntegerDemo2.add();
}
}).start();
}
Thread.sleep(1000);//等待所有线程执行完毕再输出
System.out.println(atomicIntegerDemo2.value);
}
}
用AtomicInteger类对象的getAndAdd()方法代替int类型的i++操作,最后输出结果是我们期望的20000.
那么,AtomicInteger是如何实现原子性运算的呢。点开jdk源码
查看AtomicInteger的getAndAdd()方法:
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
有一个unsafe对象,
private static final Unsafe unsafe = Unsafe.getUnsafe();
这个unsafe对象所属类Unsafe提供了许多native方法,比如
public native int getIntVolatile(Object var1, long var2);
用getIntVolatile()在底层通过对象(var1)和偏移量(var2)获取变量内存地址,直接通过内存地址而不是符号引用得到变量的当前值。有了对象(var1),属性地址偏移量(var2),旧值(var5 ),加上期望值(var5 + var4),就可以调用CAS操作(compareAndSwapInt方法)。unsafe对象的getAndAddInt方法如下:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);//第4行
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//第5行
return var5;
}
当当前线程执行第4行后,变量值被其他线程刷新,则当前线程执行第5行方法返回false,重新执行第4行获取值,直到当前值与期望值一样,才修改变量的值并返回新值。
看完了源码想仿造源码自己动手写一下AtomicInteger以加深印象。
demo3:仿造源码实现的AtomicInteger类
import sun.misc.Unsafe;
public class AtomicIntegerDemo3 {
private volatile int value;
//提供底层CAS操作的对象
private static final Unsafe unsafe=Unsafe.getUnsafe();
//属性在内存中相对于对象的地址偏移量
private static final long valueOffset;
static{
try {
//通过unsafe对象提供的方法获取value属性偏移量
valueOffset = unsafe.objectFieldOffset(AtomicIntegerDemo3.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
/**
* CAS操作方法
* @param current 旧值
* @param update 期望值
* @return 修改是否成功
*/
public boolean compareAndSet(int current,int update){
return unsafe.compareAndSwapInt(this,valueOffset,current,update);
}
//相当于AtomicInteger中getAndAdd()方法:将给定的值原子地添加到当前值
public void add() {
int current;
do {
//线程获取当前值
current = unsafe.getIntVolatile(this,valueOffset);
}while (!compareAndSet(current,current+1));//修改失败则重试
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo3 atomicIntegerDemo3 = new AtomicIntegerDemo3();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicIntegerDemo3.add();
}
}).start();
}
Thread.sleep(1000);//等待所有线程执行完毕再输出
System.out.println(atomicIntegerDemo3.value);
}
}
想法很简单,拿到底层内存地址操作相关的底层Unsafe类的实例,根据本类的value属性用unsafe对象的objectFieldOffset()方法获取属性相对本类实例的地址偏移量,然后通过循环CAS操作改变属性值。
但是运行时报了异常:
第8行private static final Unsafe unsafe=Unsafe.getUnsafe();
报错,点开getUnsafe()方法,
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
再点开VM.isSystemDomainLoader()方法:
public static boolean isSystemDomainLoader(ClassLoader var0) {
return var0 == null;
}
可以看出为直接调用getUnsafe()方法是不安全的,sunjdk设定只要调用类类加载器不为空,则抛出异常并提示Unsafe,也就是该方法只能虚拟机自己调用。
既然我们不能直接调用getUnsafe()方法来获取Unsafe的实例,那就用反射来获取吧。
demo4:手写AtomicInteger
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class AtomicIntegerDemo4 {
private volatile int value;
//提供底层CAS操作的对象
private static final Unsafe unsafe;
//属性在内存中相对于对象的地址偏移量
private static final long valueOffset;
static{
try {
//getUnsafe()方法不好使,通过反射获取unsafe对象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
//Unsafe类中的theUnsafe属性为private,设置可访问权限
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
//通过unsafe对象提供的方法获取value属性偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicIntegerDemo4.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
/**
* CAS操作方法
* @param current 旧值
* @param update 期望值
* @return 修改是否成功
*/
public boolean compareAndSet(int current,int update){
return unsafe.compareAndSwapInt(this,valueOffset,current,update);
}
//相当于AtomicInteger中getAndAdd()方法
public void add() {
int current;
do {
//线程获取当前值
current = unsafe.getIntVolatile(this,valueOffset);
}while (!compareAndSet(current,current+1));//修改失败则重试
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo4 atomicIntegerDemo4 = new AtomicIntegerDemo4();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicIntegerDemo4.add();
}
}).start();
}
Thread.sleep(1000);//等待所有线程执行完毕再输出
System.out.println(atomicIntegerDemo4.value);
}
}
修改过后通过反射获取unsafe对象,运行成功,能够正确输出20000:
虽然只是完成了getAndAdd()一个方法,但是通过阅读源码和仿造手写对Atomic原子类的原理有了一定的了解。
另外,测试发现,把value属性的volatile关键字去掉也不影响结果,jdk源码添加的关键字应该是不会多余的,猜测是与unsafe.getIntVolatile()这个方法有关,但是这是个native方法不能查看。
望有大神不吝赐教0.0