美文网首页
synchronized

synchronized

作者: 牧羊人刘俏 | 来源:发表于2021-01-28 20:38 被阅读0次

@link https://blog.csdn.net/javazejian/article/details/72828483?locationNum=5&fps=1
@link https://www.cnblogs.com/paddix/p/5405678.html
synchronized用于线程同步
synchronized 修饰static 方法,那么就使用class的对象作为锁
synchronized 修饰非static方法,那么就使用当前对象作为锁
synchronized 修改代码块,使用代码块里面的对象作为锁
通过上面的例子可以看到,所有的Object对象在设计之初,就被设置成了一把锁,这就是为什么在Object里面有wait和notify的原因
调用wait表明将自身线程堵塞在此Object上,调用notify就是唤醒堵塞在此Object上面的Thread。
既然Object被设计成了一把锁,那么在内存中存储的时候,Object肯定有相应的地方会记录自己有没有被加锁,如果自己被加锁了加锁的Thread的id是多少,还有就是有多少个Thread在等待自己在释放锁。
如下图是一个Object在堆内存中的分布图,对象头被一分为二,Mark Word与Class Metadata Address,其中Class Metadata Address指向元数据区,表明此Object属于哪个Class,我们重点看Mark Word,所有synchronized都是对这个内存区在做文章。

image.png
其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构,如下
image.png
可以看到我们使用25bit来存储对象的hashcode,所以一般我们使用一个int位来存储对象的hashCode。
1bit位存储是否是偏向锁,要么是偏向锁,要么不是偏向锁
2bit锁标志位 所以一共可以表示四种锁标志位。
0 表示轻量级锁(jdk1.6新增)
1 表示偏向锁(jdk1.6新增)
2 表示重量级锁(在jdk1.6之前的默认实现)
3 是GC标记
我们可以认为设计这些锁是针对不同的场景的,就像在Thread的竞争不激烈的时候我们更多的使用乐观锁,如果竞争激烈,我们就使用悲观锁。
同样的对一个Object锁的竞争也可以先做比较乐观的估计,如果竞争的Thread不多,不用升级到重量级锁的目的,重量级锁依赖了os的互斥锁,还要进行用户态到内核态的切换,代价很大,所以1.6之前synchronized的效率很低,因为不管有没有人来抢,都是直接一把大锁,效率上面肯定是上不去的。
image.png

我们先来看重量级锁的实现原理,可以看到当对象头的锁标志位为重量级锁的时候,其有一个指针指向互斥量的指针,这个互斥量是一个monitor对象,前面说过一个Object肯定有相应的地方会记录自己有没有被加锁,如果自己被加锁了加锁的Thread的id是多少,还有就是有多少个Thread在等待自己在释放锁。
这个地方就是monitor,肯定有人问了,为啥不直接把这些信息记录在Object的堆内存而是用指针间接的去引用。
这是因为虽然每个Object都被设计成一把锁,但是不是每个对象都会当成锁来使用,所以只有当这个Object真的被当成锁来用,我们才创建这个monitor对象,这样可以节约堆内存。
monitor在虚拟机实现的数据结构如下,果然看到了我们熟悉的地方

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;  //记录当前持有锁的Thread
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet,这样被notify的时候,就知道唤醒哪些Thread了
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表,可以查看此锁的竞争的激烈程度
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

如果当前线程持有此Monitor锁,在调用wait的时候,先判断Owner是不是自身,如果不是抛出异常,这就是为什么调动wait的时候,需要获取锁的原因,如果是自身,将Owner清空,然后_count-1,将自己堵塞在_WaitSet队列之上。
如果竞争成功,将Owner置为自己,将_count+1
具体流程如下图


image.png

synchronized底层实现原理,如果对于synchronized修饰的同步代码块,生成的汇编会有如下的指令

3: monitorenter  //进入同步方法
//..........省略其他  
15: monitorexit   //退出同步方法
16: goto          24
//省略其他.......
21: monitorexit //退出同步方法

如上,如果发现指令中有monitorenter,那么就会去申请Object锁,monitorexit可以保证,如论出现什么异常的情况,锁都可以被释放掉,防止出现死锁。

而对于synchronized修饰的方法,会针对方法的访问控制flag设置一个 synchronized标志,如下

    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED

当执行这个方法的时候发现方法的访问标志有ACC_SYNCHRONIZED也回去申请对应的Object锁。
因为synchronized的重量级锁是依赖os的底层的Mutex Lock来实现的,等于说要实现这个synchronized重量级锁还要进行用户态到内核态的切换。所以在1.6之后,synchronized有很大的优化,如下


image.png

如上,一个对象头在最开始是无锁状态,如果有Thread来加锁,会升级为偏向锁状态,如果竞争变激烈会升级为轻量级锁状态,如果竞争还是很激烈,Thread会自旋一段时间(就是在cpu上空转一会),如果还是拿不到对象头的锁,这个锁才升级为重量级锁。锁可以升级但是不能降级。
且jvm对synchronized在编译的时候做了优化,如果发现一段代码不加锁也可以得到正确的结果,那么就会把锁消除掉(当然条件是很苛刻的,毕竟编译的时候不是运行态那么的复杂),防止额外的系统资源的占用和切换。
那么接下来对各种锁做一个简单的介绍。
轻量级锁
如下,如果Thread在加锁的时候,发现对象头处于无锁状态,如下



其加锁的过程如下
1、轻量级锁的加锁过程

(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。

(2)拷贝对象头中的Mark Word复制到锁记录中。

(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。

(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下图


image.png

(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

其解锁过程如下
2、轻量级锁的解锁过程:

(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。

(2)如果替换成功,整个同步过程就完成了。

(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
如上我们可以看到当无锁状态变为轻量级锁的过程中,虽然使用到了os的cas操作,cas本质就是一个乐观锁,但是远比通过os的互斥锁的实现效率要高。如果说在Thread的竞争不激烈的情况下,这种方式还是可以接受的。
轻量级锁相对于重量级锁来说简单了很多,但是还是涉及到多次的cas和copy操作,总的来说还是有点耗资源。

而偏向锁是一种更加武断的方式,Thread可以认为就我一个在申请锁,其他的线程没有竞争,如果当前对象头为无锁状态,且非偏向,那么直接一次cas操作,将ThreadId置为current thread,如果成功,说明此Thread占有了这把锁,其具体的过程如下
1、偏向锁获取过程:

(1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

(2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。

(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

(5)执行同步代码。
2、偏向锁的释放:

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
所以说如果没有其他的Thread的竞争,对象头里面的锁会一直维持偏向锁的状态,
几个锁的扭转状态如下


image.png

几种锁的对比如下


image.png

相关文章

网友评论

      本文标题:synchronized

      本文链接:https://www.haomeiwen.com/subject/mgmxtltx.html