Java的Synchronized锁

目录

1.synchronized锁是什么?锁的对象是什么?

2.对象头

3. 偏向锁

偏向锁的加锁过程:

偏向锁的解锁过程:

4. 轻量级锁

轻量级锁的加锁过程:

轻量级锁的释放流程

5. 重量级锁

重量级锁执行流程:

6.为什么说是轻量级,重量级锁是不公平的?

7.重量级锁为什么需要自旋操作?

8.什么时候会发生锁升级,锁降级?


1.synchronized锁是什么?锁的对象是什么?

可以让synchronized修饰的方法,代码块,每次只能有一个线程在执行,以此来实现数据的安全。

给普通方法加锁,锁的是当前对象this

给静态方法加锁,锁的是当前Class类

synchronized(obj) 这样子加锁,锁的是obj对象

实现原理:monitor监视器

synchronized修饰同步代码块,javac在编译时,在synchronized同步块的进入的指令前和退出的指令后,会分别生成对应的monitorenter和monitorexit指令进行对应,代表尝试获取锁和释放锁。(为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。)

2.对象头

对象头包括:Mark Word + 指向对象所属的类的指针组成

Mark Word:存储对象自身的运行时数据,例如hashCode,GC分代年龄,锁状态标志,线程持有的锁等等。

锁信息都是存在锁对象的Mark Word中的

  1. 当对象状态为偏向锁时,Mark Word存储的是偏向的线程ID;
  2. 当状态为轻量级锁时,Mark Word存储的是指向线程栈中 LockRecord的指针;
  3. 当状态为重量级锁时,Mark Word为指向堆中的monitor对象的指针。

3. 偏向锁

为什么要进行锁升级?

由于重量级锁加锁解锁过程消耗性能,需要线程挂起和唤醒,而同步代码很快执行结束,从而大部分时间都是加解锁。

锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。

简单的来说,就是主要锁处于偏向锁状态时,会在Mark Word中存当前持有偏向锁的线程ID,如果获取锁的线程ID与它一致就说明是同一个线程,可以直接执行,不用像轻量级锁那样执行CAS操作来加锁和解锁。

偏向锁的加锁过程:

场景一:当锁对象第一次被线程获得锁的时候

线程发现是匿名偏向状态(也就是锁对象的Mark Word没有存储线程ID),则会用CAS指令,将 mark word中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。

场景二:当获取偏向锁的线程再次进入同步块时

发现锁对象存储的线程ID就是当前线程的ID,会往当前线程的栈中添加一条 DisplacedMarkWord为空的 LockRecord中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下, synchronized关键字带来的性能开销基本可以忽略。

场景二:当没有获得锁的线程进入同步块时

当没有获得锁的线程进入同步块时,发现当前是偏向锁状态,并且存储的是其他线程ID(也就是其他线程正在持有偏向锁),则会进入到撤销偏向锁的逻辑里,一般来说,会在 safepoint中去查看偏向的线程是否还存活

  • 如果线程存活且还在同步块中执行, 则将锁升级为轻量级锁,原偏向的线程继续拥有锁,只不过持有的是轻量级锁,继续执行代码块,执行完之后按照轻量级锁的解锁方式进行解锁,而其他线程则进行自旋,尝试获得轻量级锁。

  • 如果偏向的线程已经不存活或者不在同步块中, 则将对象头的 mark word改为无锁状态(unlocked)

由此可见,偏向锁升级的时机为:当一个线程获得了偏向锁,在执行时,只要有另一个线程尝试获得偏向锁,并且当前持有偏向锁的线程还在同步块中执行,则该偏向锁就会升级成轻量级锁。

偏向锁的解锁过程:

因此偏向锁的解锁很简单,其仅仅将线程的栈中的最近一条 lockrecordobj字段设置为null。需要注意的是,偏向锁的解锁步骤中并不会修改锁对象Mark Word中的thread id,简单的说就是锁对象处于偏向锁时,Mark Word中的thread id 可能是正在执行同步块的线程的id,也可能是上次执行完已经释放偏向锁的thread id,主要是为了上次持有偏向锁的这个线程在下次执行同步块时,判断Mark Word中的thread id相同就可以直接执行,而不用通过CAS操作去将自己的thread id设置到锁对象Mark Word中。

4. 轻量级锁

大部分时候可能并没有多线程竞争,只是这段时间是线程A执行同步块,另外一段时间是线程B来执行同步块,仅仅是多线程交替执行,并不是同时执行,也没有竞争,如果采用重量级锁效率比较低。

轻量级锁的加锁过程:

JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到自己的Displaced Mark Word里面。

然后线程尝试用CAS操作将锁的Mark Word替换为自己线程栈中拷贝的锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。

自旋:不断尝试去获取锁,一般用循环来实现。

自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。

JDK采用了适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

自旋也不是一直进行下去的,如果自旋到一定程度(和JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。

轻量级锁的释放流程

在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。

5. 重量级锁

当多个线程同时请求某个重量级锁时,重量级锁会设置几种状态用来区分请求的线程:

Contention List:所有请求锁的线程将被首先放置到该竞争队列,我也不知道为什么网上的文章都叫它队列,其实这个队列是先进后出的,更像是栈,就是当Entry List为空时,Owner线程会直接从Contention List的队列尾部(后加入的线程中)取一个线程,让它成为OnDeck线程去竞争锁。(主要是刚来获取重量级锁的线程是会进行自旋操作来获取锁,获取不到才会进入Contention List,所以OnDeck线程主要与刚进来还在自旋,还没有进入到Contention List的线程竞争)

Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List,主要是为了减少对Contention List的并发访问,因为既会添加新线程到队尾,也会从队尾取线程。

Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set。

OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck。

Owner:获得锁的线程称为Owner。!Owner:释放锁的线程。

重量级锁执行流程:

步骤1是线程在进入Contention List时阻塞等待之前,程会先尝试自旋使用CAS操作获取锁,如果获取不到就进入Contention List队列的尾部。

步骤2是Owner线程在解锁时,如果Entry List为空,那么会先将Contention List中队列尾部的部分线程移动到Entry List。

步骤3是Owner线程在解锁时,如果Entry List不为空,从Entry List中取一个线程,让它成为OnDeck线程,Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁,JVM中这种选择行为称为 “竞争切换”。(主要是与还没有进入到Contention List,还在自旋获取重量级锁的线程竞争)

步骤4就是OnDeck线程获取到锁,成为Owner线程进行执行。

步骤5就是Owner线程调用锁对象的wait()方法进行等待,会移动到Wait Set中,并且会释放CPU资源,也同时释放锁,

步骤6.就是当其他线程调用锁对象的notify()方法,之前调用wait方法等待的这个线程才会从Wait Set移动到Entry List,等待获取锁。

6.为什么说是轻量级,重量级锁是不公平的?

偏向锁由于不涉及到多个线程竞争,所以谈不上公平不公平,轻量级锁获取锁的方式是多个线程进行自旋操作,然后使用用CAS操作将锁的Mark Word替换为指向自己线程栈中拷贝的锁记录的指针,所以谁能获得锁就看运气,不看先后顺序。重量级锁不公平主要在于刚进入到重量级的锁的线程不会直接进入Contention List队列,而是自旋去获取锁,所以后进来的线程也有一定的几率先获得到锁,所以是不公平的。

7.重量级锁为什么需要自旋操作?

因为那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通过pthreadmutexlock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。如果同步块中代码比较少,执行比较快的话,后进来的线程先自旋获取锁,先执行,而不进入阻塞状态,减少额外的开销,可以提高系统吞吐量。

8.什么时候会发生锁升级,锁降级?

偏向锁升级为轻量级锁:就是有不同的线程竞争锁时。具体来看就是当一个线程发现当前锁状态是偏向锁,然后锁对象存储的Thread id是其他线程的id,并且去Thread id对应的线程栈查询到的lock record的obj字段不为null(代表当前持有偏向锁的线程还在执行同步块)。那么该偏向锁就会升级成轻量级锁。

轻量级锁升级为重量级锁:就是在轻量级锁中,没有获取到锁的线程进行自旋,自旋到一定次数还没有获取到锁就会进行锁升级,因为自旋也是占用CPU的,长时间自旋也是很耗性能的。锁降级因为如果没有多线程竞争,还是使用重量级锁会造成额外的开销,所以当JVM进入SafePoint安全点(可以简单的认为安全点就是所有用户线程都停止的,只有JVM垃圾回收线程可以执行)的时候,会检查是否有闲置的Monitor,然后试图进行降级。

巨人的肩膀:

https://mp.weixin.qq.com/s/H8Cd2fj82qbdLZKBlo-6Dg 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值