概念:CAS(Compare And Swap)是对一种处理器指令的称呼。
先看一段代码:
private static int count = 0;
public static void add(){
count++;
}
如果多个线程调用add()方法,会有线程安全问题:
-
第一,count静态变量的可见性问题。
-
第二,count++操作的原型性问题。
那么如何解决呢?
第一种方案:将count++放在synchronized同步代码块里,原子性和可见性都可以解决,但是问题在于synchronized需要加锁,属于比较重量级的操作,影响性能。
代码:
private static int count = 0;
public static void add(){
synchronized(this){
count++;
}
}
注意:使用volatile只能解决count静态变量的可见性问题,无法解决原子性问题,所以无法完全解决问题。
第二中解方案:创建一个AtomicInteger类,将count的初始值传入,然后调用AtomicInteger类的incrementAndGet()方法替换count++操作,incrementAndGet()方法的底层是使用的compareAndSet()方法,该方法的原理下面会讲到。
代码:
private static int count = 0;
public static void add(){
//创建一个原子类
AtomicInteger atomicInteger = new AtomicInteger(count);
//调用原子类的自增方法
int i = atomicInteger.incrementAndGet()
}
count++的线程安全问题主要体现在它不是原子性操作,是一种read-modify-write操作,这就导致了线程读写时候的问题。
第三种方案:创建一个AtomicInteger类,将count的初始值传入,然后使用compareAndSet()方法。
代码:
private static int count = 0;
private static AtomicInteger atomicInteger = new AtomicInteger(count);
public static void add(){
while(true){
int countPre = atomicInteger.get();
int countNext = atomicInteger.get() + 1;
//主要是这个方法的使用compareAndSet(v1,v2)
if(atomicInteger.compareAndSet(countPre,countNext)){
break;
}
}
}
compareAndSet(v1,v2),v1是改变之前的值,v2是改变之后的值。该实现方式主要是为了更好的理解compareAndSet()方法,因为相应的类和方法都是封装起来的,理解方法的原理,以便大家更好的使用相关类和方法。
方法原理:
第一步:首先获取v1的值,再获取v2的值。
第二部:当调用compareAndSet()方法时,会再次获取v1的值。
第三步:比较两次获取的v1值是否相等。相等就交换,并且compareAndSet()方法返回true,循环结束;不相等就继续循环,再回到第一步的操作,直到循环结束。
CAS也是有缺点的:
-
两次v1的值不相等,就会循环,如果一直循环开销会很大。
-
只能保证一个变量的原子操作。如果保证多个共享变量的原子性,需要使用到锁。
-
经典ABA问题。
ABA问题:
-
例如一个数据int类型的n=10,A线程将n改为20之后,B线程将n改回10,C线程读取时,n还是10,AB线程更改n的过程对于C线程来说就像没发生过一样。
CAS特点:
CAS和volatile配合使用可以实现无锁并发,适用于线程数少、多核cpu下。
-
CAS是基于乐观锁的思想。
乐观锁:它假定当前环境写操作比读操作少,并且遇到并发的概率很低,在读数据的时候认为别的线程不会更改正在读的数据,在写数据时,判断当前获取的值和期望的值是否相同,如果相同则则进行更新,更新期间的操作时原子性的。
-
而synchronized是基于悲观锁实现的,对每一个线程访问共享变量时,都觉得不安全,所以需要线程一个一个的访问。
敲黑板:
-
CAS主要思想体现在无锁并发,无阻塞并发。
好好理解一下:
-
CAS不会像synchronized一样,使得线程会阻塞。
-
如果CAS使用时,当前值与期望值一直无法相等,那么必然会一直循环重试,反而会影响性能。
-