首先看一下下面两句代码:
AtomicInteger atomicInteger=new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,6)+" "+atomicInteger.get());
第一行atomicInteger的初始值是5,从堆内存拿出来这个值,放到线程自己的私有内存中,也就是线程对共享变量的拷贝,compareAndSet()第一个参数是原来堆内存中的值,当我们对这个共享变量进行修改之后,然后再写回堆内存,此时发现堆内存中的这个值已经被别的线程进行了修改,那么就不再写入,如果发现这个值没有被别的线程修改,还是5(期望值),那么就把该线程计算的新值写入到堆内存。因此输出是:
true 6
说白了就是一句话:真实值和期望值相同就相同,真是值和期望值不相同就修改失败
import java.util.concurrent.atomic.AtomicInteger;
class ShareResource{
AtomicInteger atomicInteger=new AtomicInteger();
public void add() {
atomicInteger.getAndIncrement();
}
}
public class Main{
public static void main(String args[]) {
ShareResource shareResource=new ShareResource();
//这里启动十个线程,每个线程对共享变量进行十次加1操作
for (int i=0;i<10;i++) {
new Thread(new Runnable() {
public void run() {
for (int j=0;j<100;j++) {
shareResource.add();
}
}
}).start();
}
//判断一下是不是所有计算线程已经执行完毕,大于2表示后台就只有两个线程,一个是主线程,一个是垃圾搜集线程
while (Thread.activeCount()>2) {
Thread.yield();
}
System.out.print(shareResource.atomicInteger);
}
}
现在讨论为什么在不加synchronized,只是使用getAndIncrement()就能i++在多线程下的原子性问题:
首先看它的实现:
public final int getAndIncrement() {
//U是UnSafe类的实例
return U.getAndAddInt(this, VALUE, 1);
}
其中this表示当前对象,VALUE表示内存偏移量,1表示每次加1.说白了也就是通过当前对象和内存偏移量找到这个变量,然后加1.
1,Unsafe类:
是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存的数据,Unsafe类存在于sun.misc包中,其内部方法操作可以向c的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
现在看一下Unsafe类中的getAndIn,t()源码:
public final int getAndInt(Object var1,long var2,int var4){
int var5;
do{
var5=this.getIntVolatile(var1,var2);
}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));
return var5;
}
这里也就明白为什么要使用CAS而不用synchronized。使用synchronized同一时间只有一个线程来访问,一致性得到的保障,但是并发性下降。使用CAS,由于有一个do......while()循环,只要修改不成功,就一直循环,这样不仅保证了一致性,又保证了并发性。
Unsafe类----》CAS(思想)(Unsafe类是本地方法,靠的就是底层汇编,直接操作内存,因此不加synchronized也能保证原子性)
CAS缺点:1,循环时间长,开销大。2,只能保证一个共享变量的原子操作,3,引出来ABA问题。
CAS算法实现一个重要的前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差里会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
要解决ABA问题,首先看一眼原子引用类,AtomicReference
import java.util.concurrent.atomic.AtomicReference;
class Person{
private String name;
private int age;
public Person(String name,int age) {
this.name=name;
this.age=age;
}
public String toString() {
return this.name+" "+this.age;
}
}
public class Main{
public static void main(String args[]) {
Person per1=new Person("张三",10);
Person per2=new Person("李四",20);
AtomicReference<Person> atomic=new AtomicReference<>();
atomic.set(per1);
System.out.println(atomic.compareAndSet(per1, per2)+" "+atomic.get().toString());
}
}
理解了原子引,接下来就要解决ABA问题了,那就是使用带时间戳的原子引用。什么是带时间戳的源自引用呢?
就像下面:
线程1:原始值100,版本号1
线程2:原始值100,版本号1——》修改为200,版本号2——》修改为100,版本号3
由于线程1操作时间长,操作结束的时候变为:修改为300,版本号2,但是此时比较版本号发现小于线程2,仍然不能修改。
因此出现了AtomicStampedReference这个类。可以解决ABA问题。