理解Java并发(一)JVM层面对于并发问题的解决

引言:在JUC没有出现之前,Java提供了volatile,final,synchronized 等同步原语给程序员来解决并发问题,探究这些同步原语的实现有利于理清解决线程并发问题的思路和理解JUC的实现。

并发和并行

并发是指多个线程执行操作同一块共享内存的指令的过程
并行是指多个线程执行操作不共享内存的指令的过程

从解决并发问题需要的特性到同步原语

并发问题是指在多线程的场景下,由于操作系统的线程调度、处理器指令重排序和多级缓存,程序的执行结果和程序编程所期望的结果并不一致。
解决并发相关的问题一般需要使用同步原语来保证某一操作或者某些操作的原子性、线程间可见性和这些操作在单线程上的有序性等三个特性。
值得一提的是,由于Java是跨平台语言,不同的平台和虚拟机系统实现上述三种特性的方式略有差异,为了帮助程序员理解Java并发模型,Java通过JMM向程序员保证了相关同步原语特性的统一实现,向下屏蔽了不同类型的虚拟机系统,为程序员提供了统一的内存视图和规范。
相较于顺序一致性内存模型,Java内存模型留给了编译器和处理器重排序的空间,而对于happens-before内存模型,Java内存模型又提供了最小安全保证(充分的同步边缘)来防止数值的“凭空出现”,这个内容在JSR-133规范中有提及。
JSR-133规范中文版地址:
http://ifeve.com/wp-content/uploads/2014/03/JSR133%E4%B8%AD%E6%96%87%E7%89%881.pdf

volatile的实现

1.volatile的特性:
(1)保证volatile变量读写指令在多处理器下的原子性
(2)禁止对volatile变量读写指令重排序
(3)保证volatile变量读写指令对多线程的可见性
2.在JMM中的规定:volatile变量的写入操作happens-before该变量的读取操作,即线程在读取volatile变量时,总能看到另外一个线程对这个变量的最后写入。并且根据happens-before规则的传递性可知当前线程可以看到另外一个线程在写入这个volatile变量前所有共享变量的写入和读取操作。
3.在JMM中的实现:读写操作前后会插入JMM层面规定的内存屏障。
4.在X86中的实现:
(1)使用缓存一致性协议,在当前CPU中的变量缓存写入物理机主内存中之前,其他CPU无法在主内存中写入这个变量,并且操作结束之后使得其他CPU的缓存失效,必须从物理机主内存中读取数据。这样子同步了缓存和主内存。较早的还有总线锁定的方法,向其他处理器发送lock信号,声明自己独享主内存。
(2)禁止lock前缀指令之前和之后的指令重排序
关于MESI协议的博客:https://www.cnblogs.com/ynyhl/p/12119690.html

final的实现

1.final的特性:保证final修饰过的变量被任意线程可见前,已经初始化过。
2.final的JMM实现:
(1)在final域的赋值指令和构造函数return结束语句之间插入StoreStore屏障,禁止final域的赋值指令重排序到构造函数之外
(2)在读取对象引用指令和读取final域指令之间插入LoadLoad屏障,保证在读取final域时,先读取包含该final域的对象
3.在X86中的实现:
X86内存模型本身就禁止对LoadLoad和StoreStore的重排序,所以Java代码在翻译成汇编指令的时候,不会插入任何内存屏障。

synchronized重量级锁实现

在重量级锁的实现中,当对象被synchronized锁定后,对象头的Mark Word会指向ObjectMonitor,ObjectMonitor是Java语言中管程概念的具体实现,在ObjectMonitor中包含一系列变量和数据结构来维护线程对于资源的争夺过程。
在ObjectMonitor中_owner字段指向持有ObjectMonitor的线程,可以这么说,线程对于资源(锁)的争夺就是用CAS同步原语改变_owner字段指向自己的过程。
在ObjectMonitor中的_EntryList字段用来维护通过monitorenter进入了临界区还没有获取到资源的线程,这些线程处于阻塞状态,有点类似AQS中的同步队列,但是同步队列中的线程是处于等待状态。
在ObjectMonitor中的_WaitSet字段用来维护获取到资源,但是由于某些原因,需要释放资源的线程。通过wait方法使得这些线程进入WaitSet,通过notify/notifyAll唤醒线程加入EntryList重新争夺资源。

synchronized锁升级

由于使用重量级锁的代价太高,所以在Java中引入了锁升级机制。synchronized有四种锁状态,分别是无锁,偏向锁,轻量级锁,重量级锁。
在偏向锁的状态下,Mark Word并不会指向ObjectMonitor,而是有一个标记位记录下第一个获取资源的线程ID。第二个线程进入时比对当前线程ID和Mark Word中记录的线程ID,如果相同就获取资源,如果不相同,说明此时发生线程竞争,就升级为轻量级锁。需要说明的是对于同一个资源,CAS只有第一次会成功,即CAS参数中的旧的预期值等于Mark Word中的初始化值。
在这里插入图片描述

在轻量级锁状态下,线程进入临界区时,在当前线程的私有内存中创建Lock Record记录要争夺的资源信息,然后通过CAS的方式更新Mard Word中的指针指向当前线程的Lock Record,如果成功,代表当前线程获取了资源,如果失败,就进入自旋。
在自旋状态下,线程进行多次CAS操作,如果自旋成功就获取资源,如果自旋失败就升级为重量级锁。
在这里插入图片描述

synchronized锁优化

1.自适应自旋锁
根据线程的等待情况来控制自旋的次数和时间,如果等待时间较长,减少自旋次数。
2.逃逸分析与锁消除
如果JVM判断对象没有被外部引用,不用同步机制保证线程安全,就会把不必要的同步机制消除,例如发现StringBuffer对象没有发生“逃逸”,就会把StringBuffer中append方法上的synchronized修饰符去除。
3.锁粗化
多次连续加锁和解锁的操作可以简化成一次加锁和解锁,例如StringBuffer对象连续多次的append,可以优化成第一个append方法加锁,最后一个append方法解锁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值