前言:关于同步,很多人都知道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了,下面是一些它的常用方法:

  1. public final int getAndSet(int newValue);
  2. public final boolean compareAndSet(int expect, int update);
  3. public final boolean weakCompareAndSet(int expect, int update);
  4. public final int getAndIncrement();
  5. public final int getAndDecrement();
  6. public final int addAndGet(int delta);
  7. public final int decrementAndGet();
  8. public final int incrementAndGet()

其中主要的方法就是compareAndSet,我们来测试一下这个方法,首先先给定一个值是5,我们现在要把它改成2,如果expect传的是1,程序会输出什么呢?

  1. public class TestAtomicInteger {
  2.  
  3. public static void main(String[] args) {
  4.  
  5. AtomicInteger atomicInteger = new AtomicInteger(5);
  6.  
  7. boolean isChange = atomicInteger.compareAndSet(1, 2);
  8.  
  9. int i = atomicInteger.get();
  10.  
  11. System.out.println("是否变化:"+isChange);
  12.  
  13. System.out.println(i);
  14. }
  15. }
  1. //outPut:
    是否变化:false
  2. 5
  1. boolean isChange = atomicInteger.compareAndSet(5, 2);

如果我们把期望值改成5的话,最后的输出结果将是: // 是否变化:true   2

结论:只有当期望值与要改的值一致的时候,cas才会替换原始的值,设置成新值

3.2:测试AtomicInteger的线程安全性

为此我新建了10个线程,每个线程对它的值自增5000次,如果是线程安全的,应该输出:50000

  1. public class TestAtomicInteger {
  2.  
  3. static AtomicInteger number=new AtomicInteger(0);
  4.  
  5. public static class AddThread implements Runnable{
  6.  
  7. @Override
  8. public void run() {
  9.  
  10. for (int i = 0; i < 5000; i++) {
  11.  
  12. number.incrementAndGet();
  13. }
  14.  
  15. }
  16. }
  17.  
  18. public static void main(String[] args) throws InterruptedException {
  19.  
  20. Thread[] threads=new Thread[10];
  21.  
  22. for (int i = 0; i < threads.length; i++) {
  23.  
  24. threads[i]=new Thread(new AddThread());
  25.  
  26. }
  27.  
  28. for (int i = 0; i < threads.length; i++) {
  29.  
  30. threads[i].start();
  31.  
  32. }
  33.  
  34. for (int i = 0; i < threads.length; i++) {
  35.  
  36. threads[i].join();
  37.  
  38. }
  39.  
  40. System.out.println(number);
  41. }
  42. }

最后重复执行了很多次都是输出:50000

3.3:unsafe类

翻以下这个方法的源码,可以看到其中是这样实现的:

  1. public final boolean compareAndSet(int expect, int update) {
  2. return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
  3. }
  1. public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

主要交给了unsafe类的compareAndSwapInt的方法,再翻以下可以看到是native的,也就是本地调用C++实现的源码,这里我们就不深究了。关于unsafe类,它有一个最重要的点就是jdk的开发人员认为这个类是很危险的,所以是unsafe!因此不建议程序员调用这个类,为此他们还对这个类做了一个绝妙的处理,让你无法使用它:

  1. public static Unsafe getUnsafe() {
  2. Class class= Reflection.getCallerClass();
  3. if (!VM.isSystemDomainLoader(class.getClassLoader())) {
  4. throw new SecurityException("Unsafe");
  5. } else {
  6. return theUnsafe;
  7. }
  8. }
  1. public static boolean isSystemDomainLoader(ClassLoader var0) {
    return var0 == null;
    }
  1. //outPut
    Exception in thread "main" java.lang.SecurityException: Unsafe
  2. at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)

这个方法实现的原理主要是类的加载机制,应用类的类加载器是有applicationClassLoder加载的,而jdk的类,比如关键库,rt.jar是由Bootstrap加载的,而BootStrapclassLoader是最上层加载库,它其实是没有java对象的,因为jdk的常用类比如(AtomicInteger)加载的时候它会返回null,而我们自定义的类一定不会返回null,就会抛出异常!

3.4:compareAndSet的方法原理

  1. public final int incrementAndGet(){
  2.  
  3. for(;;){
  4. int current=get();
  5. int next=current+1;
  6. if(compareAndSet(current,next)){
  7. return next;
  8. }
  9. }
  10. }

可以看出这是在一个无限的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,使用这个标记可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有以下几个参数:

  1. //参数代表的含义分别是 期望值,写入的新值,期望标记,新标记值
  2. public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp);
  3.  
  4. public V getRerference();
  5.  
  6. public int getStamp();
  7.  
  8. public void set(V newReference,int newStamp);

4.4:AtomicStampReference的使用实例

我们定义了一个money值为19,然后使用了stamp这个标记,这样每次当cas执行成功的时候都会给原来的标记值+1。而后来的线程来执行的时候就因为stamp不符合条件而使cas无法成功,这就保证了每次

只会被执行一次。

  1. public class AtomicStampReferenceDemo {
  2.  
  3. static AtomicStampedReference<Integer> money =new AtomicStampedReference<Integer>(19,0);
  4.  
  5. public static void main(String[] args) {
  6.  
  7. for (int i = 0; i < 3; i++) {
  8.  
  9. int stamp = money.getStamp();
  10.  
  11. System.out.println("stamp的值是"+stamp);
  12.  
  13. new Thread(){ //充值线程
  14.  
  15. @Override
  16. public void run() {
  17.  
  18. while (true){
  19.  
  20. Integer account = money.getReference();
  21.  
  22. if (account<20){
  23.  
  24. if (money.compareAndSet(account,account+20,stamp,stamp+1)){
  25.  
  26. System.out.println("余额小于20元,充值成功,目前余额:"+money.getReference()+"元");
  27. break;
  28. }
  29. }else {
  30.  
  31. System.out.println("余额大于20元,无需充值");
  32. }
  33. }
  34. }
  35. }.start();
  36. }
  37.  
  38. new Thread(){
  39.  
  40. @Override
  41. public void run() { //消费线程
  42.  
  43. for (int j = 0; j < 100; j++) {
  44.  
  45. while (true){
  46.  
  47. int timeStamp = money.getStamp();//1
  48.  
  49. int currentMoney =money.getReference();//39
  50.  
  51. if (currentMoney>10){
  52. System.out.println("当前账户余额大于10元");
  53. if (money.compareAndSet(currentMoney,currentMoney-10,timeStamp,timeStamp+1)){
  54.  
  55. System.out.println("消费者成功消费10元,余额"+money.getReference());
  56.  
  57. break;
  58. }
  59. }else {
  60. System.out.println("没有足够的金额");
  61.  
  62. break;
  63. }
  64. try {
  65. Thread.sleep(1000);
  66. }catch (Exception ex){
  67. ex.printStackTrace();
  68. break;
  69. }
  70.  
  71. }
  72.  
  73. }
  74. }
  75. }.start();
  76.  
  77. }
  78. }

这样实现了线程去充值和消费,通过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. 几个简单常用的jQuery实例

    一.点赞效果: 1.1 效果: 1.2 代码: <!DOCTYPE html> <html lang="en"> <head> <meta ...

  2. json--处理框架

    1.Android 中的Json解析工具fastjson .序列化.反序列化 2.Android Gson的使用总结 可以处理含有内部类的类,或包含集合内部类的类: 3.Android-JSONToo ...

  3. OpenSips使用说明

    OpenSips使用说明 安装MYSQL 安装及初始化 下载地址:http://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.17-linux-gli ...

  4. phpmyadmin在nginx环境下配置错误

    location ~ \.css {           add_header  Content-Type    text/css;        } location ~ \.js {        ...

  5. 升级安装APK兼容Android7.0,解决FileUriExposedException

    见http://blog.csdn.net/ruancoder/article/details/67639621?utm_source=itdadao&utm_medium=referral

  6. .net core从依赖注入容器获取对象

    创建引擎方法:该方法用于在不使用构造注入的情况下从依赖注入容器中获取对象 /// <summary> /// 一个负责创建对象的引擎 /// </summary> public ...

  7. object视频播放

    param name标签是在这个播放插件中嵌入的一些功能和播放参数: <param name="playcount" value="1"><! ...

  8. AFNetWorking 上传功能使用及源码分析

    使用方法比较多,这里列举两种: 第一种: // 1. 使用AFHTTPSessionManager的接口 AFHTTPSessionManager *manager = [AFHTTPSessionM ...

  9. C#提取双引号中的字符串

    public static void Main(string[] args) { string strtmp = "123\"456\"qqq\"789\&qu ...

  10. es6基础(3)-正则扩展

    //正则扩展 { let regex=new RegExp('xyz','i'); let regex2=new RegExp(/xyz/i); console.log(regex.test('xyz ...