标签:模型 null its 复用 内核 存储 队列 sync 法则
当一个共享资源有可能被多个线程同时访问并修改的时候,需要用锁来保证数据的正确性。请看下图:
线程A和线程B分别往同一个银行账户里面添加货币,A线程从内存中读取(read)当前账户金额($=0)到线程A的本地栈,进行+100的操作后,这时B线程也从内存中读取当前金额($=0)到线程B的本地栈,并且进行+200的操作后写回主存,线程B前脚刚写回之后,后脚线程A又把$=200写会到本地内存中。我们顺便来复习一下JMM内存模型的8个原子操作:
我们知道,volatile
关键字只能保证变量的有序性和可见性,但是不能保证原子性。在这个例子中,即使给$变量加上volatile
关键字也是不顶用的,原因可见volatile为什么不能保证原子性以及知乎提问:volatile为什么不能保证原子性。
这时候就轮到我们的synchronized
关键字出场了,它可以在访问竞态资源时加锁,从而保证修改的时候不会出错。它有三种作用范围:
.class
对象this
Object
对象,比如monitor
在JDK6以前,synchronized
还属于重量级锁,每次枷锁都依赖操作系统Mutex Lock实现,涉及到操作系统让线程从用户态切换到内核态,切换成本很高。在JDK6以后,研究人员引入了偏向锁和轻量级锁,因为Sun公司的程序员发现大部分程序大多数时间都不会发生多个线程同时访问竞态资源的情况,每次线程都加锁解锁,每次这么搞都要操作系统在用户态和内核态之前来回切,太耗性能了。
首先要了解synchronized
的实现原理,需要理解二个预备知识:
//下图详细介绍重要变量的作用
ObjectMonitor() {
_header = NULL;
_count = 0; // 重入次数
_waiters = 0, // 等待线程数
_recursions = 0;
_object = NULL;
_owner = NULL; // 当前持有锁的线程
_WaitSet = NULL; // 调用了 wait 方法的线程被阻塞 放置在这里
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 等待锁 处于block的线程 有资格成为候选资源的线程
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
对象关联的 ObjectMonitor 对象有一个线程内部竞争锁的机制,如下图所示:
下面我们就来分析一下JDK6之前的synchronized
具体的实现逻辑。
money
变量加钱,要进行操作的时候 ,发现方法上加了synchronized
锁,这时线程调度到A线程执行,A线程就抢先拿到了锁。拿到锁的步骤为:MonitorObject
中的_owner
设置成 A线程Mark Word
设置为 Monitor
对象地址,锁标志位改为10;ContentionList
队列Waiting Queue
的尾部取出一个线程放到OnDeck
作为候选者,但是如果并发比较高,Waiting Queue
会被大量线程执行CAS操作,为了降低对尾部元素的竞争,将Waiting Queue
拆分成ContentionList
和 EntryList
二个队列, JVM将一部分线程移到EntryList
作为准备进OnDeck的预备线程。另外说明几点:ContentionList
这个竞争队列中;Contention List
中那些有资格成为候选资源的线程被移动到 Entry List
中;OnDeck
;Owner
;ContentionList、EntryList、WaitSet
中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock
内核函数实现的);Owner
的A线程执行过程中,可能调用wait
释放锁,这个时候A线程进入 Wait Set
, 等待被唤醒。以上就是synchronized
在 JDK 6之前的实现原理。
另外,synchronized
在在线程竞争锁时,首先做的不是直接进ContentionList
队列排队,而是尝试自旋获取锁(可能ContentionList
有别的线程在等锁),如果获取不到才进入 ContentionList
,这明显对于已经进入队列的线程是不公平的,所以synchronized
是非公平锁。另一个不公平的是自旋获取锁的线程还可能直接抢占 OnDeck
线程的锁资源。
那么JDK6对synchronized
做了哪些优化呢?
-XX:+UseBiasedLocking
,这是自JDK6起HotSpot虚拟机的默认值),那么当锁对象第一次被线程获取的时候,虚拟机会把对象头中的标志位设为01
、把偏向模式设置为1
,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时。虚拟机都可以不再进行任何同步操作(例如加锁、解锁以及对Mark Word的更新操作等)。0
),撤销后标志位恢复到未锁定(01
)或轻量级锁定(00
)的状态,后续的同步操作就按照轻量级锁进行。01
状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word拷贝(官方为这份拷贝加了个前缀Displaced)。00
,表示此对象处于轻量级锁定状态。10
,此时Mark Word中存储的就是指向重量级锁监视器ObjectMonitor
(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。轻量级锁的解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。假如能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有其他线程尝试过获取该锁(膨胀为重量级锁,Mark Word指向了互斥量),就要在释放重量级锁的同时,唤醒被挂起的线程。
轻量级锁能提升程序同步性能的依据是“对于绝大部分锁,在同步周期内都是不存在竞争的”这一经验法则。如果没有竞争,轻量级锁通过CAS操作成功避免了使用互斥量的开销;但如果确实存在竞争,除了互斥量本身的开销之外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。
整个锁升级的过程如下图所示:
标签:模型 null its 复用 内核 存储 队列 sync 法则
原文地址:https://www.cnblogs.com/muuu520/p/12923015.html