Sychronized详解
2022-11-22 15:48:31 0 举报
Sychronized内置锁,底层原理,管道,锁得升级过程,锁优化
作者其他创作
大纲/内容
检查方法的ACC_SYNCHRONIZED 访问标志是否被设置
CAS操作将对象头的Markword的所记录指针指向当前线程的锁记录上
对齐填充位
获得偏向锁01
原持有偏向锁的线程栈中分配锁记录
同步代码逻辑
monitorenter
未退出同步代码块
通过monitorenter和monitorexit来实现。
释放锁
唤醒原持有偏向锁的线程
在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。
0/1标志
Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量
自旋
hash: 保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。age: 保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。biased_lock: 偏向锁标识位。由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。font color=\"#f44336\
原偏向锁线程
其他线程就可以尝试访问该monitor
使用逃逸分析,编译器可以对代码做如下优化
TreadlD
方法逃逸(
CAS操作1&21、对象头中的Mark Word中锁记录指针是否仍然指向当前 线程锁记录2、拷贝在当前线程锁记录的Mark Word信息是否与对象头中的Mark Word一致
线程访问同步代码块
时间-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清为0,重新计时
是否偏向
是
锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间
开始新一轮锁竞争
CAS操作替换成当前线程ID
原持有偏向锁的线程释放锁0 01
总结:1.批量重偏向和批量撤销是针对类的优化,和对象无关。2.偏向锁重偏向一次之后不可再次重偏向。3.当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利
while(条件不满足) { wait();唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件。MESA模型的wait()方法还有一个超时参数,为了避免线程进入等待队列永久阻塞。
偏向锁批量撤销
Data N
锁状态标志01
失败
synchronized的使用
执行方法体,执行完后释放monitor
执行同步代码块
sychronized
指令执行,monitor进入数减一,如果减一后进入数为0,则线程退出monitor,不再是这个monitor的所有者
锁消除
age
轻量级锁00
原持有偏向锁的线程到达安全点
偏向锁状态执行obj.notify() 会升级为轻量级锁,调用obj.wait(timeout) 会升级为重量级锁
一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。
当前线程
从安全点继续执行
32位JVM下的对象结构描述:
synchronized基础
偏向锁撤销之调用wait/notify
偏向锁撤销之调用对象HashCode
成功
3、如果其他线程已经占用该monitor,则该线程进入阻塞状态,知道monitor进入数为0,再重新尝试获取monitor的所有权
Monitor(管程/监视器)
mutex挂起当前线程
OPP
monitorexit
检查对象的Markword中记录的是否当前线程ID
2、如果线程已经占用该monitor,只是重新进入,进入数加1
在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认策略(QMode=0)是:如果EntryList为空,则将cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取锁。_EntryList不为空,直接从_EntryList中唤醒线程。
开始偏向锁撤销(等待竞争时才释放锁的机制)
同步代码块
当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。应用场景
同步方法
自旋优化
执行monitorexit的线程必须是该monitor的占用者
失败一定次数后
通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;
升级为轻量级锁
以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。
重量级锁10
对象的HashCode
暂停原持有偏向锁的线程
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
线程逃逸
调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销。因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashcode的。轻量级锁会在锁记录中记录 hashCode 重量级锁会在 Monitor 中记录 hashCode
锁粗化
在当前线程栈中分配锁记录
拷贝对象头的Markword到原持有偏向锁的线程的锁记录上
自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)
编译成字节码后翻译
MetaData元数据指针
Epoch
Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象才有)等。实例数据:存放类的属性数据信息,包括父类的属性信息;对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
偏向状态0或1
这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量。
唤醒被挂起的那些线程
否
检查原持有偏向锁的线程的状态
线程执行monitorenter指令时尝试获取monitor的所有权
Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。Hasen模型、Hoare模型和MESA模型
获得轻量级锁,指向当前线程的锁记录的指针00
拷贝对象头的Markword到锁记录上
管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。
对象头
notify()和notifyAll()分别何时使用满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():1.所有等待线程拥有相同的等待条件;2.所有等待线程被唤醒后,执行相同的操作;3.只需要唤醒一个线程。
synchronized底层原理
转变为重量级锁指向重量级锁Monitor的指针10
不是
两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
执行线程先获取monitor
synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁。
目前锁状态?
假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),它是一个重量级锁,性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。
monitorexit指令出现两次,第一次为同步正常退出释放锁;第2次发送异步退出释放锁
Mark Word32bit
实例数据
偏向锁批量重偏向
升级为重量级锁
1.同步省略或锁消除(Synchronization Elimination)。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。2.将堆分配转化为栈分配(Stack Allocation)。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。3.分离对象或标量替换(Scalar Replacement)。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也无法偏向:当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。
原持有偏向锁的线程获得轻量级锁,指向原持有偏向锁的线程的锁记录的指针00
对齐填充(选填)
开启轻量级锁解锁
逃逸分析
64位JVM下的对象结构描述:
未活动状态/已退同步代码块
数组长度
0 条评论
回复 删除
下一页