《Java并发编程》- synchronized关键字底层实现原理分析

临界区

什么是临界区

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源

  • 一个程序运行多个线程本身没有问题
  • 多个线程读共享资源也没有问题
  • 多个线程对共享资源读写操作时会发生指令交错,就会出问题

竞态条件

多个线程在临界区内执行,由于代码的执行序列不同导致结果无法预测,称之为发生了竞态条件。
为了避免竞态条件发生,有多种手段可以达到目的:
阻塞式的解决方案:synchronized、Lock
非阻塞式:原子变量

synchronized的使用

加锁方式

synchronized同步块是Java提供的一种原子性内置锁,Java中每个对象都可以把它当做一个同步锁来使用,这些Java内置的使用者看不到的锁称为内置锁,也叫作监视器锁
加锁方式

分类具体分类被锁的对象伪代码
方法实例方法类的实例对象public synchronized void method (){…}
方法静态方法类对象public static synchronized void methis (){…}
代码块实例对象类的实例对象synchronized (this){…}
代码块class对象类对象synchronized (TestDemo.class){…}
代码块任意实例对象实例对象Objectsynchronized (newObject){…}
本质:方法上(acc_synchronized) 代码块(monitorenter monitorexit)

解决之前的共享问题

public static void increment() { 
    synchronized (lock){
        counter++; 
    }
}

synchronized底层原理

synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥源语Mutex(互斥量),它是一个重量级锁,性能较低。当然内置锁在1.5之后版本做了重大的优化,来减少所得开销,内置锁的并发性能已经基本与Lock持平,如:

  • 锁粗化(Lock Coarsening)
  • 锁消除(Lock Elimination)
  • 轻量级锁(Lightwright Locking)
  • 偏向锁(Biased Locking)
  • 自适应自旋(Adaaptive Spinning)

Java虚拟机通过一个同步结构支持方法和方法中的指令序列的同步:monitor
同步方法是通过方法中access_flags中设置ACC_SYNCHRONIED标志来实现;同步代码块是通过monitorenter和monitorexit来实现。两个指令的执行时JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大的影响。

查看sychronized的字节码指令序列

image.png

Monitor(管程/监视器)

管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发,synchronized和wait()、notify()、notifyAll()都是Java中管程技术的组成部分

MESA模型

最广泛使用的管程模型image.png

管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。

核心概念:入口等待队列(互斥) 多个条件队列(同步 阻塞唤醒机制)
wait()的正确使用方式

对于MESA来说,有一个编程范式:

while(条件不满足) {
    wait();
}
JVM层面对管程的实现

参照synchroized,组成部分如下:
ObjectMonitor
cxq(cas owner)
waitset(wait/notify,notifyAll)

Java层面对管程的实现

AQS抽象层:
同步等待队列(cas volatile int state) 入队,出队 加锁,解锁
条件等待队列(Condition await/signal,signalAll) 入队,出队

Java语言内置管程synchronized

Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量

Monitor机制在Java中的实现

java.lang.Object类定义了wait(),notify(),notifyAll()方法,这些方法的具体实现,依赖于ObjectMonitor,这是JVM基于C++实现的一套机制(hotspot源码ObjectMonitor.hpp)

ObjectMonitor() { 
    _header = NULL; //对象头 markOop 
    _count = 0; 
    _waiters = 0, 
    _recursions = 0; // 锁的重入次数 
    _object = NULL; //存储锁对象 
    _owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
    _WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点 
    _WaitSetLock = 0 ; 
    _Responsible = NULL ; 
    _succ = NULL ; 
    _cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构) 
    FreeNext = NULL ; 
    _EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失 败的线程) 
    _SpinFreq = 0 ; 
    _SpinClock = 0 ; 
    OwnerIsThread = 0 ; 
    _previous_owner_tid = 0; 
}

image.png

在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认策略(
QMode=0)是:如果EntryList为空,则将
cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取
锁。_EntryList不为空,直接从_EntryList中唤醒线程。
因此:获取锁是非公平的

对象的内存布局

Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头详解

详情参考 JVM-2-JVM内存模型与优化

Mark Wrok是如何记录锁状态的

Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。简单点理解就是:MarkWord 结构搞得这么复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处。

Mark Word的结构

image.png

  • hash: 保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。
  • age: 保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
  • biased_lock: 偏向锁标识位。由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
  • lock: 锁状态标识位。区分锁状态,比如11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
  • JavaThread:保存持有偏向锁的线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。epoch: 保存偏向时间戳。偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。

Mark Word中锁标记枚举

image.png

关于锁的总结

偏向锁:不存在竞争,偏向某个线程 thread1后续进入同步块的逻辑没有加锁解锁的开销,不存在 无锁->轻量级锁
轻量级锁:线程间存在轻微的竞争(线程交替执行,临界区罗技简单) CAS获取锁(无自旋),失败膨胀
重量级锁:多线程竞争激烈的场景,膨胀期间创建一个monitor对象,CAS自旋,阻塞

利用JOL跟踪锁标记变化

偏向锁

偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由统一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的的优化效果。
当JVM启用了偏向锁模式(jdk6默认开启),新创建对象的Mark Word中的ThreadId为0,说明此时处于可偏向但未偏向任何线程,也叫作匿名偏向状态

偏向锁延迟偏向

偏向锁模式存在偏向延迟机制:HotSpot虚拟机在启动后有个4s的延迟才会对每个新建的对象开启偏向锁模式。

偏向锁撤销

HashCode
一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashCode的

  • 轻量级锁会在锁记录中记录hashCode
  • 重量级锁会在Monitor中记录hashCode
  • 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级为轻量级锁
  • 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级为重量锁

wait/notify
偏向锁状态执行obj.notify()会升级为轻量级锁,调用wait会升级为重量级锁

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时MarkWord的结构也变为轻量级锁的结构。轻量级锁适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。

轻量级锁是否可以降级为偏向锁?

锁升级

偏向锁升级为轻量级锁
轻量级锁膨胀为重量级锁
重量级锁释放后变为无锁,此时有新线程来调用同步块,会获取什么锁?

总结:锁对象状态转换

image.png

image.png

synchronized锁优化

批量重偏向/批量撤销

  • 批量重偏向和批量撤销是针对类的优化,和对象无关
  • 偏向锁重偏向一次之后不可再次重偏向
  • 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重问题,剥夺了该类的新实例对象使用偏向锁的权利
  • 撤销偏向锁有性能问题,因此需要批量撤销,达到阈值后剥夺偏向锁权利

自旋优化

针对重量级锁竞争,使用自适应自旋来进行优化
自旋的目的是为了减少线程挂起的次数(设计用户态和内核态的切换,这才是重量级锁最大的开销)

锁粗化/锁消除

锁粗化:对一个对象反复加锁及解锁,甚至加锁操作是在循环体中出现的,例如:StringBuffer.append()
锁消除:JIT编译期间删除不必要的加锁操作(逃逸分析)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值