Java并发编程:什么是CAS?这回总算知道了

无锁的思想

众所周知,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)
//获得当前时间戳 欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 891219277

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

public int getStamp()
//设置当前对象引用和时间戳
public void set(V newReference, int newStamp)

原文地址:http://blog.51cto.com/14084556/2345457

时间: 2024-08-17 13:27:41

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

【Flume】从flume的监控度量数据XXXCounter来看JAVA并发编程中的CAS操作

图示 如上图所示红框部分,本人在做稳定性测试的时候,当flume运行几天后,我发现这个counter值逐渐变大,到一定值后,又变小了,有一个循环的过程,故而对此产生研究的欲望,下面来看看: if (txnEventCount == 0) { sinkCounter.incrementBatchEmptyCount(); } else if (txnEventCount == batchSize) { sinkCounter.incrementBatchCompleteCount(); } els

基于CAS线程安全的计算方法 java并发编程的艺术上的一个案例

package thread; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /**  * @author  changxiangxiang  * @date 2014年8月6日 下午3:25:12  * @description  * @since  sprint2  */ public class Counter {     privat

JAVA并发编程艺术 一(并发编程的挑战)

从今天起开始java并发编程艺术的学习,每一章学习完以后再这里记录下内容的重点,做个笔记,加深印象. 并发编程的目的是为了让程序运行的更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行.在进行并发是,如果希望通过多现场执行任务让程序运行得更快,会面临非常多的挑战,比如上下文切换的问题,死锁的问题,以及受限于硬件和软件的资源限制问题,本章会介绍几种并发编程的挑战以及解决方案 1.上下问切换 即使是单核处理器也支持多线程执行代码,cpu通过给每个线程分配cpu时间片来实现这个机制.时间片是

Java 并发编程:核心理论

并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能.它要求编程者对计算机最底层的运作原理有深刻的理解,同时要求编程者逻辑清晰.思维缜密,这样才能写出高效.安全.可靠的多线程并发程序.本系列会从线程间协调的方式(wait.notify.notifyAll).Synchronized及Volatile的本质入手,详细解释JDK为我们提供的每种并发工具和底层实现机制.在此基础上,我们会进一步分析java.util.concurrent包的工具类,包括其使用方式.实现源码及其背后的原理.本

【转】Java 并发编程:核心理论

并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能.它要求编程者对计算机最底层的运作原理有深刻的理解,同时要求编程者逻辑清晰.思维缜密,这样才能写出高效.安全.可靠的多线程并发程序.本系列会从线程间协调的方式(wait.notify.notifyAll).Synchronized及Volatile的本质入手,详细解释JDK为我们提供的每种并发工具和底层实现机制.在此基础上,我们会进一步分析java.util.concurrent包的工具类,包括其使用方式.实现源码及其背后的原理.本

Java并发编程:Concurrent锁机制解析

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

6、Java并发编程:volatile关键字解析

Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以重获生机. volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情.由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatil

Java 并发编程之任务取消(九)

Jvm关闭 jvm可正常关闭也可强行关闭,正常关闭有多种触发方式: 当最后一个正常(非守护,下面会讲到什么是守护线程)线程结束时 当调用system.exit时,或者通过其他特定于平台的方法关闭时(例如发送了SIGINT信号或键入Ctrl-c) 通过其他特定平台的方法关闭jvm,调用Runtime.halt或者在操作系统当中杀死JVM进程(例如发送sigkill)来强行关闭jvm. 关闭钩子 在正常关闭中,jvm首先调用所有已注册的关闭钩子,关闭钩子是指通过 Runtime.addShutdow

读《Java并发编程的艺术》(一)

离开博客园很久了,自从找到工作,到现在基本没有再写过博客了.在大学培养起来的写博客的习惯在慢慢的消失殆尽,感觉汗颜.所以现在要开始重新培养起这个习惯,定期写博客不仅是对自己学习知识的一种沉淀,更是在督促自己要不断的学习,不断的进步. 最近在进一步学习Java并发编程,不言而喻,这部分内容是很重要的.现在就以<并发编程的艺术>一书为主导线,开始新一轮的学习. 进程和线程 进程是一个应用程序在处理机上的一次执行过程,线程是进程的最小基本单位(个人理解).一个进程可以包含多个线程. 上下文切换 我们

Java并发编程学习路线

一年前由于工作需要从微软技术栈入坑Java,并陆陆续续做了一个Java后台项目,目前在搞Scala+Java混合的后台开发,一直觉得并发编程是所有后台工程师的基本功,所以也学习了小一年Java的并发工具,对整体的并发理解乃至分布式都有一定的提高,所以想和大家分享一下. 我的学习路线 首先说说学习路线,我一开始是直接上手JCIP(Java Concurrency in Practice),发现不是很好懂,把握不了那本书的主线,所以思索着从国内的作者开始先,所以便读了下方腾飞的<Java并发编程的艺