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

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个相应功能的电路单元来处理。于是可以在前一个指令尚未完成执行时,就开始下一条指令的执行。
CPU流水线
最最基本的内存操作,比如从内存中读取或者写入一个字节,它的原子性由处理器保证。
而对于复杂的内存操作,处理器只提供机制,不保证自动实现原子性。

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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值