黑马java基础_Java基础学习:Java无锁并发

无锁并发

1. 问题提出

有如下需求,保证 account.withdraw 取款方法的线程安全

ee16612a3c65aa2f61dc0018e14bfb9f.png

原有实现并不是线程安全的

966404268c30716b9478db2094bf207d.png

执行测试代码

d848332d856c744ef2ffbf2224f5251a.png

某次的执行结果

330 cost: 306 ms

1.1 为什么不安全

withdraw 方法

publicvoidwithdraw(Integer amount){ balance -= amount;}

对应的字节码

90dbc68e27210fdbe6599c365a22ac20.png

多线程执行流程

978aa3b4e0a9f20d64d5e99ff1f2fd7d.png

单核的指令交错

多核的指令交错

1.2 解决思路-锁

首先想到的是给 Account 对象加锁

626d292c45986f6d5da3a8dbfa38ac90.png

结果为

0 cost: 399 ms

1.3 解决思路-无锁

b97adcbb3c92f03302d63f8b49c3e950.png

执行测试代码

publicstaticvoidmain(String[] args){ Account.demo(newAccountSafe(10000));}

某次的执行结果

0 cost: 302 ms

2. volatile 与 CAS

33f945d4a96007448c877de35a2daf81.png

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

注意 其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。

在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。2.1 慢动作分析

d66024a68202232328956690771a4434.png

输出结果

304082270b164038403d1dade23df525.png

2.2 为什么无锁效率高

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

725f95f0b36482fa2117907b35642584.png

【可运行状态】指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行【运行状态】指获取了 CPU 时间片运行中的状态当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换【阻塞状态】如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们

490556ce019ceeaaf8c4b6e8a1b1de59.png

RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)BLOCKED,WAITING,TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述2.3 volatile

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

为什么需要 volatile?

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

b55f54151549912c2ac97414920ac8fe.png

为什么呢?分析一下:

初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。

c7181c9fd75979dde96240016f0a771c.png

因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率

46b69c8ef2d4a35a79531c4389584fef.png

1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

4a7c08f8d35317e032afc8e04652db01.png

可见性

volatile(易变关键字)

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

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

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

2.4 CAS 的特点

结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。

CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响3. 原子整数

JUC 并发包提供了:

AtomicBooleanAtomicIntegerAtomicLong以 AtomicInteger 为例

ac69f0450d0171eb0260f2e12cd74d8f.png

4. 原子引用

为什么需要引用类型?

AtomicReferenceAtomicMarkableReferenceAtomicStampedReference有如下方法

0bb412613f2b4ac4725bff5396c3b035.png

试着提供不同的 DecimalAccount 实现,实现安全的取款操作

4.1 不安全实现

3b23cc85a9e2a61e128834339e9d9700.png

4.2 安全实现-使用锁

2b5245826430e5aaff7a0e5c99408683.png

4.3 安全实现-使用 CAS

5dfa668711424429e9e747a61f2f7077.png

测试代码

463fcc69d343a73108d68e433a51f9dd.png

运行结果

4310 cost: 425 ms0 cost: 285 ms0 cost: 274 ms

4.4 ABA 问题及解决

ABA 问题

7adad27d3e36a02d86bd2cbe98723b05.png

输出

332129af25f4eccc6fe77f9b6c364b67.png

总的解决思路是,使用 CAS 修改前想办法检查,别人动过没 ? 修改失败 : 修改成功,怎么检查别人动过没呢?不能光比较 A,还要进行一个额外的检查

AtomicStampedReference

5cce7a243c122c5ae6140d5e85f350dd.png

输出为

843ca37e15592256d60bdce4daee270e.png

AtomicStampedReference 可以给引用加上版本号,追踪引用的整个变化过程,如: A -> B -> A -> C,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。

但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 AtomicMarkableReference

AtomicMarkableReference

36029b6eb98591cd152aa9b55b455c1f.png

08589be5941e20bfa16acc85f59fa903.png

输出

029c2b9ad09b97815e77c5c51cfb7135.png

可以注释掉打扫卫生线程代码,再观察输出

5. 原子累加器

5.1 累加器性能比较

643c578013a1ded4edd8533248f85de5.png

比较 AtomicLong 与 LongAdder

3cfc713bc139a3bc480eda6707d16e58.png

输出

e24d73170e36e60d25b47ef31ac0c04e.png

性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。

5.2 LongAdder 原理分析

LongAdder 是并发大师 @author Doug Lea 的作品,设计的非常精巧

LongAdder 类有几个关键域

638698cc701ba192b156feecdff05cf2.png

其中 Cell 即为累加单元

cab043fd755dff01326d569b570c6b56.png

伪共享问题

速度比较

5f26f8702fb2f6419d76bef1b8a83d1c.png

32f3a46b455cd0e1d1b0737660be1343.png

因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。

而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)

缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中

CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效

74c06d03594178c990be289e8dc92295.png

因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:

Core-0 要修改 Cell[0]Core-1 要修改 Cell[1]无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加 Cell[0]=6001, Cell[1]=8000,这时会让 Core-1 的缓存行失效

@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

f21f87b199979600d38da01d4f41e5e6.png

累加主要调用下面的方法

54367573bedce440a138ef5d447a928b.png

add 流程图

fbe66f586f159fdf17b0d1ef8a9d813b.png

6974f20d32c21d003d5ce4846674b2c6.png

e0703989d22ae2b57f708b93c28b8dc1.png

longAccumulate 流程图

每个线程刚进入 longAccumulate 时,会尝试对应一个 cell 对象(找到一个坑位)

8ce33d4e67a5a37596687b9662f5cddd.png

获取最终结果通过 sum 方法

d50121346648d1e58a7eaefcdf5c2ffc.png

6. Unsafe

6.1 概述

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得

0258fe209dbb183e336be165038ed3c6.png

6.2 Unsafe CAS 操作

80e884edc6b85459ba4287f9f13f3637.png

输出

Student(id=20, name=张三)

使用自定义的 AtomicData 实现之前线程安全的原子整数 Account 实现

fa6a7c4b5ff7d430a85c8fe01e4f33ab.png

Account 实现

81d7efb42071441fe09da3553370a817.png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值