#####################################################################java
咱们知道多线程操做共享资源时,会出现三个问题:可见性、有序性以及原子性。安全
通常状况下,咱们采用synchronized同步锁(独占锁、互斥锁),即同一时间只有一个线程可以修改共享变量,其余线程必须等待。可是这样的话就至关于单线程,体现不出来多线程的优点。多线程
那么咱们有没有另外一种方式来解决这三个问题呢?并发
在上一章中,咱们提到了一个volatile关键字,它能够解决可见性和有序性的问题。并且若是操做的共享变量是基本数据类型,而且同一时间只对变量进行读取或者写入的操做,那么原子性问题也获得了解决,就不会产生多线程问题了。框架
可是一般,咱们都要先读取共享变量,而后操做共享变量,最后写入共享变量,那么这个时候怎么保证整个操做的原子性呢?一种解决方式就是CAS技术。
CAS(Compare and Swap)即比较并交换。在讲解这个以前,先了解两个重要概念:悲观锁与乐观锁。ide
悲观锁会阻塞其余线程。乐观锁不会阻塞其余线程,若是发生冲突,采用死循环的方式一直重试,直到更新成功。函数
CAS的原理很简单,包含三个值当前内存值(V)、预期原来的值(A)以及期待更新的值(B)。ui
若是内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,返回true。不然处理器不作任何操做,返回false。this
实现CAS最重要的一点,就是比较和交换操做的一致性,不然就会产生歧义。编码
好比当前线程比较成功后,准备更新共享变量值的时候,这个共享变量值被其余线程更改了,那么CAS函数必须返回false。
要实现这个需求,java中提供了Unsafe类,它提供了三个函数,分别用来操做基本类型int和long,以及引用类型Object
public final native boolean compareAndSwapObject (Object obj, long valueOffset, Object expect, Object update); public final native boolean compareAndSwapInt (Object obj, long valueOffset, int expect, int update); public final native boolean compareAndSwapLong (Object obj, long valueOffset, long expect, long update);
参数的意义:
- obj 和 valueOffset:表示这个共享变量的内存地址。这个共享变量是obj对象的一个成员属性,valueOffset表示这个共享变量在obj类中的内存偏移量。因此经过这两个参数就能够直接在内存中修改和读取共享变量值。
- expect: 表示预期原来的值。
- update: 表示期待更新的值。
接下来咱们来看看java并发框架下的atomic包是如何使用CAS的。
调用JUC并发框架下原子类的方法时,不须要考虑多线程问题。那么咱们分析它是怎么解决多线程问题的。以AtomicInteger类为例
// 经过它来实现CAS操做的。由于是int类型,因此调用它的compareAndSwapInt方法 private static final Unsafe unsafe = Unsafe.getUnsafe(); // value这个共享变量在AtomicInteger对象上内存偏移量, // 经过它直接在内存中修改value的值,compareAndSwapInt方法中须要这个参数 private static final long valueOffset; // 经过静态代码块,在AtomicInteger类加载时就会调用 static { try { // 经过unsafe类,获取value变量在AtomicInteger对象上内存偏移量 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } // 共享变量,AtomicInteger就保证了对它多线程操做的安全性。 // 使用volatile修饰,解决了可见性和有序性问题。 private volatile int value;
有三个重要的属性:
- unsafe: 经过它实现CAS操做,由于共享变量是int类型,因此调用compareAndSwapInt方法。
- valueOffset: 共享变量value在AtomicInteger对象上内存偏移量
- value: 共享变量,使用volatile修饰,解决了可见性和有序性问题。
// 直接读取。由于是volatile关键子修饰的,老是能看到(任意线程)对这个volatile变量最新的写入 public final int get() { return value; } // 直接写入。由于是volatile关键子修饰的,因此它修改value变量也会当即被别的线程读取到。 public final void set(int newValue) { value = newValue; }
由于value变量是volatile关键字修饰的,它老是能读取(任意线程)对这个volatile变量最新的写入。它修改value变量也会当即被别的线程读取到。
// 若是value变量的当前值(内存值)等于指望值(expect),那么就把update赋值给value变量,返回true。 // 若是value变量的当前值(内存值)不等于指望值(expect),就什么都不作,返回false。 // 这个就是CAS操做,使用unsafe.compareAndSwapInt方法,保证整个操做过程的原子性 public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
经过调用unsafe的compareAndSwapInt方法实现CAS函数的。可是CAS函数只能保证比较并交换操做的原子性,可是更新操做并不必定会执行。好比咱们想让共享变量value自增。
共享变量value自增是三个操做,1.读取value值,2.计算value+1的值,3.将value+1的值赋值给value。分析这三个操做:
- 读取value值,由于value变量是volatile关键字修饰的,可以读取到任意线程对它最后一次修改的值,因此没问题。
- 计算value+1的值:这个时候就有问题了,可能在计算这个值的时候,其余线程更改了value值,由于没有加同步锁,因此其余线程能够更改value值。
- 将value+1的值赋值给value: 使用CAS函数,若是返回false,说明在当前线程读取value值到调用CAS函数方法前,共享变量被其余线程修改了,那么value+1的结果值就不是咱们想要的了,由于要从新计算。
public final int getAndAddInt(Object obj, long valueOffset, int var) { int expect; // 利用循环,直到更新成功才跳出循环。 do { // 获取value的最新值 expect = this.getIntVolatile(obj, valueOffset); // expect + var表示须要更新的值,若是compareAndSwapInt返回false,说明value值被其余线程更改了。 // 那么就循环重试,再次获取value最新值expect,而后再计算须要更新的值expect + var。直到更新成功 } while(!this.compareAndSwapInt(obj, valueOffset, expect, expect + var)); // 返回当前线程在更改value成功后的,value变量原先值。并非更改后的值 return expect; }
这个方法在Unsafe类中,利用do_while循环,先利用当前值,计算更新值,而后经过compareAndSwapInt方法设置value变量,若是compareAndSwapInt方法返回失败,表示value变量的值被别的线程更改了,因此循环获取value变量最新值,再经过compareAndSwapInt方法设置value变量。直到设置成功。跳出循环,返回更新前的值。
// 将value的值当前值的基础上加1,并返回当前值 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } // 将value的值当前值的基础上加-1,并返回当前值 public final int getAndDecrement() { return unsafe.getAndAddInt(this, valueOffset, -1); } // 将value的值当前值的基础上加delta,并返回当前值 public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } // 将value的值当前值的基础上加1,并返回更新后的值(即当前值加1) public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } // 将value的值当前值的基础上加-1,并返回更新后的值(即当前值加-1) public final int decrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, -1) - 1; } // 将value的值当前值的基础上加delta,并返回更新后的值(即当前值加delta) public final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta; }
都是利用unsafe.getAndAddInt方法实现的。
import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; class Data { AtomicInteger num; public Data(int num) { this.num = new AtomicInteger(num); } public int getAndDecrement() { return num.getAndDecrement(); } } class MyRun implements Runnable { private Data data; // 用来记录全部卖出票的编号 private List<Integer> list; private CountDownLatch latch; public MyRun(Data data, List<Integer> list, CountDownLatch latch) { this.data = data; this.list = list; this.latch = latch; } @Override public void run() { try { action(); } finally { // 释放latch共享锁 latch.countDown(); } } // 进行买票操做,注意这里没有使用data.num>0做为判断条件,直到卖完线程退出。 // 那么作会致使这两处使用了共享变量data.num,那么作多线程同步时,就要考虑更多条件。 // 这里只for循环了5次,表示每一个线程只卖5张票,并将全部卖出去编号存入list集合中。 public void action() { for (int i = 0; i < 5; i++) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } int newNum = data.getAndDecrement(); System.out.println("线程"+Thread.currentThread().getName()+" num=="+newNum); list.add(newNum); } } } public class ThreadTest { public static void startThread(Data data, String name, List<Integer> list,CountDownLatch latch) { Thread t = new Thread(new MyRun(data, list, latch), name); t.start(); } public static void main(String[] args) { // 使用CountDownLatch来让主线程等待子线程都执行完毕时,才结束 CountDownLatch latch = new CountDownLatch(6); long start = System.currentTimeMillis(); // 这里用并发list集合 List<Integer> list = new CopyOnWriteArrayList(); Data data = new Data(30); startThread(data, "t1", list, latch); startThread(data, "t2", list, latch); startThread(data, "t3", list, latch); startThread(data, "t4", list, latch); startThread(data, "t5", list, latch); startThread(data, "t6", list, latch); try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } // 处理一下list集合,进行排序和翻转 Collections.sort(list); Collections.reverse(list); System.out.println(list); long time = System.currentTimeMillis() - start; // 输出一共花费的时间 System.out.println("\n主线程结束 time=="+time); } }
结果输出
线程t1 num==30 线程t2 num==29 线程t3 num==28 线程t5 num==26 线程t4 num==27 线程t6 num==25 线程t1 num==24 线程t2 num==23 线程t6 num==22 线程t4 num==19 线程t5 num==20 线程t3 num==21 线程t2 num==18 线程t3 num==13 线程t5 num==14 线程t1 num==15 线程t6 num==17 线程t4 num==16 线程t2 num==12 线程t1 num==9 线程t5 num==10 线程t3 num==11 线程t4 num==7 线程t6 num==8 线程t5 num==5 线程t4 num==4 线程t1 num==6 线程t2 num==3 线程t3 num==2 线程t6 num==1 [30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1] 主线程结束 time==58
咱们使用AtomicInteger,代替同步锁来解决多线程安全的。
######################################################################
假设如今线程A和线程B同时执行getAndAdd操做:
1.AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据Java内存模型,线程A和线程B各自持有一份value的副本,值为3。
2.线程A经过getIntVolatile(var1, var2)方法获取到value值3,线程切换,线程A挂起。
3.线程B经过getIntVolatile(var1, var2)方法获取到value值3,并利用compareAndSwapInt方法比较内存值也为3,比较成功,修改内存值为2,线程切换,线程B挂起。
4.线程A恢复,利用compareAndSwapInt方法比较,发现手里的值3和内存值2不一致,此时value正在被另一个线程修改,线程A不能修改value值。
5.线程的compareAndSwapInt实现,循环判断,从新获取value值,由于value是volatile变量,因此线程对它的修改,线程A老是可以看到。线程A继续利用compareAndSwapInt进行比较并替换,直到compareAndSwapInt修改为功返回true。
整个过程当中,利用CAS保证了对于value的修改的线程安全性。
不过因为CAS编码确实稍微复杂,并且jdk做者自己也不但愿你直接使用unsafe(后面会讲到)来进行代码的编写,因此若是不能深入理解CAS以及unsafe仍是要慎用,使用一些别人已经实现好的无锁类或者框架就行了。
CAS虽然很高效的解决原子操做,可是CAS仍然存在三大问题。
(1)ABA问题。
循环时间长开销大和只能保证一个共享变量的原子操做ABA问题。由于CAS须要在操做值的时候检查下值有没有发生变化,若是没有发生变化则更新,可是若是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,可是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法做用是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
(2)循环时间长开销大。
自旋CAS若是长时间不成功,会给CPU带来很是大的执行开销。若是JVM能支持处理器提供的pause指令那么效率会有必定的提高,pause指令有两个做用,第一它能够延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它能够避免在退出循环的时候因内存顺序冲突(memory order violation)而引发CPU流水线被清空(CPU pipeline flush),从而提升CPU的执行效率。
(3)只能保证对一个共享变量的原子操做。
当对一个共享变量执行操做时,咱们可使用循环CAS的方式来保证原子操做,可是对多个共享变量操做时,循环CAS就没法保证操做的原子性,这个时候就能够用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操做。好比有两个共享变量i=2,j=a,合并一下ij=2a,而后用CAS来操做ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行CAS操做。
##########################################################################
##########################################################################