2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > 深入HotSpot虚拟机源码探究synchronized底层实现原理【万字总结synchronized】

深入HotSpot虚拟机源码探究synchronized底层实现原理【万字总结synchronized】

时间:2022-03-16 16:39:57

相关推荐

深入HotSpot虚拟机源码探究synchronized底层实现原理【万字总结synchronized】

文章目录

一、synchronized原理(1)首先准备好HotSpot源码(2)解压,使用vscode或者其他编辑器打开(3)初始monitor监视器锁(先了解后细说)(4)建立宏观概念(初始基本流程)(5)分析锁竞争源码(6)分析锁等待源码(7)分析锁释放源码(8)为什么synchronized是重量级锁?二、synchronized的锁及其优化(1)初识偏向锁(2)初识轻量级锁(3)初始自旋锁,查看HotSpot中自旋锁的实现(4)锁消除(5)锁粗化(6)锁升级(7)平常写代码如何对synchronized进行优化三、synchronized的五大特性

一、synchronized原理

synchronized是Java中的关键字,无法通过JDK源码查看它的实现,它是由JVM提供支持的,所以如果想要了解具体的实现需要查看JVM源码

(1)首先准备好HotSpot源码

jdk8 hotspot源码下载地址:http://hg./jdk8u/jdk8u/hotspot/

选择zip或者gz格式下载即可

(2)解压,使用vscode或者其他编辑器打开

src是hotspot的源码目录

cpu:和cpu相关的一些操作

os:在不同操作系统上的一些区别操作

os_cpu:关联os和cpu的实现

share:公共代码

tools:一些工具类vm:公共源码

(3)初始monitor监视器锁(先了解后细说)

相信都对下面几行代码非常熟悉,如果不熟悉synchronized的底层的话,可能会直接认为这个锁是依赖Object对象的。这当然是无稽之谈!

Object lock = new Object();synchronized (lock) {}

其实无论是synchronized代码块还是synchronized方法,其锁的实现最终依赖monitor监视器(先记住这个概念后面细说);那么你是否头上有个大大的问号,那么这个Object对象有什么用呢

其实这要从对象头中的MarkWord说起了(这里我长话短说,后面细说);每个Java对象在内存中包含了三部分数据对象头、实例数据、对齐填充

对象头:包含了markword(状态标志位)和类元信息指针、数组长度(如果对象是数组则多这一项)

实例数据:存放具体的实例变量数据

对齐填充:JVM要求Java对象分配内存必须是8的倍数(不满足8的倍数时填充一些字节)

重点在markword ! ! ! ! !

这个markword的状态是动态变化的(节省空间),分为四种状态-无锁、偏向锁、轻量级锁、重量级锁;某一时刻Object的状态只能处于其中一种,这应该没什么疑问吧。这个动态变化涉及到了锁优化(锁升级、锁粗化、锁消除),这个概念先了解,后面细说!!!

重点来了 !!!!

Monitor被翻译成"监视器",可以理解为实现同步的一种工具,通常被描述为一个对象,Java中每个对象都关联着一把“看不见的锁”,,为什么?看完这段描述你就明白了!当我们使用synchronized给对象上锁之后(注意这里假设认为是重量级锁),该对象中的markword字段是处于重量级锁状态,然后它会被设置指向Monitor对象的指针(Monitor由C++实现)

这个monitor不是我们创建的,而是JVM执行到同步代码块时创建的,monitor里面有两个重要的变量,分别是owner(占有锁的线程),recursions(线程获取锁的次数)

(4)建立宏观概念(初始基本流程)

打开HotSpot源码文件src/share/vm/runtime/ObjectMonitor.hpp

在hotspot中,monitor是由ObjectMonitor对象来实现的,找到该对象对应的构造器

首先描述一下这个核心流程(只看核心部分)

Owner:持有monitor的线程,对应上面源码中的_object变量

WaitSet:处于等待状态的线程会被放到该队列(例如调用wait()方法),对应上面源码中的_WaitSet变量

EntryList:当多个线程竞争锁时,竞争锁失败的线程会被放入到该队列,处于阻塞状态,需要唤醒。对应上面源码中的_EntryList变量

(5)分析锁竞争源码

synchronized (lock) {num++;}

上面的同步代码经过反编译之后得到如下字节码指令

想必都听说过monitorenter和monitorexit指令,一个表示获取监视器锁,一个表示释放监视器锁;

关于锁竞争的JVM源码,最终会调用到MonitorObject类中的enter()方法

void ATTR ObjectMonitor::enter(TRAPS) {Thread * const Self = THREAD ;void * cur ;//通过CAS操作尝试将_owner变量设置为当前线程,如果_owner为NULL表示锁未被占用//CAS:内存值、预期值、新值,只有当内存值==预期值,才能将新值替换内存值cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;if (cur == NULL) {//如果未NULL,表示获取锁成功,直接返回即可assert (_recursions == 0 , "invariant") ;assert (_owner== Self, "invariant") ;return ;}//线程重入,synchronized的可重入特性原理,_owner保存的线程与当前正在执行的线程相同,将_recursions++if (cur == Self) {_recursions ++ ;return ;}//表示线程第一次进入monitor,则进行一些设置if (Self->is_lock_owned ((address)cur)) {assert (_recursions == 0, "internal state error");_recursions = 1 ; //锁的次数设置为1_owner = Self ; //将_owner设置为当前线程OwnerIsThread = 1 ; return ;}..........省略.....//获取锁失败for (;;) {jt->set_suspend_equivalent();//等待锁的释放EnterI (THREAD) ;if (!ExitSuspendEquivalent(jt)) break ;_recursions = 0 ;_succ = NULL ;exit (false, Self) ;jt->java_suspend_self();}Self->set_current_pending_monitor(NULL);}}

总结下来,也就是四步骤

Ⅰ、通过CAS尝试将_owner变量设置为当前线程

Ⅱ、如果是线程重入(下面有举例),则将_recurisons++

Ⅲ、如果线程是第一次进入,则将_recurisons设置为1,将_owner设置为当前线程,该线程获取锁成功并返回

Ⅳ、如果获取锁失败,则等待锁的释放

synchronized (lock) {num++;synchronized (lock) {//锁重入_recurisons+1}}

(6)分析锁等待源码

在锁竞争源码中最后一步,如果获取锁失败,则等待锁的释放,由MonitorObject类中的EnterI()方法来实现

void ATTR ObjectMonitor::EnterI (TRAPS) {Thread * Self = THREAD ;assert (Self->is_Java_thread(), "invariant") ;assert (((JavaThread *) Self)->thread_state() == _thread_blocked , "invariant") ;//再次尝试获取锁,获取成功直接返回if (TryLock (Self) > 0) {....return ;}DeferredInitialize () ;//尝试自旋获取锁,获取锁成功直接返回if (TrySpin (Self) > 0) {....return ;}//前面的尝试都失败,则将该线程信息封装到node节点ObjectWaiter node(Self) ;Self->_ParkEvent->reset() ;node._prev = (ObjectWaiter *) 0xBAD ;node.TState = ObjectWaiter::TS_CXQ ;ObjectWaiter * nxt ;//将node节点插入到_cxq的头部,前面说过锁获取失败的线程首先会进入_cxq//_cxq是一个单链表,等到一轮过去在该_cxq列表中的线程还未成功获取锁,//则进入_EntryList列表for (;;) {//注意这里的死循环操作node._next = nxt = _cxq ;//这里插入节点时也使用了CAS,因为可能有多个线程失败将加入_cxq链表if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;//如果线程CAS插入_cxq链表失败,它会再抢救一下看看能不能获取到锁if (TryLock (Self) > 0) {...return ;}}//竞争减弱时,将该线程设置为_Responsible(负责线程),定时轮询_owner//后面该线程会调用定时的park方法,防止死锁if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;}TEVENT (Inflated enter - Contention) ;int nWakeups = 0 ;int RecheckInterval = 1 ;//前面获取锁失败的线程已经放入到了_cxq列表,但还未挂起//下面是将_cxq列表挂起的代码,线程一旦挂起,必须唤醒之后才能继续操作for (;;) {//挂起之前,再次尝试获取锁,看看能不能成功,成功则跳出循环if (TryLock (Self) > 0) break ;assert (_owner != Self, "invariant") ;if ((SyncFlags & 2) && _Responsible == NULL) {Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;}//将当前线程挂起(park()方法)// park self//如果当前线程是_Responsible线程,则调用定时的park方法,防止死锁if (_Responsible == Self || (SyncFlags & 1)) {TEVENT (Inflated enter - park TIMED) ;Self->_ParkEvent->park ((jlong) RecheckInterval) ;// Increase the RecheckInterval, but clamp the value.RecheckInterval *= 8 ;if (RecheckInterval > 1000) RecheckInterval = 1000 ;} else {TEVENT (Inflated enter - park UNTIMED) ;Self->_ParkEvent->park() ;}//当线程被唤醒之后,会再次尝试获取锁if (TryLock(Self) > 0) break ;//唤醒锁之后,还出现竞争,记录唤醒次数,这里的计数器//并没有受锁的保护,也没有原子更新,为了获取更低的探究影响TEVENT (Inflated enter - Futile wakeup) ;if (ObjectMonitor::_sync_FutileWakeups != NULL) {ObjectMonitor::_sync_FutileWakeups->inc() ;}++ nWakeups ; //唤醒次数//自旋尝试获取锁if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ;if ((Knob_ResetEvent & 1) && Self->_ParkEvent->fired()) {Self->_ParkEvent->reset() ;OrderAccess::fence() ;}if (_succ == Self) _succ = NULL ;// Invariant: after clearing _succ a thread *must* retry _owner before parking.OrderAccess::fence() ;}//已经获取到了锁,将当前节点从_EntryList队列中删除UnlinkAfterAcquire (Self, &node) ;if (_succ == Self) _succ = NULL ;...return ;}

总结下来也就一下几步:

首先tryLock再次尝试获取锁,之后再CAS尝试获取锁;失败后将当前线程信息封装成ObjectWaiter对象。在for(;;)循环中,通过CAS将该节点插入到_cxq链表的头部(这个时刻可能有多个获取锁失败的线程要插入),CAS插入失败的线程再次尝试获取锁如果还没获取到锁,则将线程挂起;等待唤醒当线程被唤醒时,再次尝试获取锁

我能从这个源码设计理念中学到什么?

看完这个锁等待源码,你是不是有了一个疑问,为什么使用了多次tryLock尝试获取锁和CAS获取锁?源码中无限推迟了线程的挂起操作,你可以看到从开始到线程挂起的代码中,出现了多次的尝试获取锁;因为线程的挂起与唤醒涉及到了状态的转换(内核态和用户态),这种频繁的切换必定会给系统带来性能上的瓶颈。所以它的设计意图就是尽量推辞线程的挂起时间,取一个极限的时间挂起线程。

另外源码中定义了负责线程_Responsible,这种标识的线程调用的是定时的park(线程挂起),避免死锁

你永远也不知道在某个时刻你全部的线程会不会同时挂起,所以最好的解决办法就是:设计一种Responsible负责线程,让它一直活跃或者定时醒来。

(7)分析锁释放源码

void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {Thread * Self = THREAD ; if (THREAD != _owner) {//判断当前线程是否是线程持有者//当前线程是之前持有轻量级锁的线程。由轻量级锁膨胀后还没调用过enter方法,_owner会是指向Lock Record的指针if (THREAD->is_lock_owned((address) _owner)) {assert (_recursions == 0, "invariant") ;_owner = THREAD ;_recursions = 0 ;OwnerIsThread = 1 ;} else {//当前线程不是锁的持有者--》出现异常TEVENT (Exit - Throw IMSX) ;assert(false, "Non-balanced monitor enter/exit!");if (false) {THROW(vmSymbols::java_lang_IllegalMonitorStateException());}return;}}//重入,计数器-1,返回if (_recursions != 0) {_recursions--; // this is simple recursive enterTEVENT (Inflated exit - recursive) ;return ;}//_Responsible设置为NULLif ((SyncFlags & 4) == 0) {_Responsible = NULL ;}#if INCLUDE_JFRif (not_suspended && EventJavaMonitorEnter::is_enabled()) {_previous_owner_tid = JFR_THREAD_ID(Self);}#endiffor (;;) {assert (THREAD == _owner, "invariant") ;if (Knob_ExitPolicy == 0) {//先释放锁,这时如果有其他线程获取锁,则能获取到OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lockOrderAccess::storeload() ;// See if we need to wake a successor//等待队列为空,或者有"醒着的线程”,则不需要去等待队列唤醒线程了,直接返回即可if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {TEVENT (Inflated exit - simple egress) ;return ;}TEVENT (Inflated exit - complex egress) ;//当前线程重新获取锁,因为后序要唤醒队列//一旦获取失败,说明有线程获取到锁了,直接返回即可,不需要获取锁再去唤醒线程了if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {return ;}TEVENT (Exit - Reacquired) ;} else {if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lockOrderAccess::storeload() ;// Ratify the previously observed values.if (_cxq == NULL || _succ != NULL) {TEVENT (Inflated exit - simple egress) ;return ;}//当前线程重新获取锁,因为后序要唤醒队列//一旦获取失败,说明有线程获取到锁了,直接返回即可,不需要获取锁再去唤醒线程了if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {TEVENT (Inflated exit - reacquired succeeded) ;return ;}TEVENT (Inflated exit - reacquired failed) ;} else {TEVENT (Inflated exit - complex egress) ;}}guarantee (_owner == THREAD, "invariant") ;ObjectWaiter * w = NULL ;int QMode = Knob_QMode ; //根据QMode的不同,会有不同的唤醒策略if (QMode == 2 && _cxq != NULL) {//QMode==2,_cxq中有优先级更高的线程,直接唤醒_cxq的队首线程.........return ;}//当QMode=3的时候 讲_cxq中的数据加入到_EntryList尾部中来 然后从_EntryList开始获取if (QMode == 3 && _cxq != NULL) {.....}....... //省略.......//当QMode=4的时候 讲_cxq中的数据加入到_EntryList前面来 然后从_EntryList开始获取if (QMode == 4 && _cxq != NULL) {......}//批量修改状态标志改成TS_ENTERObjectWaiter * q = NULL ;ObjectWaiter * p ;for (p = w ; p != NULL ; p = p->_next) {guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;p->TState = ObjectWaiter::TS_ENTER ;p->_prev = q ;q = p ;}//插到原有的_EntryList前面 从员_EntryList中获取// Prepend the RATs to the EntryListif (_EntryList != NULL) {q->_next = _EntryList ;_EntryList->_prev = q ;}_EntryList = w ;}....................省略}}

核心流程如下:

1.将_recursions减1,_owner置空

2.如果队列中等待的线程为空或者_succ不为空(有"醒着的线程”,则不需要取唤醒线程了),直接返回即可。

3.第二条不满足,当前线程重新获取锁,去唤醒线程

4.唤醒线程,根据QMode的不同,有不同的唤醒策略

QMode = 2且cxq非空:cxq中有优先级更高的线程,直接唤醒_cxq的队首线程

QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部;

QMode = 4且cxq非空:把cxq队列插入到EntryList的头部;

QMode = 0:暂时什么都不做,继续往下看;

只有QMode=2的时候会提前返回,等于0、3、4的时候都会继续往下执行:

我能从这个源码设计理念中学到什么?

首先在它的锁释放源码中,首先就将锁释放,然后再去判断是否有醒着的线程;如果不满足再让该线程重新获取锁去唤醒线程。如何理解它设计理念的精髓之处,首先先将锁释放,因为可能有线程正在尝试或者自旋获取锁,然后 TODO:

(8)为什么synchronized是重量级锁?

从上面的ObjectMonitor类中的函数调用设计到了Atom:cmpxchg_ptr、Atom:inc_ptr等内核函数,没有获取到锁的线程会被挂起,竞争到锁的线程会被唤醒;这涉及到了状态的转换,即内核态和用户态的转换,浪费资源。

内核:控制计算机的硬件资源,为上层应用程序提供服务

系统调用:内核给上层应用提供的接口,为了能够访问到硬件资源

用户空间:用户程序执行的空间

系统调用的具体过程如下:

1.用户态程序将一些参数数据放在寄存器或者堆栈中,表明需要

2.用户态程序系统调用

3.CPU切换到内核态,并跳转到指定位置的指令

4.读取寄存器或者堆栈中的数据参数,执行相应的请求服务

5.完成系统调用,切换到用户态并返回系统调用结果

从上面可以看出系统调用设计到了参数的传递,同时还需要保存切换前用户态下的状态,这种频繁的切换无疑给系统带来了性能上的瓶颈;所以JDK6 synchronized进行了优化

二、synchronized的锁及其优化

在JDK5及其之前,只有重量级锁,在JDK6实现了几种锁优化技术,锁升级、锁消除、锁粗化,提高了synchronized的效率。

无锁->偏向锁->轻量级锁->重量级锁

(1)初识偏向锁

顾名思义就是"偏向"第一个获取锁的线程,会在markword中存储该线程的id;以后该线程进入和退出同步代码块只需要检查是否为偏向锁、锁标志位和线程id即可。

public class TendencyTest {public static void main(String[] args) {MyThread t = new MyThread();t.start();}}class MyThread extends Thread {private static Object lock = new Object();@Overridepublic void run() {synchronized (lock) {System.out.println(ClassLayout.parseInstance(lock).toPrintable());}}}

上面这段简单的代码就是使用偏向锁的场景,查看执行结果:可以看出刚好是1和01表明是当前处于偏向锁。

偏向锁原理:

1.当线程第一次获取锁时,虚拟机会将是否为偏向锁设置为1,将锁标志位设置为01;等到以后该线程再来访问同步代码块时,不需要再进行任何同步操作

2.偏向锁的撤销恢复到无锁或者轻量级锁状态,需要在全局安全点才能撤销

(2)初识轻量级锁

顾名思义,轻量级锁就是相对于重量级锁而言的,它并不是用来代替重量级锁的,引入的目的是为了在多线程交互的场景下,避免重量级锁带来的性能消耗。

轻量级锁原理:

(1)首先判断当前对象是否处于无锁,如果是,则JVM将在当前线程栈帧中创建一个Lock Record,用于存储对象目前的markword的拷贝

(2)让Lock Record中owner指向锁对象,CAS尝试将MarkWord更新为指向Lock Record的指针,将MarkWord的数据存入Lock Records

(3)如果CAS成功,对象的Mark Word将会存储Lock Record 地址 和 锁状态 00

(4)如果CAS失败,此时会有两种情况:

如果是其他线程已经持有了该轻量级锁,则表示发生竞争,此时进入锁膨胀。

如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数,只不过新添加的Lock Record中没有Object的Mark word内容,为null。

(5)当退出 synchronized 代码块,如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一,将null的Lock Record删除。

当退出 synchronized 代码块,锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头。

成功,则解锁成功。

失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

(3)初始自旋锁,查看HotSpot中自旋锁的实现

当某个线程获取锁,CPU一直被其他线程占用着,就一直循环检测锁是否被释放,而不是进入线程阻塞状态。自旋锁是一种基于CAS的一种锁,它依赖CPU的空转,每一次自旋通常会暂停一段时间;它适用于线程执行时间较短的场景,在这种场景下CPU的空转开销是远小于线程切换的。

一句话总结自旋锁:自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。

JDK6推出了新的自适应自旋锁。自适应意味着自旋的时间不再固定了,而是由于上一个锁拥有者自旋获取锁的时间所决定。(比如上一个获取锁之前自旋的时间为1s,那么这次可能就是1.2s,比上一次长一点)

查看自旋锁在HotSpot的实现

int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {//原始的自旋锁int ctr = Knob_FixedSpin ;if (ctr != 0) {//当自旋次数不等于0时while (--ctr >= 0) {--操作if (TryLock (Self) > 0) return 1 ; //每次自旋尝试获取锁SpinPause () ; //自旋一次暂停一段时间}return 0 ;}//新版自适应自旋for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) {// Knob_PreSpin默认是自旋10次if (TryLock(Self) > 0) {//每次自旋尝试获取锁int x = _SpinDuration ;if (x < Knob_SpinLimit) {if (x < Knob_Poverty) x = Knob_Poverty ;_SpinDuration = x + Knob_BonusB ; //如果获取锁成功,修改一下自旋的时间,允许比上次长一点}return 1 ;}SpinPause () ; //自旋一次暂停的时间}

(4)锁消除

锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。根据代码逃逸技术分析,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。

public String back(String str1, String str2) {StringBuffer stringBuffer = new StringBuffer();stringBuffer.append(str1).append(str2).toString();}

StringBuffer的append代码如下:

@Overridepublic synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;}

看上面这几行代码,首先分析StringBuffer中append同步方法锁的是哪个对象?肯定是当前new的StringBuffer对象,由于每个线程来执行back方法时,都会创建一个StringBuffer对象,所以它的锁对象是不同的;另外可以发现这个StringBuffer并没有逃逸出这个方法。所以可以进行锁消除,将 synchronized去掉。

(5)锁粗化

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

下面代码示例:

public class StringBufferTest {StringBuffer stringBuffer = new StringBuffer();public void append(){stringBuffer.append("a").append("b").append("c");}

上述代码每次调用 stringBuffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

(6)锁升级

偏向锁->轻量级锁->重量解锁

Ⅰ.偏向锁->轻量级锁:当某个线程首次去获取锁时,会将MarkWord中的线程id设置为当前线程id;后续再有线程来尝试获取锁时,需要MarkWord中的线程id和当前线程id是否一致,如果一致就无需CAS来加锁解锁;如果不一致需要判断MarkWord记录的线程是否存活,如果不存活,重置锁状态为无锁,其他线程可以设置为偏向锁;如果存活,找到它对应的栈帧信息,检测该线程是否还需要继续持有锁,如果需要,则暂停它,撤销偏行锁,膨胀为轻量级锁;如果不需要则重置锁状态为无锁。

Ⅱ.轻量级锁->重量级锁:当线程1获取轻量级锁,首先会在该线程1栈帧中开辟一段Displaced MarkWord空间,然后将对象头中的MarkWord复制到Displaced MarkWord,将对象头中MarkWord的的地址替换为Displaced MarkWord的地址

这时线程2通过CAS方式来获取锁,将MarkWord到线程2的锁记录空间;之后发现锁已经被线程1获取了,那么它就会通过自旋的方式等待锁的释放。

这个自旋是有次数的,默认是10次(源码注释说明20-100最适合),也提供了自适应自旋时间,上面自旋锁已经解释过。

一旦超过了自旋次数,那么会撤销轻量级锁,膨胀为重量级锁。

注意只能锁升级,不能锁降级。偏向锁可以重置为无锁。

(7)平常写代码如何对synchronized进行优化

1.减少同步代码块中的内容,缩短执行时间

synchronized (lock) {num++;}

2.降低锁粒度

将锁拆分为多个锁,降低锁粒度;最为著名的就是Hastable和ConcurrentHashMap做对比,Hashtable锁住的是整个哈希表,效率低下;ConcurrentHashMap在JDK8之前使用了锁分段技术,锁住的是Segment段,JDK8更是将锁的粒度降低到了Node级别,使用CAS+Synchronized锁住根节点。

3.读写锁分离

读时不加锁,写入时才加锁。

三、synchronized的五大特性

1.Synchronized保证原子性

public class SynchronizedTest {private static int num;private static Object lock = new Object();public static void main(String[] args) throws InterruptedException {Runnable runnable = () -> {for (int i = 0; i < 1000; i++) {synchronized (lock) {//同步代码块num++;}}};List<Thread> list = new ArrayList<>();for (int i = 0; i < 4; i++) {Thread t = new Thread(runnable);t.start();list.add(t);}for (Thread t : list) {t.join();}System.out.println("num:" + num);}}

经过反编译查看字节码指令

同步代码块反编译后的字节码指令如上图,其中monitorenter和monitorexit这两个JVM指令是同步代码块实现的核心,monitorenter表示获取监视器锁,monitorexit表示释放监视器锁。当某个线程获取锁之后,其他线程必须等待该线程释放锁,才能执行同步代码块中的内容。

2.Synchronized保证可见性

public class SynchronizedTest02 {private static boolean flag = true;private static Object lock = new Object();public static void main(String[] args) throws InterruptedException {new Thread(() -> {while (flag) {synchronized (lock) {}}}).start();TimeUnit.SECONDS.sleep(2);new Thread(() -> {flag = false;System.out.println("修改了flag变量为false");}).start();}}

使用了synchronized同步代码块之后,程序能够正常结束,无论是Synchronized还是Volatile都是使用memory barrier来保证可见性的。

monitorenter指令之后会有一个Load屏障,重新拉取被别的线程修改后的值;monitorexit指令之前会有一个Store屏障,将自身修改后的数据刷新到高速缓冲或主内存中。

3.Synchronized保证有序性

public class SynchronizedTest03 {private static int num = 0;private static boolean flag = true;private static final Object lock = new Object();public static void main(String[] args) {new Thread(() -> {synchronized (lock) {num++;flag = false;}}).start();}}

Synchronized也是通过内存屏障来保证有序性的,通过Acquire Barrier和Release Barrier来实现。

Acquire Barrier在一个读操作之后插入,禁止该读操作和以后的任何读写操作发生重排序

Release Barrier在一个写操作之前插入,禁止该写操作与任何读写操作发生重排序

4.Synchronized的可重入特性

public class SynchronizedTest04 {public static void main(String[] args) {new Thread(() -> {synchronized (SynchronizedTest.class) {System.out.println(Thread.currentThread().getName() + "进入同步代码块一");synchronized (SynchronizedTest.class) {System.out.println(Thread.currentThread().getName() + "进入同步代码块二");}}}).start();}}

Synchronized的锁对象中有一个计数器,会记录线程获得锁的次数,每次获取锁,计数器加1,每次释放锁,计数器减1,当计数器为0时,完成释放;能够避免死锁,方便使用其他方法进行封装

5.Synchronized不可中断特性

public class Synchronized05 {public static void main(String[] args) throws InterruptedException {Runnable runnable = () -> {synchronized (Synchronized05.class) {System.out.println(Thread.currentThread().getName() + "正在执行同步代码块...");TimeUnit.SECONDS.sleep(10);}};new Thread(runnable).start();TimeUnit.SECONDS.sleep(2);new Thread(runnable).start();}}

某个线程获取锁之后,其他线程处于阻塞或者等待状态。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。