并发小小梳理

缓存一致性协议

MESI
Ø M(Modified) 修改缓存,当前 CPU 缓存已经被修改,表示已经和内存中的
数据不一致了
Ø I(Invalid) 失效缓存,说明 CPU 的缓存已经不能使用了
Ø E(Exclusive) 独占缓存,当前 cpu 的缓存和内存中数据保持一直,而且其他
处理器没有缓存该数据
Ø S(Shared) 共享缓存,数据和内存中数据一致,并且该数据存在多个 cpu
缓存中
每个 Core 的 Cache 控制器不仅知道自己的读写操作,也监听其它 Cache 的读
写操作,嗅探(snooping)"协议
嗅探协议是:某个CPU(CPU A)发起本地写请求(Local Write),CPU A中的Cache Line保存了最新内存变量值以后,其状态被修改为Modified。随后,如果CPU B发起对同一个变量的读操作(Remote Read),则CPU A在总线上嗅探到这个读请求以后,先将Cache Line里修改过的数据回写(Write Back)到Memory中,然后在内存总线上放一份Cache Line的拷贝作为应答,最后再将自身的Cache Line的状态修改为Shared,由此产生的结果是CPU A与CPU B里对应的Cache Line的状态都为Shared。
CPU 的读取会遵循几个原则

  1. 如果缓存的状态是 I,那么就从内存中读取,否则直接从缓存读取。
  2. 如果缓存处于 M 或者 E 的 CPU 嗅探到其他 CPU 有读的操作,就把自己的缓
    存写入到内存,并把自己的状态设置为 S。
  3. 只有缓存状态是 M 或 E 的时候,CPU 才可以修改缓存中的数据,修改后,缓
    存状态变为 MC。

内存模型

Java 内存模型的主要目标是定义程序中各个变量的访问规
则,也就是在虚拟机中将变量存储到内存以及从内存中取出变量(这里的变
量,指的是共享变量,也就是实例对象、静态字段、数组对象等存储在堆内存
中的变量
。而对于局部变量这类的,属于线程私有,不会被共享)这类的底层
细节。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。

Java 内存模型定义了线程和内存的交互方式,在 JMM 抽象模型中,分为主内
存、工作内存。主内存是所有线程共享的,工作内存是每个线程独有的。线程
对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主
内存中的变量。并且不同的线程之间无法访问对方工作内存中的变量,线程间
的变量值的传递都需要通过主内存来完成,他们三者的交互关系如下。

所以,总的来说,JMM 是一种规范,目的是解决由于多线程通过共享内存进行
通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会
对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性
和有序性。

synchronized

synchronized扩后后面的对象是一把锁,在java中任意一个对象都可以成为锁

synchronized的字节码指令

通过javap -v 来查看对应代码的字节码指令,对于同步块的实现使用了monitorenter和monitorexit指令,他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。
monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权。

synchronized的锁的原理

dk1.6以后对synchronized锁进行了优化,包含偏向锁、轻量级锁、重量级锁

Java对象头

在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit)
在这里插入图片描述
每个 Java Object 在 JVM 内部都有一个 native 的 C++ 对象 oop/oopDesc 与之对应。oopDesc中定义的markOop。
在这里插入图片描述
所有的Java对象是天生的Monitor,每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象。

锁升级和获取过程

锁的级别从低到高逐步升级, 无锁->偏向锁->轻量级锁->重量级锁.
自旋锁(CAS)
自旋锁就是让不满足条件的线程等待一段时间,而不是立即挂起。如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程,如果失败,升级为轻量级锁

轻量级锁

引入轻量级锁的主要目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。虚拟机将使用CAS操作尝试将对象Mark Word中的Lock Word更新为指向当前线程Lock Record的指针,如果成功,设置为轻量级锁的状态,如果失败,升级为重量级锁。

重量级锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
在这里插入图片描述

wait和notify

都需要获取监视器锁
调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁。然后当其他线程调用notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值