热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

CAS无锁技术

前言:关于同步,很多人都知道synchronized,Reentrantlock等加锁技术,这种方式也很好理解,

前言:关于同步,很多人都知道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&#61;new AtomicInteger(0);public static class AddThread implements Runnable{&#64;Overridepublic void run() {for (int i &#61; 0; i <5000; i&#43;&#43;) {number.incrementAndGet();}}}public static void main(String[] args) throws InterruptedException {Thread[] threads&#61;new Thread[10];for (int i &#61; 0; i }

最后重复执行了很多次都是输出&#xff1a;50000 

3.3&#xff1a;unsafe类

翻以下这个方法的源码&#xff0c;可以看到其中是这样实现的&#xff1a;

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的方法&#xff0c;再翻以下可以看到是native的&#xff0c;也就是本地调用C&#43;&#43;实现的源码&#xff0c;这里我们就不深究了。关于unsafe类&#xff0c;它有一个最重要的点就是jdk的开发人员认为这个类是很危险的&#xff0c;所以是unsafe&#xff01;因此不建议程序员调用这个类&#xff0c;为此他们还对这个类做了一个绝妙的处理&#xff0c;让你无法使用它&#xff1a;

public static Unsafe getUnsafe() {Class class&#61; Reflection.getCallerClass();if (!VM.isSystemDomainLoader(class.getClassLoader())) {throw new SecurityException("Unsafe");} else {return theUnsafe;}}

public static boolean isSystemDomainLoader(ClassLoader var0) {
return var0 &#61;&#61; null;
}

//outPut
Exception in thread "main" java.lang.SecurityException: Unsafeat sun.misc.Unsafe.getUnsafe(Unsafe.java:90)

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

3.4:compareAndSet的方法原理

public final int incrementAndGet(){for(;;){int current&#61;get();int next&#61;current&#43;1;if(compareAndSet(current,next)){
return next;} }}

可以看出这是在一个无限的for循环里&#xff0c;然后获取当前的值&#xff0c;再给他加1(固定写死的值&#xff0c;每次自增1)。然后通过comePareandSet把当前的值和通过&#43;1获取的值经过cas设值&#xff0c;这个方法返回一个boolean值&#xff0c;当成功的时候就返回当前的值&#xff0c;这样就保证了只有一个线程可以设值成功。注意&#xff1a;这里是一个死循环&#xff0c;只有当前值等于设置后的&#43;1的值时&#xff0c;它才会跳出循环。这也证明cas是一个不断尝试的过程

四&#xff1a;经典的ABA问题与解决方法

4.2:AbA问题的产生

    要了解什么是ABA问题&#xff0c;首先我们来通俗的看一下这个例子&#xff0c;一家火锅店为了生意推出了一个特别活动&#xff0c;凡是在五一期间的老用户凡是卡里余额小于20的&#xff0c;赠送10元&#xff0c;但是这种活动没人只可享受一次。然后火锅店的后台程序员小王开始工作了&#xff0c;很简单就用cas技术&#xff0c;先去用户卡里的余额&#xff0c;然后包装成AtomicInteger&#xff0c;写一个判断&#xff0c;开启10个线程&#xff0c;然后判断小于20的&#xff0c;一律加20&#xff0c;然后就很开心的交差了。可是过了一段时间&#xff0c;发现账面亏损的厉害&#xff0c;老板起先的预支是2000块&#xff0c;因为店里的会员总共也就100多个&#xff0c;就算每人都符合条件&#xff0c;最多也就2000啊&#xff0c;怎么预支了这么多。小王一下就懵逼了&#xff0c;赶紧debug&#xff0c;tail -f一下日志&#xff0c;这不看不知道&#xff0c;一看吓一跳&#xff0c;有个客户被充值了10次!

阐述&#xff1a;

假设有个线程A去判断账户里的钱此时是15&#xff0c;满足条件&#xff0c;直接&#43;20&#xff0c;这时候卡里余额是35.但是此时不巧&#xff0c;正好在连锁店里&#xff0c;这个客人正在消费&#xff0c;又消费了20&#xff0c;此时卡里余额又为15&#xff0c;线程B去执行扫描账户的时候&#xff0c;发现它又小于20&#xff0c;又用过cas给它加了20&#xff0c;这样的话就相当于加了两次&#xff0c;这样循环往复肯定把老板的钱就坑没了&#xff01;

本质&#xff1a;

ABA问题的根本在于cas在修改变量的时候&#xff0c;无法记录变量的状态&#xff0c;比如修改的次数&#xff0c;否修改过这个变量。这样就很容易在一个线程将A修改成B时&#xff0c;另一个线程又会把B修改成A,造成casd多次执行的问题。

4.3&#xff1a;AtomicStampReference 

AtomicStampReference在cas的基础上增加了一个标记stamp&#xff0c;使用这个标记可以用来觉察数据是否发生变化&#xff0c;给数据带上了一种实效性的检验。它有以下几个参数&#xff1a;

//参数代表的含义分别是 期望值&#xff0c;写入的新值&#xff0c;期望标记&#xff0c;新标记值
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&#xff0c;然后使用了stamp这个标记&#xff0c;这样每次当cas执行成功的时候都会给原来的标记值&#43;1。而后来的线程来执行的时候就因为stamp不符合条件而使cas无法成功&#xff0c;这就保证了每次

只会被执行一次。

public class AtomicStampReferenceDemo {static AtomicStampedReference money &#61;new AtomicStampedReference(19,0);public static void main(String[] args) {for (int i &#61; 0; i <3; i&#43;&#43;) {int stamp &#61; money.getStamp();System.out.println("stamp的值是"&#43;stamp);new Thread(){ //充值线程&#64;Overridepublic void run() {while (true){Integer account &#61; money.getReference();if (account<20){if (money.compareAndSet(account,account&#43;20,stamp,stamp&#43;1)){System.out.println("余额小于20元&#xff0c;充值成功&#xff0c;目前余额&#xff1a;"&#43;money.getReference()&#43;"元");break;}}else {System.out.println("余额大于20元,无需充值");}}}}.start();}new Thread(){&#64;Overridepublic void run() { //消费线程for (int j &#61; 0; j <100; j&#43;&#43;) {while (true){int timeStamp &#61; money.getStamp();//1int currentMoney &#61;money.getReference();//39if (currentMoney>10){System.out.println("当前账户余额大于10元");if (money.compareAndSet(currentMoney,currentMoney-10,timeStamp,timeStamp&#43;1)){System.out.println("消费者成功消费10元&#xff0c;余额"&#43;money.getReference());break;}}else {System.out.println("没有足够的金额");break;}try {Thread.sleep(1000);}catch (Exception ex){ex.printStackTrace();break;}}}}}.start();}}

 这样实现了线程去充值和消费&#xff0c;通过stamp这个标记属性来记录cas每次设置值的操作&#xff0c;而下一次再cas操作时&#xff0c;由于期望的stamp与现有的stamp不一样&#xff0c;因此就会设值失败&#xff0c;从而杜绝了ABA问题的复现。

 五&#xff1a;总结

      本篇博文主要分享了cas的技术实现原理&#xff0c;对于无锁技术&#xff0c;它有很多好处。同时&#xff0c;指出了它的弊端ABA问题&#xff0c;与此同时&#xff0c;也给出了解决方法。jdk源码中很多用到了cas技术&#xff0c;而我们自己如果使用无锁技术&#xff0c;一定要谨慎处理ABA问题&#xff0c;最好使用jdk现有的api&#xff0c;而不要尝试自己去做&#xff0c;无锁是一个双刃剑&#xff0c;用好了&#xff0c;绝对可以让性能比锁有很大的提升&#xff0c;用不好就很容易造成数据污染与脏读&#xff0c;望谨慎之。

转:https://www.cnblogs.com/maohuidong/p/10027104.html



推荐阅读
  • 1Lock与ReadWriteLock1.1LockpublicinterfaceLock{voidlock();voidlockInterruptibl ... [详细]
  • Hibernate延迟加载深入分析-集合属性的延迟加载策略
    本文深入分析了Hibernate延迟加载的机制,特别是集合属性的延迟加载策略。通过延迟加载,可以降低系统的内存开销,提高Hibernate的运行性能。对于集合属性,推荐使用延迟加载策略,即在系统需要使用集合属性时才从数据库装载关联的数据,避免一次加载所有集合属性导致性能下降。 ... [详细]
  • MyBatis多表查询与动态SQL使用
    本文介绍了MyBatis多表查询与动态SQL的使用方法,包括一对一查询和一对多查询。同时还介绍了动态SQL的使用,包括if标签、trim标签、where标签、set标签和foreach标签的用法。文章还提供了相关的配置信息和示例代码。 ... [详细]
  • 本文详细介绍了Android中的坐标系以及与View相关的方法。首先介绍了Android坐标系和视图坐标系的概念,并通过图示进行了解释。接着提到了View的大小可以超过手机屏幕,并且只有在手机屏幕内才能看到。最后,作者表示将在后续文章中继续探讨与View相关的内容。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 本文介绍了GregorianCalendar类的基本信息,包括它是Calendar的子类,提供了世界上大多数国家使用的标准日历系统。默认情况下,它对应格里高利日历创立时的日期,但可以通过调用setGregorianChange()方法来更改起始日期。同时,文中还提到了GregorianCalendar类为每个日历字段使用的默认值。 ... [详细]
  • 合并列值-合并为一列问题需求:createtabletab(Aint,Bint,Cint)inserttabselect1,2,3unionallsel ... [详细]
  • 本文介绍了MVP架构模式及其在国庆技术博客中的应用。MVP架构模式是一种演变自MVC架构的新模式,其中View和Model之间的通信通过Presenter进行。相比MVC架构,MVP架构将交互逻辑放在Presenter内部,而View直接从Model中读取数据而不是通过Controller。本文还探讨了MVP架构在国庆技术博客中的具体应用。 ... [详细]
author-avatar
天蝎丿冷傲丨
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有