Java高并发核心编程—CAS与JUC原子类

news/2024/11/22 10:41:03/

注:本笔记是阅读《Java高并发核心编程卷2》整理的笔记!

CAS原理
JUC原子类一Atomic
基本原子类
数组原子类
引用原子类
字段更新原子类
AtomicInteger 线程安全原理
引用类型原子类
属性更新原子类
ABA问题
提升高并发场景下CAS提作的性能
以空间换时间: LongAdder
CAS优势与弊端
提升CAS性能

JVM的synchronized轻量级锁使用CAS(Compare And Swap,比较并交换)进行自旋抢锁, CAS是CPU指令级的原子操作并处于用户态下,所以JVM轻量级锁开销较小。 在java.util.concurrent.atomic包的原子类(如AtomicXXX中)都使用了CAS保障对数字成员进行操作的原子性。java.util.concurrent的大多数类(包括显式锁、并发容器)都是基于AQS和AtomicXXX实现的,其中AQS通过CAS保障其内部双向队列队头、队尾操作的原子性

CAS原理

JDK 5所增加的JUC(java.util.concurrent)并发包对操作系统的底层CAS原子操作进行了封装,为上层Java程序提供了CAS操作的API。 CAS是一种无锁算法,该算法关键依赖两个值——期望值(就值)和新值,底层CPU利用原子操作判断内存原值与期望值是否相等,如果相等就给内存地址赋新值,否则不做任何操作。

使用CAS进行无锁编程的步骤大致如下:

  1. 获得字段的期望值(oldValue)。
  2. 计算出需要替换的新值(newValue)。
  3. 通过CAS将新值(newValue)放在字段的内存地址上,如果CAS失败就重复第1)步到第2)步,直到CAS成功,这种重复俗称CAS自旋。
do
{获得字段的期望值(expValue),也就是读取内存原值;计算出需要替换的新值(newValue);
} while (!CAS(内存地址, expValue, newValue)) // 判断期望值是否等于现在的内存原值,如果等于表示此期间没人修改,否则就被修改了,重新获得期望值。

假设共享变量V内存值为100,线程A对V减1操作,线程B对V加1操作。使用CAS实现:无论A、B想要对其修改都先获取V的值,作为期望值,然后才进行操作,最后提交修改的时候需要用开始的期望值去与内存原值V进行对比,如果相等表示未被修改。如果不相等表示已被修改了,重新进行前面操作,获取V值作为期望值。。。

当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很少, CAS性能会很高;当并发修改的线程多,冲突出现的机会多时,自旋的次数也会很多, CAS性能会大大降低。所以,提升CAS无锁编程效率的关键在于减少冲突的机会。 所以CAS适合用于并发线程数较少的场景。

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果

可以使用Unsafe类实现CAS原子操作:

//通过CAS原子操作,进行“比较并交换”
public final boolean unSafeCompareAndSet(int oldValue, int newValue)
{   //valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据。原子操作:使用unsafe的“比较并交换”方法进行value属性的交换return unsafe.compareAndSwapInt( this, valueOffset,oldValue ,newValue );
}

JUC原子类—Atomic

在多线程并发执行时,诸如“++”或“–”类的运算不具备原子性,不是线程安全的操作。通常情况下,大家会使用synchronized将这些线程不安全的操作变成同步操作,但是这样会降低并发程序的性能。所以, JDK为这些类型不安全的操作提供了一些原子类,与synchronized同步机制相比, JDK原子类基于CAS轻量级原子操作实现,使得程序运行效率变得更高。

JUC并发包中原子类都存放在java.util.concurrent. atomic类路径下,可以将JUC包中的原子类分为4类:基本原子类、数组原子类、原子引用类和字段更新原子类。 主要使用的为基本原子类和数组原子类,其他稍作了解。

基本原子类

基本原子类的功能是通过原子方式更新Java基础类型变量的值 :

  • AtomicInteger:整型原子类。
  • AtomicLong:长整型原子类。
  • AtomicBoolean:布尔型原子类。

在多线程环境下,如果涉及基本数据类型的并发操作,不建议采用synchronized重量级锁进行线程同步,而是建议优先使用基础原子类保障并发操作的线程安全性。 基础原子类AtomicInteger常用的方法主要如下:

public final int get() //获取当前的值
public final int getAndSet(int newValue) //获取当前的值,然后设置新的值
public final int getAndIncrement() //获取当前的值,然后自增
public final int getAndDecrement() //获取当前的值,然后自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update); //通过CAS方式设置整数值int tempvalue = 0;
AtomicInteger i = new AtomicInteger(0);
//取值,然后设置一个新值 i=3
tempvalue = i.getAndSet(3);
//取值,然后自增 i=4
tempvalue = i.getAndIncrement(); 
//取值,然后增加5 i=9
tempvalue = i.getAndAdd(5);
//CAS交换 i=100
boolean flag = i.compareAndSet(9, 100);
数组原子类

数组原子类的功能是通过原子方式更新数组中的某个元素的值 :

  • AtomicIntegerArray:整型数组原子类。
  • AtomicLongArray:长整型数组原子类。
  • AtomicReferenceArray:引用类型数组原子类

上面三个类提供的方法几乎相同,所以我们这里以AtomicIntegerArray为例来介绍。AtomicIntegerArray类常用方法如下:

//获取 index=i 位置元素的值
public final int get(int i)
//返回index=i位置的当前的值,并将其设置为新值: newValue
public final int getAndSet(int i, int newValue)
//获取index=i位置元素的值,并让该位置的元素自增
public final int getAndIncrement(int i)
//获取index=i位置元素的值,并让该位置的元素自减
public final int getAndDecrement(int i)
//获取index=i位置元素的值,并加上预期的值
public final int getAndAdd(int delta)
//如果输入的数值等于预期值,就以原子方式将位置i的元素值设置为输入值(update)
boolean compareAndSet(int expect, int update)
//最终将位置i的元素设置为newValue
//lazySet方法可能导致其他线程在之后的一小段时间内还是可以读到旧的值
public final void lazySet(int i, int newValue);int tempvalue = 0;
//原始的数组
int[] array = { 1, 2, 3, 4, 5, 6 };
//包装为原子数组
AtomicIntegerArray i = new AtomicIntegerArray(array);
//获取第0个元素,然后设置为2 ,输出 tempvalue:1; i:[2, 2, 3, 4, 5, 6] 
tempvalue = i.getAndSet(0, 2);
//获取第0个元素,然后自增,输出tempvalue:2; i:[3, 2, 3, 4, 5, 6]
tempvalue = i.getAndIncrement(0); 
//获取第0个元素,然后增加一个delta 5,输出tempvalue:3; i:[8, 2, 3, 4, 5, 6]
tempvalue = i.getAndAdd(0, 5); 
引用原子类

引用原子类主要包括以下三个:

  • AtomicReference:引用类型原子类。
  • AtomicMarkableReference:带有更新标记位的原子引用类型。
  • AtomicStampedReference:带有更新版本号的原子引用类型。
字段更新原子类

字段更新原子类主要包括以下三个:

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
AtomicInteger 线程安全原理

基础原子类(以AtomicInteger为例)主要通过CAS自旋+volatile相结合的方案实现,既保障了变量操作的线程安全性,又避免了synchronized重量级锁的高开销,使得Java程序的执行效率大为提升。 CAS用于保障变量操作的原子性, volatile关键字用于保障变量的可见性(即一个线程修改了某个volatile变量的值,该值对其他线程立即可见。),二者常常结合使用。

源代码实现:

//Unsafe类实例,也是使用Unsafe类的CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
//内部value值,使用volatile保证线程可见性
private volatile int value;
//对比expect(期望值)与value,若不同则返回false
//若expect与value相同,则将新值赋给value,并返回true,否则循环自旋,直到成功
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
引用类型原子类

基础的原子类型只能保证一个变量的原子操作,当需要对多个变量进行操作时, CAS无法保证原子性操作,这时可以用AtomicReference(原子引用类型)保证对象引用的原子性。简单来说,如果需要同时保障对多个变量操作的原子性,就可以把多个变量放在一个对象中进行操作。

使用原子引用类型AtomicReference包装了User对象之后,只能保障User引用的原子操作,对被包装的User对象的字段值修改时不能保证原子性,这点要切记。

这里以AtomicReference为例子来介绍 ,首先定义一个User类,属性包括 uid,nickName,age三个。

public class User implements Serializable{String uid; //用户IDString nickName; //昵称public volatile int age; //年龄public User(String uid, String nickName){this.uid = uid;this.nickName = nickName;}@Overridepublic String toString(){return "User{" +"uid='" + getUid() + '\'' +", nickName='" + getNickName() + '\'' +", platform=" + getPlatform() +'}';}
}

使用AtomicReference对User的引用进行原子性修改,代码如下:

//包装的原子对象
AtomicReference<User> userRef = new AtomicReference<User>();
//待包装的User对象
User user = new User("1", "张三");
//为原子对象设置值
userRef.set(user); 
//要使用CAS替换的User对象
User updateUser = new User("2", "李四");
//使用CAS替换 , 成功success为true,user为李四
boolean success = userRef.compareAndSet(user, updateUser); 
属性更新原子类

如果需要保障对象某个字段(或者属性)更新操作的原子性,需要用到属性更新原子类。这里以AtomicIntegerFieldUpdater为例来介绍,使用属性更新原子类保障属性安全更新的流程大致需要两步:

  • 第一步,更新的对象属性必须使用public volatile修饰符。
  • 第二步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设臵想要更新的类和属性。
//使用静态方法newUpdater()创建一个更新器updater
AtomicIntegerFieldUpdater<User> updater=
AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
User user = new User("1", "张三");
//使用属性更新器的getAndIncrement、 getAndAdd增加user的age值
Print.tco(updater.getAndIncrement(user)); // 1
Print.tco(updater.getAndAdd(user, 100)); // 101
//使用属性更新器的get获取user的age值
Print.tco(updater.get(user)); // 101
ABA问题

比如一个线程A从内存位置M中取出V1,另一个线程B也取出V1。现在假设线程B进行了一些操作之后将M位置的数据V1变成了V2,然后又在一些操作之后将V2变成V1。之后,线程A进行CAS操作,但是线程A发现M位置的数据仍然是V1,最后线程A操作成功。尽管线程A的CAS操作成功,但是不代表这个过程是没有问题的,线程A操作的数据V1可能已经不是之前的V1,而是被线程B替换过的V1,这就是ABA问题。

举例:假设共享变量V内存值为100,线程A对V减1再加1操作,线程B对V加1再减1操作。使用CAS实现:无论A、B想要对其修改都先获取V的值,作为期望值,然后才进行操作,最后提交修改的时候需要用开始的期望值去与内存原值V进行对比,如果相等表示未被修改。如果不相等表示已被修改了。在这种情况下,无论A、B线程谁先读取并修改,之后的线程总能不通过自旋修改成功!即好像这个乐观锁不存在似的!

解决方案:

使用乐观锁的版本号方式,乐观锁每次在执行数据的修改操作时都会带上一个版本号,版本号和数据的版本号一致就可以执行修改操作并对版本号执行加1操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

JDK提供了一个类似AtomicStampedReference类来解决ABA问题。AtomicStampReference在CAS的基础上增加了一个Stamp(印戳或标记),使用这个印戳可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。 AtomicStampReference的compareAndSet()方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳标志是否等于预期标志,如果全部相等,就以原子方式将引用值和印戳标志的值更新为给定的更新值。

//compareAndSet方法的第一个参数是原CAS中的原参数,第二个参数是要替换后的新参数,第
//三个参数是原来CAS数据旧的版本号,第四个参数表示替换后的版本号。
public boolean compareAndSet(V expectedReference, //预期引用值V newReference, //更新后的引用值int expectedStamp, //旧的版本号int newStamp) //新的版本号,+1即可

提升高并发场景下 CAS 操作的性能

在争用激烈的场景下,会导致大量的CAS空自旋。比如,在大量的线程同时并发修改一个AtomicInteger时,可能有很多线程会不停地自旋,甚至有的线程会进入一个无限重复的循环中。大量的CAS空自旋会浪费大量的CPU资源,大大降低了程序的性能。大量的CAS操作还可能导致“总线风暴” 。在高并发场景下如何提升CAS操作性能呢?可以使用LongAdder替代AtomicInteger。

以空间换时间: LongAdder

Java 8提供一个新的类LongAdder,以空间换时间的方式提升高并发场景下CAS操作性能。LongAdder核心思想就是热点分离,与ConcurrentHashMap的设计思想类似:将value值分离成一个数组,当多线程访问时,通过Hash算法将线程映射到数组的一个元素进行操作;而获取最终的value结果时,则将数组的元素求和。LongAdder的内部成员包含一个base值和一个cells数组。在最初无竞争时,只操作base的值;当线程执行CAS失败后, 才初始cells数组,并为线程分配所对应的元素。 相当于分段乐观锁!

//下面这种是传统做法
//定义一个原子对象
AtomicLong atomicLong = new AtomicLong(0);
atomicLong.incrementAndGet();
sout(atomicLong.get());
//下面这种是LongAdder做法
//定义一个LongAdder 对象
LongAdder longAdder = new LongAdder();
longAdder.add(1);
sout(longAdder.longValue());

AtomicLong使用内部变量value保存着实际的long值,所有的操作都是针对该value变量进行。也就是说,在高并发环境下, value变量其实是一个热点,也就是N个线程竞争一个热点。重试线程越多,就意味着CAS的失败概率越高,从而进入恶性CAS空自旋状态。 LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽(元素)中,各个线程只对自己槽中的那个值进行CAS操作。这样热点就被分散了,冲突的概率就小很多。 使用LongAdder,即使线程数再多也不担心,各个线程会分配到多个元素上去更新,增加元素个数就可以降低 value的“热度”, AtomicLong中的恶性CAS空自旋就解决了。如果要获得完整的LongAdder存储的值,只要将各个槽中的变量值累加,返回最终的累加之后的值即可。LongAdder的实现思路与ConcurrentHashMap中分段锁基本原理非常相似,本质上都是不同的线程在不同的单元上进行操作,这样减少了线程竞争,提高了并发效率。

CAS优势与弊端:

CAS的优势主要有两点:

  • 属于无锁编程,线程不存在阻塞和唤醒这些重量级的操作。
  • 进程不存在用户态和内核态之间的运行切换,进程不需要承担频繁切换的开销。

CAS的弊端:

  • ABA问题。解决方法:版本号机制。JDK提供了AtomicStampedReference 版本号解决ABA问题。印戳 作为版本。
  • 只能保证一个共享变量之间的原子性操作 ,规避方法为:把多个共享变量合并成一个共享变量来操作。 规避方法合并成一个对象,JDK提供了AtomicReference类来保证引用对象之间的原子性 。
  • 无效CAS会带来开销问题,自旋CAS如果长时间不成功(不成功就一直循环执行,直到成功为止),就会给CPU带来非常大的执行开销。
提升CAS性能
  • 分散操作热点,使用LongAdder替代基础原子类AtomicLong, LongAdder将单个CAS热点(value值)分散到一个cells数组中。
  • 使用队列削峰,将发生CAS争用的线程加入一个队列中排队,降低CAS争用的激烈程度。自旋改成队列排队。

http://www.ppmy.cn/news/75977.html

相关文章

【饿了么UI】elementUI密码框图标实现睁眼和闭眼效果(阿里巴巴iconfront图标库vue项目本地引用)

elementUI中输入框的密码框属性&#xff0c; 默认是一个始终睁眼的图标&#xff0c;测试今天提bug要有闭眼效果&#xff08;无大语&#xff09;… 因为elementUI中的icon没有闭眼的&#xff0c;所以还要去iconfront下载引入 效果图&#xff1a; 点击后 一、下载图标 http…

Spring Security 中的过滤器链是什么?它的作用是什么

Spring Security是一个安全框架&#xff0c;它提供了强大的安全保护功能&#xff0c;可以帮助开发者更加方便地实现应用程序的安全性。Spring Security中的过滤器链是其中一个非常重要的部分&#xff0c;它起到了非常重要的作用。本文将介绍什么是Spring Security中的过滤器链&…

Linux基础内容(21)—— 进程消息队列和信号量

Linux基础内容&#xff08;20&#xff09;—— 共享内存_哈里沃克的博客-CSDN博客 目录 1.消息队列 1.定义 2.操作 2.信号量 1.定义 2.细节 3.延申 4.操作 3.IPC的特点共性 1.消息队列 1.定义 定义&#xff1a;是操作系统提供的内核级队列 2.操作 msgget&#xff1a;…

AC规则-4-规则和冲突解决

3.3 Introduction to Access Control Rule Conflict Resolution 3.3 访问控制规则冲突解决简介 本节从高层次讨论访问控制规则冲突解决。 本文档稍后会提供更多详细信息。 规则的优先级不是基于它在其他规则中的阅读顺序。 管理冲突规则的策略基于三个基本原则&#xff08;…

【Android】(最新)跑马灯文字水平滚动(79/100)

先上效果&#xff1a; Android系统中TextView实现跑马灯效果&#xff0c;必须具备以下几个条件&#xff1a; android:singleLine“true”android:ellipsize“marquee”android:marqueeRepeatLimit“marquee_forever”TextView必须单行显示&#xff0c;即内容必须超出TextView…

【LeetCode: 10. 正则表达式匹配 | 暴力递归=>记忆化搜索=>动态规划 】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

Linux——网络基础1

所谓 "局域网" 和 "广域网" 只是一个相对的概念. 比如, 我们有 "天朝特色" 的广域网, 也可以看做一个比较大的局域 网. 操作系统内部存在着多种协议,那么操作系统要管理这些协议吗? 是,管理方式:先描述,再组织。 协议本质就是软件,软件是可…

Linux系统vim查看文件中文乱码

Linux系统查看文件-cat中文正常显示 vim中文乱码 1、背景2、环境3、目的4、原因5、操作步骤5.1、修改vim编码配置 6、验证 1、背景 服务器部署业务过程中查看文件内容&#xff0c;使用cat 命令查看中文正常显示&#xff0c;使用vim命令查看显示中文乱码 cat 查看 vim 查看 …