JUC并发编程笔记(中)Volatile、CAS、LongAdder

本文探讨了Java中volatile关键字、synchronized的内存模型特性,比较了volatile和synchronized在保证可见性和有序性方面的区别,以及指令重排的作用和优化。还介绍了CAS和Atomic类在并发控制中的应用,包括ABA问题的解决方案和无锁并发的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

方法一:为变量添加修饰:volatile(易变化关键字)

volatile static boolean isrun = true ;

这样做的目的是:加上volatile 的变量,每次循环都是只能在主存当中获取,不会从高速缓存区中获取!

测试结果:T1线程停止

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-asyXIqPV-1634140010840)(JUC并发编程.assets/image-20211010192206659.png)]

方法二:使用synchronized

在Java内存模型中,synchronized规定,线程在加锁时, 先清空工作内存→在主内存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。(线程获得对象锁后,会清空工作区内存,重新在主存中获取!)

可见性VS原子性

重点区分:volatile和synchronized ;

  • 我们的volatile只能保证线程看到的变量是实时的,但是并不能保证是安全的!

  • 多个线程同时访问,即使被volatile修饰,仍然可能会出现指令交错问题!

有序性

JVM会在不影响正确的条件下,调整语句的执行顺序!这种特性称作【指令重排】

//如下i和j的++操作调换顺序不影响结果!

public class ReSortTest {

static int i = 0 ;

static int j = 0 ;

public static void main(String[] args) {

i++; //修改为j++

j++; //修改为i++

}

}

思考:正常执行是正确的,而且多线程条件下指令重排可能是会出现问题的,为什么要进行指令重排的优化呢?

指令重排序优化

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令—指令译码—执行指令—内存访问—数据写回这5个阶段

重排之前:指令串行执行!

现代CPU支持多级指令流水线,例如支持同时执行取指令~指令译码–执行指令–内存访问–数据写回的处理器,就可以称之为五级指令流水线。这时CPU可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC =1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。

重拍之后:指令并行执行 !

总结:指令级别的优化,我们线程的不同指令的不同阶段可同时进行!【指令级别的并发】

重排序的目的:为的是一个指令执行某一个阶段的时候,通过重排序,让其他执行执行其他的阶段!达到最大的指令并发!

当然前提是:重排互不影响结果 !

public class ReSortTest {

static int i = 0 ;

static int j = 0 ;

public static void main(String[] args) {

i++; //2条指令可重排序!

j++;

i= j - 10 ; //不可重排序,会影响结果

j++ ;

}

}

禁止指令重排序

可以使用volatile实现,因为volatile可以使得被修饰的变量之前的操作是不会被重排序的

Volatile原理 *

以上可以了解到Volatile可以保证共享变量的有序性、可见性 , 我们接下来了解一下原理 ;

volatile的底层实现原理是内存屏障,Memory Barrier (Memory Fence)

  • 对volatile变量的写指令后会加入写屏障 ;

  • 对volatile变量的读指令前会加入读屏障 ;

1、如何保证的可见性
  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

public class ReSortTest {

static int i = 0 ;

volatile static int j = 0 ;

public static void main(String[] args) {

i++;

j++; //由于j是volatile修饰的,此处对j进行写操作,所以插入写屏障 !

//所以j++ 以及之前的代码全部会被同步到主存当中

}

}

  • 读屏障(lfence)保证的是在该屏障之前的,对共享变量的改动,都同步到主存当中!

public class ReSortTest {

volatile static int j = 0 ;

public static void main(String[] args) {

if(j > 1){ //读取j ,在此之前的代码插入读屏障!(保证读取到的是主存中最新的数据!)

}

}

}

2、如何保证有序性
  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

public class ReSortTest {

static int i = 0 ;

volatile static int j = 0 ;

public static void main(String[] args) {

i++;

j++; //由于j是volatile修饰的,此处对j进行写操作,所以插入写屏障 !

//所以j++ 以及之前的代码全部会被同步到主存当中

//写屏障 , 之前的代码不会发生指令重排序!

}

}

  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

public class ReSortTest {

volatile static int j = 0 ;

public static void main(String[] args) {

//读屏障:之后的代码不会被指令重排序

if(j > 1){ //读取j ,在此之前的代码插入读屏障!(保证读取到的是主存中最新的数据!)

}

}

}

总结:

  • 读屏障之后的代码不会发产生指令重排序、而且读到的都是主存中的数据

  • 写屏障之前的代码不会发生指令重排序、而且之前的代码会全部更新在主存当中!

虽然能解决可见性和有序性,但是仍然不能解决指令交错问题(原子性) ;

3、DCL(Double Check Locking)问题的分析、纠正、解决

happens before规则

七大规则(保证共享变量可见性的七种方法)!

五、共享模型之无锁


CAS + Volatile 无锁实现并发,保证线程安全(乐观锁)

CAS的工作方式

CAS (Compare And Set) : 比较并设置

//测试代码!

static AtomicInteger baclace ;

public void withdraw(Integer amount){

while(true){

int pre = balance.get() ;

int next = pre - amount ;

if (balance.ComapreAndSet(pre,next)) break ; //比较并设置设置值

}

}

//多个线程访问如下方法

其中ComapreAndSet,简称就是CAS(也有Compare And Swap的说法) ,它必须是原子操作!

当CAS方法执行时,prev 会与主存的实时balance比较一次,如果发现不一致(其他线程修改了),那么就返回false ;

//源码

public final boolean compareAndSet(int expect, int update) {

return unsafe.compareAndSwapInt(this, valueOffset, expect, update); //执行cas时expect会与自身value比较

}

CAS 与 volatile

获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。即一个线程对volatile变量的修改,对另一个线程可见。

注意

volatile仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)

//在我们原子整数当中,value都是被volatile修饰过的!

private volatile int value;

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

为什么无锁效率高?

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。

而且最好用于线程数少于核心数的情况,线程数多的话CAS所在线程分不到时间片依然会进行上下文切换!

总结:因为CAS无锁保证线程安全的话,线程不会说会受到其他线程的影响陷入BLOCK阻塞状态,而是多个线程都会操作共享对象,但是cas会一直比较保证线程安全,线程是不会停止的,sync有锁方式则会出现一个线程获得锁,其他线程只能陷入BLOCK状态等待!

CAS的特点

  • 结合CAS和volatile可以实现无锁并发,适用于线程数少、多核CPU的场景下。

  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我点再重试呗。

  • synchronized是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们想改,我改完了解开锁,你们才有机会。

  • CAS体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思

  • 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一·

  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

原子整数

JUC的子包 java.util.concurrent.atomic 提供了

  • AtomicInteger

  • AtomicBoolean

  • AtomicLong

AtomicInteger为例:

AtomicInteger i = new AtomicInteger(0);

//下边方法属于原子方法,线程安全的!

System.out.println(i.getAndIncrement()); // 结果为 0 等价 i ++ (线程不安全的!)

System.out.println(i.incrementAndGet()); // 结果为 2 等价 ++ i

System.out.println(i.getAndAdd(5));// 结果2

System.out.println(i.addAndGet(5));// 结果12

读取到的 要更改为

System.out.println(i.updateAndGet(x -> x * 10)); //输出 50

//本质都是compare and set ;

原子引用

除了保护我们的基本类型,还可以保护BigDecimal这种引用类型 ;

//测试代码!

private AtomicReference baclace ; //外加一层AtomicReference

public void withdraw(Integer amount){

while(true){

BigDecimal pre = balance.get() ;

BigDecimal next = pre.subtract(amount) ; //引用数据类型减法

if (balance.ComapreAndSet(pre,next)) break ; //比较并设置设置值

}

}

BigDecimal decimal = new BigDecimal(“1000”); //初始化时最好传递的时字符串!

ABA问题

我们都知道我们cas保证的时最新的值和pre是否相等来判断是否被修改,但是存在这么一种情况:值被修改但是,修改后还是跟pre一致,这种情况,cas则无法判断是否被修改过 ;(虽然对业务无影响,但是仍是个隐患!)

AtomicStampedReference

因此,为了解决ABA这种问题引入

AtomicStampedReference str = new AtomicStampedReference<>(“a”,0); // 0相当于版本号,只要修改过就会 + 1

//除了比较值是否相等还会比较版本号,版本号会记录改过的次数

AtomicMarkableReference

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

那么如何才能正确的掌握Redis呢?

为了让大家能够在Redis上能够加深,所以这次给大家准备了一些Redis的学习资料,还有一些大厂的面试题,包括以下这些面试题

  • 并发编程面试题汇总

  • JVM面试题汇总

  • Netty常被问到的那些面试题汇总

  • Tomcat面试题整理汇总

  • Mysql面试题汇总

  • Spring源码深度解析

  • Mybatis常见面试题汇总

  • Nginx那些面试题汇总

  • Zookeeper面试题汇总

  • RabbitMQ常见面试题汇总

JVM常频面试:

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Mysql面试题汇总(一)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Mysql面试题汇总(二)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Redis常见面试题汇总(300+题)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
tyle=“zoom: 33%;” />

那么如何才能正确的掌握Redis呢?

为了让大家能够在Redis上能够加深,所以这次给大家准备了一些Redis的学习资料,还有一些大厂的面试题,包括以下这些面试题

  • 并发编程面试题汇总

  • JVM面试题汇总

  • Netty常被问到的那些面试题汇总

  • Tomcat面试题整理汇总

  • Mysql面试题汇总

  • Spring源码深度解析

  • Mybatis常见面试题汇总

  • Nginx那些面试题汇总

  • Zookeeper面试题汇总

  • RabbitMQ常见面试题汇总

JVM常频面试:

[外链图片转存中…(img-ahvpVT97-1712037963588)]

Mysql面试题汇总(一)

[外链图片转存中…(img-XNRj16D3-1712037963589)]

Mysql面试题汇总(二)

[外链图片转存中…(img-JNUYOMRp-1712037963589)]

Redis常见面试题汇总(300+题)

[外链图片转存中…(img-rHpvHVAQ-1712037963589)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值