线程安全之原子性
一:非原子性的原因
先举个栗子:
上述例子中,可以看到,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可以解决非原子性
3.3 atomicInteger
atomicInteger是原子性的整型
3.4 Unsafe类
Unsafe可以让我们直接去管理内存,操作内存中的数据,因为Unsafe是final类,并且构造函数是私有的,所以不能直接实例化Unsafe。需要通过反射去实例化Unsafe。
四: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,这样就完成了出栈。
代码实现:
上述代码会出现一个问题:
如上图:线程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; }
测试类:
上述的测试类,也和上面画图所打印的一致,加了版本号之后,栈内的元素是C,D而不是B了。
六:java.util.concurrent包内元素
1. AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater;更新器,可以让volatile修饰的变量变成原子性的。