前言:关于同步,很多人都知道synchronized,Reentrantlock等加锁技术,这种方式也很好理解,是在线程访问的临界区资源上建立一个阻塞机制,需要线程等待

其它线程释放了锁,它才能运行。这种方式很显然是奏效的,但是它却带来一个很大的问题:程序的运行效率。线程的上下文切换是非常耗费资源的,而等待又会有一定的时间消耗,那么有没有一种方式既能控制程序的同步效果,又能避免这种锁带来的消耗呢?答案就是无锁技术,本篇博客讨论的中心就是无锁。

一:有锁与无锁

二:cas技术原理

三:AtomicInteger与unsafe类

四:经典的ABA问题与解决方法

五:总结

正文

一:有锁与无锁

1.1:悲观锁与乐观锁

数据库有两种锁,悲观锁的原理是每次实现数据库的增删改的时候都进行阻塞,防止数据发生脏读;乐观锁的原理是在数据库更新的时候,用一个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();// int currentMoney =money.getReference();// 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,而不要尝试自己去做,无锁是一个双刃剑,用好了,绝对可以让性能比锁有很大的提升,用不好就很容易造成数据污染与脏读,望谨慎之。

												

探索CAS无锁技术的更多相关文章

  1. CAS无锁技术

    前言:关于同步,很多人都知道synchronized,Reentrantlock等加锁技术,这种方式也很好理解,是在线程访问的临界区资源上建立一个阻塞机制,需要线程等待 其它线程释放了锁,它才能运行. ...

  2. 并发之CAS无锁技术

        CAS算法即是:Compare And Swap,比较并且替换:     CAS算法存在着三个参数,内存值V,旧的预期值A,以及要更新的值B.当且仅当内存值V和预期值B相等的时候,才会将内存值 ...

  3. java并发:AtomicInteger 以及CAS无锁算法【转载】

    1 AtomicInteger解析 众所周知,在多线程并发的情况下,对于成员变量,可能是线程不安全的: 一个很简单的例子,假设我存在两个线程,让一个整数自增1000次,那么最终的值应该是1000:但是 ...

  4. (转载)java高并发:CAS无锁原理及广泛应用

    java高并发:CAS无锁原理及广泛应用   版权声明:本文为博主原创文章,未经博主允许不得转载,转载请注明出处. 博主博客地址是 http://blog.csdn.net/liubenlong007 ...

  5. CAS无锁算法与ConcurrentLinkedQueue

    CAS:Compare and Swap 比较并交换 java.util.concurrent包完全建立在CAS之上的,没有CAS就没有并发包.并发包借助了CAS无锁算法实现了区别于synchroni ...

  6. CAS无锁机制原理

    原子类 java.util.concurrent.atomic包:原子类的小工具包,支持在单个变量上解除锁的线程安全编程 原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读 ...

  7. CAS无锁实现原理以及ABA问题

    CAS(比较与交换,Compare and swap) 是一种有名的无锁算法.无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(N ...

  8. CAS无锁操作

    https://coolshell.cn/articles/8239.html 主要讲的是<Implementing Lock-Free Queues>的论点,具体直接看论文最好.这里总结 ...

  9. CAS无锁策略

    并发编程时,对于共享资源的使用需要确保绝对的安全性.除了利用锁机制之外,还有一种无锁的概念.所谓无锁,就是假定在并发情况下,对于共享资源的访问没有冲突,线程可以一直不停的运行,无需阻塞,如果产生冲突, ...

随机推荐

  1. 【JAVA】关于java中 类.class.getResource("/").getPath()获取路径有空格的问题

    写了一个web工程,在本地测试正确,但是部署到服务器上就出现错误.原因是读取不到配置文件. 后来从打印出来的文件路径中发现是用Java的class.getResource("/") ...

  2. tomcat部署项目,80端口被占,解决方案

    第一个解决方案: 最大的可能:被System占了. 解决Windows Server 2008 System进程占用80端口 输入netstat -ano 可以看到80端口被PID4占用,于是打开任务 ...

  3. 搭建hexo博客并部署到github上

    hexo是由Node.js驱动的一款快速.简单且功能强大的博客框架,支持多线程,数百篇文章只需几秒即可生成.支持markdown编写文章,可以方便的生成静态网页托管在github上. 感觉不错. 前端 ...

  4. Python基础框架和工具

    最近在学Python金融大数据分析,在安装Python进行大数据分析的环境时遇到很多问题,例如:在安装pandas包时候就要到各种错误,总是缺少很多安装包,最后发现利用Python的Anaconda进 ...

  5. LeetCode 120——三角形最小路径和

    1. 题目 2. 解答 详细解答方案可参考北京大学 MOOC 程序设计与算法(二)算法基础之动态规划部分. 从三角形倒数第二行开始,某一位置只能从左下方或者右下方移动而来,因此,我们只需要求出这两者的 ...

  6. POJ 2177 Ghost Busters(三维几何)

    Description The famous Ghost Busters team has decided to upgrade their Ectomobile (aka Ecto-1) with ...

  7. 【转】MySQLroot用户忘记密码解决方案(安全模式,修改密码的三种方式)

    文章出自:http://www.2cto.com/database/201412/358128.html 1.关闭正在运行的MySQL2.启动MySQL的安全模式,命令如下: ? 1 mysqld - ...

  8. Thunder团队第六周 - Scrum会议4

    Scrum会议4 小组名称:Thunder 项目名称:i阅app Scrum Master:胡佑蓉 工作照片: 苗威同学在拍照,所以不在照片内. 参会成员: 王航:http://www.cnblogs ...

  9. 针对“来用”团队项目之NABC分析

    本项目特点之一:扩展性强 NABC分析: N(need):我们这个开发的这个软件主要是集娱乐软件和实用工具于一身的大容器,这里面有很多应用程序,针对不同用户需要,至少有一款应用程序能够满足用户的需要, ...

  10. 3dContactPointAnnotationTool开发日志(十二)

      因为ReferenceImage的锚点是固定的左下角,缩放时controller面板也会跟着动.为了使Scale的时候controller上的slider不会远离指针,于是把controller固 ...