欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

java并发编程的艺术-2-并发机制的底层实现原理

时间:2023-07-31
2.1 volatile的应用

volatile和synchronized的轻重关系

volatile不会引起线程上下文切换和调度

可见性的含义:

可见性处理的是线程之间的可见性
对象是线程之间的共享变量

以下分析基于intel处理器

1、volatile的定义与原理

volatile是排他锁的轻量替代品
一些术语

缓存行缓存中的最小存储单位原子操作不可中断的一系列操作缓存行填充将内容数据填充到缓存中(比如L1、L2、L3中的一个或几个)缓存命中处理器需要的数据正好在缓存中,就直接从缓存中读取数据而非内存写命中处理器将处理结果写回到缓存中而不是内存中

情景:cpuA正在执行threadA,threadA将要对一个变量name进行修改,在源代码中此变量由volatile修饰,编译后生成一条包含LOCK指令的语句。现在name变量本尊在内存中放着。

缓存行填充:cpuA将内存中的name变量取到缓存cacheA中,但尚未进行修改。缓存行填充:cpuB也将name变量取到自己的缓存cacheB中,但尚未进行运算。cpuA对name进行运算。(此时cpu注意到了LOCK指令,LOCK指令开始生效)写命中:cpuA将结果写回缓存cacheA。LOCK指令的效果:锁总线。在后来的cpu中,不使用锁总线而是锁缓存:

cacheA中的name立即写回到内存,覆盖内存中的name在MESI缓存一致性协议的作用下,发生了“锁缓存现象”:cacheB中的name数据所在的缓存行失效。cpuB要对name进行修改,发现缓存不命中,因为已经失效,于是重新从系统内存中读取数据name。

什么是MESI控制协议?

在多线程并发的环境中,一个cpu应当考虑:我的缓存、别人的缓存、内存:这三方的一致问题。MESI控制协议就是用来解决这个问题的。
基于嗅探技术,实现缓存行四种状态的转换。

如何理解linkedTransferQueue将头结点和为节点扩展到64字节的行为?

对于一些以64字节作为缓存行长度的cpu来说,如果不扩展,那么可能头结点和尾节点进入同一缓存行,针对头结点锁缓存,就把本不需要锁的尾节点也给锁住了。
不能滥用,因为会带来性能损耗。更多的数据进入缓存行。

2.2、synchronized的原理与应用 2.2.1 java对象头

如何理解“Java中的每一个对象都可以作为锁”?
synchronized的三种使用形式:锁普通方法、锁静态方法、锁方法块:中,分别锁的是什么?

public synchronized void hello(){} //锁的是hello方法所在的对象public static synchronized void hello(){} //锁的是方法所在类synchronized(this){} //锁的是括号里面的对象

对象和monitor的关系是什么?#
monitorenter和monitorexit指令让cpu做什么?
java对象头结构:

首先:头的长度:数组对象3个字,非数组对象2个字。其次:头的内容:

数组对象:Mark Word ; Class metadata Address ; Array Length:标记、类地址、数组长度。非数组对象:只有标记、类地址。 最重要的一部分:标记

对象的五种锁状态,对应五种不同的Mark Word结构:无锁、轻量级锁、重量级锁、GC标记、偏向锁32位系统无锁状态对应的Mark Word结构:hashcode、分代年龄、0、01 2.2.2 锁的升级与对比

在1.6中有哪四种锁?

无锁、偏向锁、轻量级锁、重量级锁(级别递增)

将锁设计为只能升级不能降级的策略的目的?#

1 偏向锁

基于以下情况来设计:在大多数情况下,锁不存在多线程竞争,而总是由同一线程多次获得。所以我们应该让同一线程多次获得锁的代价尽可能地小,据此设计出了偏向锁。

“偏向”的含义

让对象偏向一个线程:
具体实现:对象头中记录线程ID

“偏向”建立两步走:确认“偏向锁”标识,让锁“偏向”本线程

测试是否已经偏向本线程测试偏向锁标识

若没有设置:CAS设置若设置了:CAS进一步设置对象头,“偏向”本线程

偏向锁撤销的时机

时机:出现竞争,且全局安全点
全局安全点:所有的工作线程停止执行
这也决定了偏向锁的使用场景是:竞争很少。如果存在大量竞争,那么就会频繁出现偏向锁的撤销行为,而这个行为需要等待全局安全点,所以工作线程暂停,影响系统工作效率。

偏向锁撤销的流程
源码解析

if 对象不是偏向锁then 直接返回// 对象是偏向锁if 无偏向线程if 不允许重偏向then 设置无锁//有偏向线程,先判断存活if 如果当前线程是偏向线程else (当前线程不是偏向线程)判断偏向线程是否存活if 不存活if 允许重偏向then 设置mark word为匿名偏向else (不允许重偏向)设置无锁//有存活的偏向线程if 偏向所有者正在持有锁升级为轻量级锁,处理锁重入情况else (偏向所有者不正在持有锁)if 运行重偏向then 设置为匿名偏向else 设置无锁

总结:升级为轻量级锁的前提是:
1、对象是偏向锁
2、有存活的偏向线程
3、偏向所有者正在持有锁

注意:之前由于出现竞争,多个线程阻塞在全局安全点上,现在竞争解决后(不管是升级成为了轻量级锁还是恢复为无锁),之前阻塞的线程会继续执行。

2 轻量级锁

加锁过程:
线程执行到了同步块
创建空间、复制Mark、Mark替换
若失败,自旋

以下参考
轻量级锁分为:

自旋锁

不会挂起线程,而是原地自旋等待,期望锁在较短时间能能被释放为自己所用。自旋过程会消耗cpu,所以不适用于等待时间过久的情况。可以通过设置一个固定的:“最大自选次数”,来避免过度占用cpu,默认这个数字是10。一旦超过这个数字,锁升级为重量级锁。 自适应自旋锁

记忆线程和锁之间的关系。如果一个线程曾经多次获得过一个锁,倾向于认为未来它有更大可能会再次获得这个锁,于是虚拟机延长该线程自旋次数。反之,如果一个线程过去很少获得该锁,虚拟机倾向于认为未来它也很难获取到该锁,有可能直接忽略这个线程的自旋过程,直接升级为重量级锁。
一旦升级为重量级锁,那么原来已经通过“占用轻量级锁”方式占用锁的线程就无法使用CAS操作来“解除轻量级锁”,因为锁已经变了,同一把钥匙打不开了。所以解锁失败后会采用解重量级锁的方式来解锁,也就是说:释放锁,唤醒等待的线程
轻重量级锁究竟不同在哪里?
3 总结两个升级过程:


4 重量级锁

参考
依赖于对象内部的monitor锁来实现
依赖于操作系统的Mutex(互斥) Lock来实现
涉及到线程的阻塞和唤醒,用户态和内核态的转变。

锁优点缺点场景偏向锁和非同步方法相比仅存在纳秒级差距一旦出现锁竞争,需要进行锁撤销,消耗较大只有一个线程访问同步块轻量级锁线程竞争采用自旋,不涉及线程阻塞和唤醒,响应快自旋消耗cpu追求响应时间重量级锁线程竞争采用阻塞,不会自旋消耗cpu涉及线程阻塞和唤醒,响应时间慢,上下文切换开销大追求吞吐量2.3 原子操作的实现原理

(以下操作基于intel处理器)
概念1:CPU流水线。将一条指令分为5-6步后,交给CPU中5-6个相应功能的电路单元来处理。于是可以在前一个指令尚未完成执行时,就开始下一条指令的执行。

最最基本的内存操作,比如从内存中读取或者写入一个字节,它的原子性由处理器保证。
而对于复杂的内存操作,处理器只提供机制,不保证自动实现原子性。

1 机制一:总线锁

情景:
多处理器同时“读改写”一个共享变量,比如(同时执行i++),违反原子性。

这里“同时”的含义,多个CPU缓存了同一份变量。

实现:
处理器提供一个LOCK#信号。锁总线。
总线是处理器和内存沟通的桥梁。
总线锁住,则其他CPU无法访问总线,那么本CPU就可以独占共享内存

2 机制二:缓存锁

缓存锁的设计避免了总线锁的弊端

总线锁导致其他处理器无法操作其他内存地址的数据,而我们最初的需求,仅仅是不想让其他处理器操作指定内存地址的数据

实现:
首先,缓存锁依赖于CPU内部的三级高速缓存:L1 L2 L3。
参考
一个CPU将内存区域缓存到缓存中后,通过缓存一致性协议保证原子性。如果同时其他CPU已经对同一内存进行缓存,那么后者相应的缓存行被设置为无效。

不使用缓存锁定的情况
1、不能缓存,或跨缓存行缓存。这是采用总线锁定。
2、处理器支持缓存锁定。

3 Java实现原子操作 循环+CAS

基于处理器的CMPXCHG指令。循环的原因是CAS可能会失败。失败是因为compare阶段失败。失败后循环直到成功。

值得注意的是,CMPXCHG是一个LOCK前缀的指令,会在内存区域加锁。这体现出CAS和volatile的联系。

CAS三大问题

ABA问题

解决方式:追加版本号。
1.5开始为Atomic包提供一个类AtomicStampedReference来解决这个问题。
compare的时候需要比较两个数据:Reference和Stamp。

其中Reference相当于数据本身,它可以辨别A和B的区别,而不能辨别第一个A和第二个A的区别。Stamp相当于版本号,它可以辨别第一个A和第二个A的区别。 自旋消耗的问题

自旋就会引起CPU开销。
书中提到了pause指令,这部分没搞懂。

只能保证一个共享变量的原子操作

在1.5之后,引入了AtomicReference来保证对象操作的原子性,可以将多个变量放在一个对象中进行CAS操作

JVM中的锁与CAS

在JVM中,除了偏向锁,其他的锁的获取和释放都使用了循环CAS。

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。