前言:关于同步,不少人都知道synchronized,Reentrantlock等加锁技术,这种方式也很好理解,是在线程访问的临界区资源上创建一个阻塞机制,须要线程等待java
其它线程释放了锁,它才能运行。这种方式很显然是奏效的,可是它却带来一个很大的问题:程序的运行效率。线程的上下文切换是很是耗费资源的,而等待又会有必定的时间消耗,那么有没有一种方式既能控制程序的同步效果,又能避免这种锁带来的消耗呢?答案就是无锁技术,本篇博客讨论的中心就是无锁。程序员
一:有锁与无锁数据库
二:cas技术原理api
三:AtomicInteger与unsafe类安全
四:经典的ABA问题与解决方法app
五:总结ide
正文性能
一:有锁与无锁测试
1.1:悲观锁与乐观锁this
数据库有两种锁,悲观锁的原理是每次实现数据库的增删改的时候都进行阻塞,防止数据发生脏读;乐观锁的原理是在数据库更新的时候,用一个version字段来记录版本号,而后经过比较是否是本身要修改的版本号再进行修改。这其中就引出了一种比较替换的思路来实现数据的一致性,事实上,cas也是基于这样的原理。
二:CAS技术原理
2.1:cas是什么?
cas的英文翻译全称是compare and set ,也就是比较替换技术,·它包含三个参数,CAS(V,E,N),其中V(variile)表示欲更新的变量,E(Excepted)表示预期的值,N(New)表示新值,只有当V等于E值的时候吗,才会将V的值设为N,若是V值和E值不一样,则说明已经有其它线程对该值作了更新,则当前线程什么都不作,直接返回V值。
举个例子,假如如今有一个变量int a=5;我想要把它更新为6,用cas的话,我有三个参数cas(5,5,6),咱们要更新的值是5,找到了a=5,符合V值,预期的值也是5符合,而后就会把N=6更新给a,a的值就会变成6;
2.2:cas的优势
2.2.1cas是以乐观的态度运行的,它老是认为当前的线程能够完成操做,当多个线程同时使用CAS的时候只有一个最终会成功,而其余的都会失败。这种是由欲更新的值作的一个筛选机制,只有符合规则的线程才能顺利执行,而其余线程,均会失败,可是失败的线程并不会被挂起,仅仅是尝试失败,而且容许再次尝试(固然也能够主动放弃)
2.2.2:cas能够发现其余线程的干扰,排除其余线程形成的数据污染
三:AtomicInteger与unsafe类
CAS在jdk5.0之后就被获得普遍的利用,而AtomicInteger是很典型的一个类,接下来咱们就来着重研究一下这个类:
3.1:AtomicInteger
关于Integer,它是final的不可变类,AtomicInteget能够把它视为一种整数类,它并不是是fianl的,但倒是线程安全的,而它的实现就是著名的CAS了,下面是一些它的经常使用方法:
public final int getAndSet(int newValue); public final boolean compareAndSet(int expect, int update); public final boolean weakCompareAndSet(int expect, int update); public final int getAndIncrement(); public final int getAndDecrement(); public final int addAndGet(int delta); public final int decrementAndGet(); public final int incrementAndGet()
其中主要的方法就是compareAndSet,咱们来测试一下这个方法,首先先给定一个值是5,咱们如今要把它改为2,若是expect传的是1,程序会输出什么呢?
public class TestAtomicInteger { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(5); boolean isChange = atomicInteger.compareAndSet(1, 2); int i = atomicInteger.get(); System.out.println("是否变化:"+isChange); System.out.println(i); } }
//outPut:
是否变化:false 5
boolean isChange = atomicInteger.compareAndSet(5, 2);
若是咱们把指望值改为5的话,最后的输出结果将是: // 是否变化:true 2
结论:只有当指望值与要改的值一致的时候,cas才会替换原始的值,设置成新值
3.2:测试AtomicInteger的线程安全性
为此我新建了10个线程,每一个线程对它的值自增5000次,若是是线程安全的,应该输出:50000
public class TestAtomicInteger { static AtomicInteger number=new AtomicInteger(0); public static class AddThread implements Runnable{ @Override public void run() { for (int i = 0; i < 5000; i++) { number.incrementAndGet(); } } } public static void main(String[] args) throws InterruptedException { Thread[] threads=new Thread[10]; for (int i = 0; i < threads.length; i++) { threads[i]=new Thread(new AddThread()); } for (int i = 0; i < threads.length; i++) { threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println(number); } }
最后重复执行了不少次都是输出:50000
3.3:unsafe类
翻如下这个方法的源码,能够看到其中是这样实现的:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
主要交给了unsafe类的compareAndSwapInt的方法,再翻如下能够看到是native的,也就是本地调用C++实现的源码,这里咱们就不深究了。关于unsafe类,它有一个最重要的点就是jdk的开发人员认为这个类是很危险的,因此是unsafe!所以不建议程序员调用这个类,为此他们还对这个类作了一个绝妙的处理,让你没法使用它:
public static Unsafe getUnsafe() { Class class= Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(class.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
public static boolean isSystemDomainLoader(ClassLoader var0) {
return var0 == null;
}
//outPut
Exception in thread "main" java.lang.SecurityException: Unsafe at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
这个方法实现的原理主要是类的加载机制,应用类的类加载器是有applicationClassLoder加载的,而jdk的类,好比关键库,rt.jar是由Bootstrap加载的,而BootStrapclassLoader是最上层加载库,它实际上是没有java对象的,由于jdk的经常使用类好比(AtomicInteger)加载的时候它会返回null,而咱们自定义的类必定不会返回null,就会抛出异常!
3.4:compareAndSet的方法原理
public final int incrementAndGet(){ for(;;){ int current=get(); int next=current+1; if(compareAndSet(current,next)){
return next; } } }
能够看出这是在一个无限的for循环里,而后获取当前的值,再给他加1(固定写死的值,每次自增1)。而后经过comePareandSet把当前的值和经过+1获取的值通过cas设值,这个方法返回一个boolean值,当成功的时候就返回当前的值,这样就保证了只有一个线程能够设值成功。注意:这里是一个死循环,只有当前值等于设置后的+1的值时,它才会跳出循环。这也证实cas是一个不断尝试的过程
四:经典的ABA问题与解决方法
4.2:AbA问题的产生
要了解什么是ABA问题,首先咱们来通俗的看一下这个例子,一家火锅店为了生意推出了一个特别活动,凡是在五一期间的老用户凡是卡里余额小于20的,赠送10元,可是这种活动没人只可享受一次。而后火锅店的后台程序员小王开始工做了,很简单就用cas技术,先去用户卡里的余额,而后包装成AtomicInteger,写一个判断,开启10个线程,而后判断小于20的,一概加20,而后就很开心的交差了。但是过了一段时间,发现帐面亏损的厉害,老板起先的预支是2000块,由于店里的会员总共也就100多个,就算每人都符合条件,最多也就2000啊,怎么预支了这么多。小王一下就懵逼了,赶忙debug,tail -f一下日志,这不看不知道,一看吓一跳,有个客户被充值了10次!
阐述:
假设有个线程A去判断帐户里的钱此时是15,知足条件,直接+20,这时候卡里余额是35.可是此时不巧,正好在连锁店里,这个客人正在消费,又消费了20,此时卡里余额又为15,线程B去执行扫描帐户的时候,发现它又小于20,又用过cas给它加了20,这样的话就至关于加了两次,这样循环往复确定把老板的钱就坑没了!
本质:
ABA问题的根本在于cas在修改变量的时候,没法记录变量的状态,好比修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改为B时,另外一个线程又会把B修改为A,形成casd屡次执行的问题。
4.3:AtomicStampReference
AtomicStampReference在cas的基础上增长了一个标记stamp,使用这个标记能够用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有如下几个参数:
//参数表明的含义分别是 指望值,写入的新值,指望标记,新标记值 public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp); public V getRerference(); public int getStamp(); public void set(V newReference,int newStamp);
4.4:AtomicStampReference的使用实例
咱们定义了一个money值为19,而后使用了stamp这个标记,这样每次当cas执行成功的时候都会给原来的标记值+1。然后来的线程来执行的时候就由于stamp不符合条件而使cas没法成功,这就保证了每次
只会被执行一次。
public class AtomicStampReferenceDemo { static AtomicStampedReference<Integer> money =new AtomicStampedReference<Integer>(19,0); public static void main(String[] args) { for (int i = 0; i < 3; i++) { int stamp = money.getStamp(); System.out.println("stamp的值是"+stamp); new Thread(){ //充值线程 @Override public void run() { while (true){ Integer account = money.getReference(); if (account<20){ if (money.compareAndSet(account,account+20,stamp,stamp+1)){ System.out.println("余额小于20元,充值成功,目前余额:"+money.getReference()+"元"); break; } }else { System.out.println("余额大于20元,无需充值"); } } } }.start(); } new Thread(){ @Override public void run() { //消费线程 for (int j = 0; j < 100; j++) { while (true){ int timeStamp = money.getStamp();//1 int currentMoney =money.getReference();//39 if (currentMoney>10){ System.out.println("当前帐户余额大于10元"); if (money.compareAndSet(currentMoney,currentMoney-10,timeStamp,timeStamp+1)){ System.out.println("消费者成功消费10元,余额"+money.getReference()); break; } }else { System.out.println("没有足够的金额"); break; } try { Thread.sleep(1000); }catch (Exception ex){ ex.printStackTrace(); break; } } } } }.start(); } }
这样实现了线程去充值和消费,经过stamp这个标记属性来记录cas每次设置值的操做,而下一次再cas操做时,因为指望的stamp与现有的stamp不同,所以就会设值失败,从而杜绝了ABA问题的复现。
五:总结
本篇博文主要分享了cas的技术实现原理,对于无锁技术,它有不少好处。同时,指出了它的弊端ABA问题,与此同时,也给出了解决方法。jdk源码中不少用到了cas技术,而咱们本身若是使用无锁技术,必定要谨慎处理ABA问题,最好使用jdk现有的api,而不要尝试本身去作,无锁是一个双刃剑,用好了,绝对可让性能比锁有很大的提高,用很差就很容易形成数据污染与脏读,望谨慎之。