什么是原子操作
原子操作可以是一个步骤,也可以是多个步骤,但是其顺序不可以被打乱,也不可以被切割只执行其中的一部分(不可中断性)。
将整个操作视作一个整体,资源在该次操作中保持一致,这是原子性的核心特征
下面我们看一段代码:
package com.hzw;
public class Counter {
volatile int i = 0;
public void add() {
i++;
}
}
package com.hzw;
public class DemoCounterTest {
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
counter.add();
}
System.out.println("done...");
}
}).start();
}
Thread.sleep(6000L);
System.out.println(counter.i);
}
}
预计打印i
的结果应该是10X10000
但是实际并不是,打印的结果要比预计的小,这是为什么呢?
没错,出现了线程安全问题,因为i++
不是原子操作导致的线程安全问题。
下面我们来剖析一下,为什么
i++
不是原子性操作通过
javap -v -p Counter.class
对Counter文件进行反编译,查看add方法编译后的字节码指令
可知,如上图,i++
方法被编译成了四个字节码指令。那么这四个字节码指令是如何执行的呢?
- getfield:从堆内存中获取字段的值放入操作数栈
- Icons_1:将常量1放入操作数栈
- iadd:从操作数栈顶取出两个值求和以后再压入操作数栈
- putfield:将操作数栈里的数据去除设回到堆内存中的字段
当多个线程同时操作时线程1与线程2同时拿到
i=0
当线程1完成putfield后堆内存里``i=1与线程2拿到的
i=0不一致,不符合原子性的特征,所以
i++`不是原子性操作
原子性问题的场景
-
判断了某种状态后,这个状态失效了
if(owner == null) { owner = currentThread(); }
当线程1与线程2同时进入
if
代码块,结果线程1将owner
进行赋值,此时线程2进入if
代码块的判断状态owner == null
失效了因为此时owner != null
,造成了原子性问题 -
加载了一个值,这个值失效了
例如:上文中多个线程之间的i++
操作
如何解决
-
对需要保证原子操作的代码进行加锁
package com.hzw; public class CounterSync { volatile int i = 0; public synchronized void add() { i++; } }
package com.hzw; public class CounterLock { volatile int i = 0; Lock lock = new ReentrantLock(); public void add() { lock.lock(); try { i++; }finally { lock.unlock(); } } }
-
使用AtomicInteger类进行原子性的
i++
操作package com.hzw; public class CounterAtomic { AtomicInteger i = new AtomicInteger(0); public void add() { i++; } }
在AtomicInteger类中使用了CAS操作实现了原子性的
i++
操作,那么什么是CAS操作呢?
CAS(Compare and swap)
Compare and swap 比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证,即CAS是一个原子操作。
CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B。在操作期间先对旧值进行比较,若没有发生变化,才交换成新值,发生了变化则不交换,此时会在次重新取旧值进行CAS操作,形成一个自旋。
使用CAS解决原子性问题
Java中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSewapLong()等几个方法实现CAS。
package com.hzw.subject1;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class CounterUnsafe {
volatile int i = 0;
private static Unsafe unsafe = null;
//i字段的偏移量
private static long valueOffset;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
//获取i字段的偏移量
Field fieldI = CounterUnsafe.class.getDeclaredField("i");
valueOffset = unsafe.objectFieldOffset(fieldI);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void add() {
for (; ; ) {
int current = unsafe.getIntVolatile(this, valueOffset);
if (unsafe.compareAndSwapInt(this, valueOffset, current, current + 1)) {
break;
}
}
}
}
什么是字段偏移量?
在对象中除了可以通过普通get/set方法或反射操作字段外,还可以通过字段偏移量进行操作字段,如上图,如果要访问age字段,就可以通过字段偏移量10
进行访问