无锁的思想

众所周知,Java中对并发控制的最常见方法就是锁,锁能保证同一时刻只能有一个线程访问临界区的资源,从而实现线程安全。然而,锁虽然有效,但采用的是一种悲观的策略。它假设每一次对临界区资源的访问都会发生冲突,当有一个线程访问资源,其他线程就必须等待,所以锁是会阻塞线程执行的。

当然,凡事都有两面,有悲观就会有乐观。而无锁就是一种乐观的策略,它假设线程对资源的访问是没有冲突的,同时所有的线程执行都不需要等待,可以持续执行。如果遇到冲突的话,就使用一种叫做CAS (比较交换) 的技术来鉴别线程冲突,如果检测到冲突发生,就重试当前操作到没有冲突为止。

CAS概述

CAS的全称是 Compare-and-Swap,也就是比较并交换,是并发编程中一种常用的算法。它包含了三个参数:V,A,B。

其中,V表示要读写的内存位置,A表示旧的预期值,B表示新值

CAS指令执行时,当且仅当V的值等于预期值A时,才会将V的值设为B,如果V和A不同,说明可能是其他线程做了更新,那么当前线程就什么都不做,最后,CAS返回的是V的真实值。

而在多线程的情况下,当多个线程同时使用CAS操作一个变量时,只有一个会成功并更新值,其余线程均会失败,但失败的线程不会被挂起,而是不断的再次循环重试。正是基于这样的原理,CAS即时没有使用锁,也能发现其他线程对当前线程的干扰,从而进行及时的处理。

CAS的应用类

Java中提供了一系列应用CAS操作的类,这些类位于java.util.concurrent.atomic包下,其中最常用的就是AtomicInteger,该类可以看做是实现了CAS操作的Integer,所以,下面我们就通过学习该类的案例来一窥全貌CAS的妙用。

学习AtomicInteger之前,我们先来看一段代码实例:

public class AtomicDemo {

    public static int NUMBER = 0;

    public static void increase() {
NUMBER++;
} public static void main(String[] args) throws InterruptedException {
AtomicDemo test = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++)
test.increase();
}).start();
}
Thread.sleep(200);
System.out.println(test.NUMBER);
}
}

在main函数中开启了10个线程,执行后会轮流调用 increase(),当然我们知道,运行后输出的结果肯定不是我们期望的值,因为没有做线程安全的处理,所以10个线程流量操作临界区的资源NUMBER就会出错。

解决办法并不难,用我们之前学过的锁,例如synchronized修饰代码块,程序就会正常输出10000。当然,用锁解决并不是我们想要的方式,因为锁会阻塞线程,影响程序的性能,这时候,AtomicInteger就可以派上用场了。

将上面的程序改造一下,变成下面这样:

public static AtomicInteger NUMBER = new AtomicInteger(0);

public static void increase() {
NUMBER.getAndIncrement();
} public static void main(String[] args) throws InterruptedException {
AtomicDemo test = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++)
test.increase();
}).start();
}
Thread.sleep(200);
System.out.println(test.NUMBER);
}

运行main方法,程序输出的就是我们想要的值,也就是10000。

上面的代码中,increase方法里调用了NUMBER.getAndIncrement() ,这是AtomicInteger的自增方法,会对当前的值加1,并且返回旧值,点进方法的源码,它调用的是unsafe.getAndAddInt()方法:

public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

getAndAddInt的作用是对当前值加1,并返回旧值。

unsafe是Unsafe类的一个变量,通过Unsafe.getUnsafe()来获取

private static final Unsafe unsafe = Unsafe.getUnsafe();

Unsafe类是一个比较特殊的类,它是一个JDK内部使用的专属类,用一般的编辑器无法直接查看源码,只能看到反编译后的class文件。

这里要扩展一个知识点,就是Java本身无法访问操作系统,需要使用native方法,而Unsafe类中的方法就包含了大量的native方法,提高了Java对系统底层的原子操作能力。例如我们代码中使用到的getAndAddInt()底层就是调用一个native方法,用idea点击方法,得到下面反编译后的代码:

public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

compareAndSwapInt的作用是比较并交换整数值,如果指定的字段的值等于期望值,也就是CAS中的 'A' (预期值),那么就会把它设置为新值 (CAS中的 'B'),不难想象,该方法内部的实现必然是依靠原子操作完成的。除此之外,Unsafe类中还提供了其他的原子操作的方法,例如上面源码中的getIntVolatile就是使用volatile语义获得给定对象的值,这些方法通过底层的原子操作高效的提升了应用层面的性能。

CAS的缺点

虽然CAS的性能比起锁要强大很多,但它也存在一些缺点,例如:

1、循环的时间开销大

在getAndAddInt的方法中,我们可以看到,只是简单的设置一个值却调用了循环,如果CAS失败,会一直进行尝试。如果CAS长时间不成功,那么循环就会不停的跑,无疑会给系统造成很大的开销。

2、ABA问题

前面说过,CAS判断变量操作成功的条件是V的值和A是一致的,这个逻辑有个小小的缺陷,就是如果V的值一开始为A,在准备修改为新值前的期间曾经被改成了B,后来又被改回为A,经过两次的线程修改对象的值还是旧值,那么CAS操作就会误任务该变量从来没被修改过。这就是CAS中的“ABA”问题。

当然,"ABA"问题也有解决方案,Java并发包中提供了一个带有时间戳的对象引用 AtomicStampedReference,其内部不仅维护了一个对象值,还维护了一个时间戳,当AtomicStampedReference对应的数值被修改时,除了更新数据本身,还需要更新时间戳,只有对象值和时间戳都满足期望值,才能修改成功。这是AtomicStampedReference的几个有关时间戳信息的方法:

//比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳
public boolean compareAndSet(V expectedReference, V newReference,
int expectedStamp, int newStamp)
//获得当前时间戳
public int getStamp()
//设置当前对象引用和时间戳
public void set(V newReference, int newStamp)

Java并发编程:什么是CAS?这回总算知道了的更多相关文章

  1. Java并发编程系列-(3) 原子操作与CAS

    3. 原子操作与CAS 3.1 原子操作 所谓原子操作是指不会被线程调度机制打断的操作:这种操作一旦开始,就一直运行到结束,中间不会有任何context switch,也就是切换到另一个线程. 为了实 ...

  2. JAVA并发编程: CAS和AQS

       版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/u010862794/article/details/72892300 说起JAVA并发编程,就不得不聊 ...

  3. Java并发编程入门与高并发面试(三):线程安全性-原子性-CAS(CAS的ABA问题)

    摘要:本文介绍线程的安全性,原子性,java.lang.Number包下的类与CAS操作,synchronized锁,和原子性操作各方法间的对比. 线程安全性 线程安全? 线程安全性? 原子性 Ato ...

  4. java并发编程(2)--volatile(转)

    转载:http://ifeve.com/volatile/ 作者:方 腾飞 花名清英,并发网(ifeve.com)创始人,畅销书<Java并发编程的艺术>作者,蚂蚁金服技术专家.目前工作于 ...

  5. Java 并发编程:Callable和Future

    项目中经常有些任务需要异步(提交到线程池中)去执行,而主线程往往需要知道异步执行产生的结果,这时我们要怎么做呢?用runnable是无法实现的,我们需要用callable实现. import java ...

  6. Java并发编程的4个同步辅助类(CountDownLatch、CyclicBarrier、Semaphore、Phaser)

    我在<JDK1.5引入的concurrent包>中,曾经介绍过CountDownLatch.CyclicBarrier两个类,还给出了CountDownLatch的演示案例.这里再系统总结 ...

  7. java并发编程系列七:volatile和sinchronized底层实现原理

    一.线程安全 1.  怎样让多线程下的类安全起来 无状态.加锁.让类不可变.栈封闭.安全的发布对象 2. 死锁 2.1 死锁概念及解决死锁的原则 一定发生在多个线程争夺多个资源里的情况下,发生的原因是 ...

  8. Java并发编程的4个同步辅助类(CountDownLatch、CyclicBarrier、Semphore、Phaser)

    我在<jdk1.5引入的concurrent包>中,曾经介绍过CountDownLatch.CyclicBarrier两个类,还给出了CountDownLatch的演示案例.这里再系统总结 ...

  9. java并发编程的艺术(一)---锁的基本属性

    本文来源于翁舒航的博客,点击即可跳转原文观看!!!(被转载或者拷贝走的内容可能缺失图片.视频等原文的内容) 若网站将链接屏蔽,可直接拷贝原文链接到地址栏跳转观看,原文链接:https://www.cn ...

  10. Java并发编程之三:volatile关键字解析 转载

    目录: <Java并发编程之三:volatile关键字解析 转载> <Synchronized之一:基本使用>   volatile这个关键字可能很多朋友都听说过,或许也都用过 ...

随机推荐

  1. s6-2 UDP

    User Datagram Protocol  UDP 是一个无连接的(connectionless)的传输层协议  UDP传输数据段,无须建立连接  UDP 在 RFC 768中描述  很多 ...

  2. Catalan数与出栈顺序个数,Java编程模拟

    问题描述: 队列中有从1到7(由小到大排列)的7个整数,问经过一个整数栈后,出栈的所有排列数有多少?如果整数栈的容量是4(栈最多能容纳4个整数),那么出栈的排列数又是多少? 分析:对于每一个数字i, ...

  3. 背水一战 Windows 10 (85) - 文件系统: 获取文件夹和文件, 分组文件夹, 排序过滤文件夹和文件, 搜索文件

    [源码下载] 背水一战 Windows 10 (85) - 文件系统: 获取文件夹和文件, 分组文件夹, 排序过滤文件夹和文件, 搜索文件 作者:webabcd 介绍背水一战 Windows 10 之 ...

  4. css font-family属性设置中文字体乱码

    一般设置字体,个人都喜欢用中文,比如:font-family:"微软雅黑":但是偶尔会出现设置以后字体显示乱码的问题 解决方法[1]: 看看你的CSS文件的第一行有没有:@char ...

  5. Java基本数据类型总结、类型转换、常量的声明规范,final关键字的用法

    1  Java 基本数据类型 变量就是申请内存来存储值.也就是说,当创建变量的时候,需要在内存中申请空间. 内存管理系统根据变量的类型为变量分配存储空间,分配的空间只能用来储存该类型数据. 因此,通过 ...

  6. MySQL:基础架构和工作流程

    [参考文章]:01|基础架构:一条查询语句的执行流程 1. 基本架构 大体来说,MySQL可以分为Server层和存储引擎两部分. Server层包括链接器,分析器,优化器,执行器等,涵盖大多数核心服 ...

  7. 使用node.js + json-server + mock.js 搭建本地开发mock数据服务

    在开发过程中,前后端不论是否分离,接口多半是滞后于页面开发的.所以建立一个REST风格的API接口,给前端页面提供虚拟的数据,是非常有必要的.对比过多种mock工具后,我最终选择了使用 json se ...

  8. PCIE 2.0协议概念基本科普

    PCIE的概念:是电脑总线PCI的一种,它沿用现有的PCI编程概念及通信标准,但建基于更快的串行通信系统. 英特尔是该接口的主要支持者.PCIe仅应用于内部互连.由于PCIe是基于现有的PCI系统,只 ...

  9. 【xsy1303】生成树 乱搞

    题目大意:给你n个点,问这n个点构成的最小标准差生成树的值 这题题解里面写的都是什么代码?? 你用O(n^4)的复杂度,枚举出两条边,然后求平均数. 对于剩下的边,我们求出这些边与平均数的差,然后求最 ...

  10. SQL分别求行、列的平均值

    日常工作中,会需要用SQL求平均值,分别是求某一项的平均值或求某一个对象的平均值,放到表格就是求一行中的几个字段的平均值和求一列的平均值. 第一种:[列的平均值]AVG:这个函数相信大家都不陌生的,求 ...