一:非原子性的原因
先举个栗子:
public class ThreadCount {
volatile int a = 0;
public void add() {
a++;
}
}
public static void main(String[] args) throws InterruptedException {
ThreadCount atomic = new ThreadCount();
for (int i=0; i<6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j=0; j<10000; j++) {
atomic.add();
}
System.out.println("结束。。。");
}
}).start();
}
Thread.sleep(5000);
System.out.println(atomic.a);
}
上述例子中,可以看到,for循环下建立6个线程分别调用a++操作,每个线程调用10000,经过分析,最后输出a的值应该是60000,运行之后结果如下:

多次运行之后,可以发现,最后输出的a的值大多数情况下都是小于60000的,这是因为什么原因呢?
反编译a++的类,可以看到:

a++操作并不是一次操作,反编译之后的字节码指令操作是四步,也就是说a++操作本身不是原子性的;下面分析下每步操作:
1. getfield

看上图,第一步是从堆内存中获取a=0的值,放到操作数栈中,因为a=0是属于类下面的变量,所以是类实例化后,在堆内存中。
2. iconst_1

iconst_1是把数字1放到操作数栈中。
3. iadd
iadd操作是把操作数栈中的0和1,拿出来在CPU中计算,计算完成后把值放到操作数栈中。
4. putfield

计算完成后的值,把堆内存中的值修改为1。
注意:原子性,就是一个操作或者多个操作,执行的顺序不能改变,也不能被分割只执行其中的一部分;原子性操作,整个资源的操作是一体的,要么执行成功,要么全部失败。
二:CAS(compare and swap)机制
CAS机制属于硬件的同步原语,即硬件操作内存的指令,是原子性的。
CAS机制在去修改内存中的值时,会传两个参数,一个旧值,一个新值,若旧值和内存中的值一致,就把内存中的值替换为新值,否则内存中的值不做改变。

以上图举例子说明:假设T1线程和T2线程都执行CAS(0,1),想把内存中a的值修改为1,再假设T2线程稍后于T1线程执行,那么当T1线程执行成功,把a的值修改为1后,T2线程还在继续执行,而T2线程也是把a修改为1,这个时候就出现了问题,最后修改的值是一样的,也就是说T2线程的a++操作被分割了,在T1还未执行完之前,a的值为0,T1执行完后,a的值变为了1,所以a++操作不是原子性的。
三:解决非原子性问题
3.1 synchronized关键字
在方法的前面加上synchronized可以解决非原子性
public class ThreadSync {
volatile int a = 0;
public synchronized void add() {
a++;
}
}
public static void main(String[] args) throws InterruptedException {
ThreadSync atomic = new ThreadSync();
for (int i=0; i<6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j=0; j<10000; j++) {
atomic.add();
}
System.out.println("结束。。。");
}
}).start();
}
Thread.sleep(5000);
System.out.println(atomic.a);
}
3.2 Lock锁
public class ThreadLock {
volatile int a = 0;
Lock lock = new ReentrantLock(); //可重入锁
public void add() {
try {
lock.lock();
a++;
lock.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadLock atomic = new ThreadLock();
for (int i=0; i<6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j=0; j<10000; j++) {
atomic.add();
}
System.out.println("结束。。。");
}
}).start();
}
Thread.sleep(5000);
System.out.println(atomic.a);
}
3.3 atomicInteger
atomicInteger是原子性的整型
public class ThreadAtomic {
AtomicInteger atomic = new AtomicInteger(0);
public void add() {
atomic.incrementAndGet();
}
public int getValue() {
return atomic.get();
}
}
public static void main(String[] args) throws InterruptedException {
ThreadAtomic atomic = new ThreadAtomic();
for (int i=0; i<6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j=0; j<10000; j++) {
atomic.add();
}
System.out.println("结束。。。");
}
}).start();
}
Thread.sleep(5000);
System.out.println(atomic.getValue());
}
3.4 Unsafe类
Unsafe可以让我们直接去管理内存,操作内存中的数据,因为Unsafe是final类,并且构造函数是私有的,所以不能直接实例化Unsafe。需要通过反射去实例化Unsafe。
public final class Unsafe {
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
public class ThreadUnsafe {
volatile int a = 0;
//初始化Unsafe实体
public static Unsafe unsafe = null;
//初始化下标
public static long valueOffset;
static {
try {
//通过反射实例化Unsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
//通过反射获取a字段
Field valueField = ThreadUnsafe.class.getDeclaredField("a");
//获取a字段的下标
valueOffset = unsafe.objectFieldOffset(valueField);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void add() {
for (;;) {
if (unsafe.compareAndSwapInt(this, valueOffset, a, a+ 1)) {
return;
}
}
}
}
compareAndSwapInt返回值是boolean类型的,参数分别是对象,字段偏移量,旧值,新值。
四:CAS中存在的问题
1. CAS只能对单个变量进行操作,不能对多个变量实现原子性操作;
2. CAS自旋,可能会导致很多线程处于高频运行的状态,去争抢CPU,这样会带来CPU性能的损耗,(CAS失败,一直去重试);
3. ABA问题,不能准确的看出变量的变化。
五:ABA问题及解决ABA问题

假设T1线程和T2线程都去执行CAS(0, 1),假设T1先与T2执行,再T1执行完CAS(0, 1)把a的值修改为1后,又继续执行了CAS(1, 0),把a的值又改回了0,那么本来T2线程应该执行失败,可是却可能执行成功了。
下面举个例子:
模拟入栈和出栈操作:
1. 入栈


上图中,要把C入栈,首先把oldTop指向栈顶,top本身也是指向栈顶,把C的下一个栈帧指向栈顶,也就是A,最后再把top栈顶指向C,即可完成入栈。
2. 出栈


上图中,首先top和oldTop都是指向A,即栈顶,newTop指向A的下一个元素,再把top指向newTop,oldTop即是出栈的A,这样就完成了出栈。
代码实现:
//栈里面的每一个节点元素
public class Node {
//值
public String value;
//下一个节点元素
public Node next;
public Node(String value) {
this.value = value;
}
@Override
public String toString() {
return "value: " + value;
}
}
public static AtomicReference<Node> top = new AtomicReference<>();
//入栈
public void push(Node node) {
Node oldTop;
do {
oldTop = top.get();
node.next = oldTop;
} while (!top.compareAndSet(oldTop, node));
}
//出栈
public Node pop(int time) {
Node oldTop;
Node newTop;
do {
oldTop = top.get();
if (oldTop == null) {
return null;
}
newTop = oldTop.next;
//为了能更好的体现出ABA问题,休眠
LockSupport.parkNanos(1000 * 1000 * time);
} while (!top.compareAndSet(oldTop, newTop));
return oldTop;
}
上述代码会出现一个问题:


如上图:线程1执行一步操作让A出栈,线程2执行五步操作,让A,B出栈,再入栈D,C,A;根据上面的代码可以看到,线程1让 A出栈后,newTop指向的是下一个节点元素,即B,而这个时候,线程1挂起了,线程2开始执行,由于线程2没有挂起,会一直执行,最后栈内元素为D,C,A,而线程1指向的是B元素,然后导致,栈顶指向B,C,D节点元素就无辜的没有了。
这样的ABA问题怎么解决呢?加个版本号就行了,即每一个操作都加上版本号,操作的时候不仅比对值,还有版本号。修改上述代码:
public static AtomicStampedReference<Node> top = new AtomicStampedReference<>(null, 0);
//入栈
public void push(Node node) {
Node oldTop;
int version;
do {
version = top.getStamp();
oldTop = top.getReference();
node.next = oldTop;
} while (!top.compareAndSet(oldTop, node, version, version+1));
}
//出栈
public Node pop(int time) {
Node oldTop;
Node newTop;
int version;
do {
version = top.getStamp();
oldTop = top.getReference();
if (oldTop == null) {
return null;
}
newTop = oldTop.next;
LockSupport.parkNanos(1000 * 1000 * time);
} while (!top.compareAndSet(oldTop, newTop, version, version+1));
return oldTop;
}
测试类:
public class StackTest {
public static void main(String[] args) {
// StackDemo stack = new StackDemo();
StackDemo2 stack = new StackDemo2();
stack.push(new Node("B"));
stack.push(new Node("A"));
Thread thread1 = new Thread(() -> {
Node node = stack.pop(800);
System.out.println(Thread.currentThread().getName() + "," + node.toString());
});
thread1.start();
Thread thread2 = new Thread(() -> {
Node nodeA = stack.pop(0);
System.out.println(Thread.currentThread().getName() + "," + nodeA.toString());
Node nodeB = stack.pop(0);
System.out.println(Thread.currentThread().getName() + "," + nodeB.toString());
stack.push(new Node("D"));
stack.push(new Node("C"));
stack.push(nodeA);
});
thread2.start();
LockSupport.parkNanos(1000 * 1000 * 1000 * 500);
Node node = null;
while ((node = stack.pop(0)) != null) {
System.out.println(node.toString());
}
}
}
上述的测试类,也和上面画图所打印的一致,加了版本号之后,栈内的元素是C,D而不是B了。
六:java.util.concurrent包内元素
1. AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater;更新器,可以让volatile修饰的变量变成原子性的。
public class ThreadAtomic2 {
public static void main(String[] args) {
//把age变成原子性的
AtomicIntegerFieldUpdater<User> fieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
User user = new User(10, "test");
System.out.println(fieldUpdater.getAndSet(user, 20));
}
}
class User {
volatile int age;
volatile String name;
public User(int age, String name) {
this.age = age;
this.name = name;
}
}
2\. LongAdder,DoubleAdder计数器,可以加也可以减,使用方法如下:
public static void main(String[] args) {
LongAdder adder = new LongAdder();
for (int i =0; i<3; i++) {
adder.increment();
}
System.out.println(adder.sum());
}
3\. LongAccumulator,DoubleAccumulator计数器,可以自定义规则,使用方法如下:
public static void main(String[] args) {
LongAccumulator accumulator = new LongAccumulator((x, y) -> {
return x+y;
},0);
for (int i =0; i<3; i++) {
accumulator.accumulate(1);
}
System.out.println(accumulator.get());
}
到此,线程的原子性问题结束.