文章目录
一、synchronized原理
synchronized是Java中的关键字,无法通过JDK源码查看它的实现,它是由JVM提供支持的,所以如果想要了解具体的实现需要查看JVM源码
(1)首先准备好HotSpot源码
jdk8 hotspot源码下载地址:http://hg.openjdk.java.net/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 enter
TEVENT (Inflated exit - recursive) ;
return ;
}
//_Responsible设置为NULL
if ((SyncFlags & 4) == 0) {
_Responsible = NULL ;
}
#if INCLUDE_JFR
if (not_suspended && EventJavaMonitorEnter::is_enabled()) {
_previous_owner_tid = JFR_THREAD_ID(Self);
}
#endif
for (;;) {
assert (THREAD == _owner, "invariant") ;
if (Knob_ExitPolicy == 0) {
//先释放锁,这时如果有其他线程获取锁,则能获取到
OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock
OrderAccess::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 lock
OrderAccess::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_ENTER
ObjectWaiter * 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 EntryList
if (_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();
@Override
public 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代码如下:
@Override
public 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();
}
}
某个线程获取锁之后,其他线程处于阻塞或者等待状态。