Synchronizied锁原理及锁优化

基本原理解析

synchronized可以修饰方法与代码块,首先看下在同步代码块中的实现方式

同步代码块

public class Monitor {

    public void synCodeBlock() {
        synchronized (this) {
            System.out.println("running");
        }
    }
}

反编译查看字节码内容,会发现同步代码块的开始结束存在着monitorentermonitorexit字节码命令。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WXx3pABq-1582688106840)(https://s2.ax1x.com/2019/12/17/QTumy6.png)]

关于这两条命令,在JVM规范中有着描述.
monitorenter:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

  • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
  • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
  • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

每个对象都关联一个monitor,当monitor被占用会处于锁定状态,线程执行monitorenter尝试获取monitor持有权。

  • 如果monitor的锁计数器为0,则线程进入monitor并且将其设置为1,该线程占有monitor。
  • 如果线程已经占有monitor,将锁计数器加1
  • 如果另一个线程已经占用了monitor,线程将会阻塞直至monitor锁计数器为0,然后尝试去占有monitor。

monitorexit:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

执行monitorexit的线程必须是当前对象的monitor的占有者。
该线程将递减所计数器,如果锁计数器为0,该线程将退出monitor并不再是其占有者。其他阻塞线程尝试去进入monitor成为其占有者。

++主要是通过对Monitor的占用和接除占用来完成对代码的锁定,从而实现同步++

同步方法

public class Monitor {

    public synchronized void synMethod() {
        System.out.println("running");
    }
}

查看其字节码内容

JVM 规范描述

Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.

大致意思:
同步方法是隐式的,作为方法的调用和返回的一部分。在调用方法时通过运行时常量池中的method_info结构体中存放ACC_SYNCHRONIZED的标志区分同步方法。当线程调用设置了ACC_SYNCHRONIZED的方法时,执行线程会占有monitor,当方法执行完毕会退出monitor。这段时间该其他线程阻塞等待。如果发生方法调用期间发生异常而且该方法没有处理该异常,则线程会在方法抛出异常之前自动退出monitor。

++通过上面的描述可以看出其实不论是同步方法还是同步代码块都是通过占有和退出Monitor对象来实现同步。++

Monitor

通过JVM虚拟机规范了解到,每个对象都有一个monitor与之关联, Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,被称为监视器锁,而在Hotspot虚拟机底层是通过ObjectMonitor的数据结构进行实现Monitor监视器锁的。源码如下:

ObjectMonitor() {
         _header       = NULL;
         _count        = 0;//竞争此监视器的线程数
         _waiters      = 0,//等待线程数
         _recursions   = 0;//重入次数
         _object       = NULL;
         _owner        = NULL;//指向获得ObjectMonitor对象的线程
         _WaitSet      = NULL;//处于wait状态的线程,会被加入到wait set;
         _WaitSetLock  = 0 ;
         _Responsible  = NULL ;
         _succ         = NULL ;
         _cxq          = NULL ;
         FreeNext      = NULL ;
         _EntryList    = NULL ;//处于等待锁block状态的线程,会被加入到entry list;
         _SpinFreq     = 0 ;
         _SpinClock    = 0 ;
         OwnerIsThread = 0 ;
         _previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
         }

当多线程执行同一个同步方法或同步代码块,会先进入_EntryList集合竞争,当线程获取到monitor后进入_owner区域将变量设置为当前线程,同时将进入计数器_recursions加1。当退出monitor后_recursions减一释放monitor。在调用wait方法会释放持有的monitor,即释放锁,其他线程可竞争占有monitor。若线程执行完毕也会释放持有的monitor。大致流程如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hZHVgBda-1582688106843)(https://s2.ax1x.com/2019/12/18/QTY3bn.jpg)]

Monitor类似于一个房间,进入房间便获得了其占有权,退出房间便释放其所有权,其他人则可以竞争进入房间来获得其占有权。获得所有权即有了资源的使用权,也就实现了对共享资源的同步,这就是Monitor监视器锁的实现机制。

++大量文章将_count误解为锁计数器,翻阅源码发现,其实其主要是记录竞争该监视器的线程数,阻止垃圾回收,值大致等于|_WaitSet| + |_EntryList|的线程数++

// reference count to prevent reclaimation/deflation
// at stop-the-world time.  See deflate_idle_monitors().
// _count is approximately |_WaitSet| + |_EntryList|

重入

在上面的Monitor数据结构中有一个_recursions,该字段是用来记录该线程进入monitor的次数。而这个字段的设立则说明了monitor是可以多次进入,即可重入性,表明了Synchronized为可重入锁,也称递归锁。
如果某线程试图获取一个它已经持有的锁,这个请求就会成功,这就是锁的可重入性。
Monitor结构中的_recursions便是线程获取监视器锁持有权的计数器。线程第一次获取监视器锁时将该计数器设置为1,如果该线程再次请求获得锁则该计数器递增,相反的,线程释放锁时计数器会递减,当该计数器为0时,则表明当前monitor即监视器锁没有被任何线程持有。

可重入锁的最大作用就是避免死锁,例如下面代码:

public class Machine {

	public synchronized void doSomething(){
		System.out.println("运行...");
	}
}

public class WashingMachine extends Machine{
	
	@Override
	public synchronized void doSomething(){
		System.out.println("洗衣服...");
		super.doSomething();
	}
}

子类重写父类方法并且调用父类方法,而锁均为同一对象,假如synchronized无法实现可重入,则会出现死锁。

简单了解了Monitor的机制,那Monitor是存储在什么位置呢?

Mark Word

HotSpot虚拟机中对象在内存中存储可以分为三个部分:对象头(Header)、实例化数据(InstanceData)、对齐填充(Padding)
而对象头(Header)通常包括两部分信息:

  • 1.用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。这部分官方称为Mark Word
  • 2.类型指针(klass pointer),即对象指向它的类元数据指针,通过该指针确定是哪个类的实例。

(注:但是如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小)

上面信息展示了存储对象锁的位置是在对象头的Mark Word中。
查看MarkWord 在Hotspot Jvm源码中注释,描述了MarkWord的存储状态。

*  64 bits:
*  --------
*  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
*  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
*  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
*  size:64 ----------------------------------------------------->| (CMS free block)
*
*  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
*  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
*  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
*  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

(上半部分是没有开启COOPs-指针压缩的结构,下半部分是开启了指针压缩的结构)
64位虚拟机下对象头长度为64bit,而各标志位含义分别为:unused:未使用;hash:HashCode; age:GC分代年龄标识;biased_lock:偏向锁标识;lock:对象的状态 ;JavaThread: 线程ID;epoch:偏向时间戳;

在JavaSE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,通过该标志位实现锁状态提升。无锁–>偏向锁–>轻量级锁–>重量级锁。而Synchronized锁关键字即为重量级锁的实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n5ynWTEt-1582688106844)(https://s2.ax1x.com/2019/12/28/lmYwGD.png)]

到这里便可以看出Monitor其实是存储在对象头中,也大致了解了为什么可以通过对象进行加锁。

锁优化

synchronized实现对性能最大的影响是阻塞的实现,Java线程实现映射于操作系统原生线程上,挂起和恢复线程的操作都需要转入内核态完成。而为了降低不必要性能的损耗,便对锁进行了优化处理,其中包括自旋锁,偏向锁,轻量级锁及锁消除,锁粗化等。

自旋锁

请求锁的线程假设持有锁的线程短时间内就会释放锁,则该线程可以选择等待一会,但不释放处理器的执行时间,让其执行一个循环等待(自旋),这个技术就是自选锁。 而如果持有锁的线程占有时间过长,自旋等待虽然避免了阻塞的状态切换导致的性能损耗,但却占用了处理器的执行时间,所以自旋时间有一定的限度,如果等待时间过长依然会挂起线程。
而在jdk1.6中引入了自适应的自旋锁,线程自旋的时间由前一次在同一个锁的自选时间及锁的拥有者状态决定,从而减少浪费处理器资源的场景。

偏向锁

偏向锁的意思是指锁会偏向于第一个获得它的线程,假如在接下来的执行过程中,该锁没有被其他线程获取则不需要进行同步。当其他线程尝试去获取锁时,则结束偏向模式,所以当有多个线程争抢锁时偏向锁显得有些多余。偏向锁虽然可以提高带有同步但无竞争场景下的性能,但大多数并发场景下禁用偏向锁模式反而会提高性能。

轻量级锁

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
加锁:

  • 1.代码进入同步块时,此时同步对象处于无锁状态,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
  • 2.虚拟机使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针
    • 更新成功线程即拥有了该对象的锁,锁标志位修改为‘00’,此对象处于轻量级锁定状态
    • 更新失败先检查对象的MarkWord是否指向当前线程的栈帧,如果已经持有锁则直接进入同步代码块执行,否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”

释放锁:对象的Mark Word仍然指向线程的线程的Lock Record,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,替换成功则同步结束。替换失败说明其他线程尝试获取过锁,那就在释放锁的同时唤醒被挂起的线程。

参考:

java并发笔记三之synchronized 偏向锁 轻量级锁 重量级锁证明
synchronized
synchronized原理
jdk源码剖析二: 对象内存布局、synchronized终极原理
https://www.jianshu.com/p/fd780ef7a2e8
https://www.cnblogs.com/durenniu/p/10949491.html
https://www.cnblogs.com/sheeva/p/6366782.html
https://segmentfault.com/a/1190000009912198
https://www.cnblogs.com/paddix/p/5367116.html


// The markOop describes the header of an object.  
//  
// Note that the mark is not a real oop but just a word.  
// It is placed in the oop hierarchy for historical reasons.  
//  
// Bit-format of an object header (most significant first, big endian layout below):  
//  
//  32 bits:  
//  --------  
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)  
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)  
//             size:32 ------------------------------------------>| (CMS free block)  
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)  
//  
//  64 bits:  
//  --------  
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)  
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)  
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)  
//  size:64 ----------------------------------------------------->| (CMS free block)  
//  
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)  
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)  
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)  
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)  
//  
//  - hash contains the identity hash value: largest value is  
//    31 bits, see os::random().  Also, 64-bit vm's require  
//    a hash value no bigger than 32 bits because they will not  
//    properly generate a mask larger than that: see library_call.cpp  
//    and c1_CodePatterns_sparc.cpp.  
//  
//  - the biased lock pattern is used to bias a lock toward a given  
//    thread. When this pattern is set in the low three bits, the lock  
//    is either biased toward a given thread or "anonymously" biased,  
//    indicating that it is possible for it to be biased. When the  
//    lock is biased toward a given thread, locking and unlocking can  
//    be performed by that thread without using atomic operations.  
//    When a lock's bias is revoked, it reverts back to the normal  
//    locking scheme described below.  
//  
//    Note that we are overloading the meaning of the "unlocked" state  
//    of the header. Because we steal a bit from the age we can  
//    guarantee that the bias pattern will never be seen for a truly  
//    unlocked object.  
//  
//    Note also that the biased state contains the age bits normally  
//    contained in the object header. Large increases in scavenge  
//    times were seen when these bits were absent and an arbitrary age  
//    assigned to all biased objects, because they tended to consume a  
//    significant fraction of the eden semispaces and were not  
//    promoted promptly, causing an increase in the amount of copying  
//    performed. The runtime system aligns all JavaThread* pointers to  
//    a very large value (currently 128 bytes (32bVM) or 256 bytes (64bVM))  
//    to make room for the age bits & the epoch bits (used in support of  
//    biased locking), and for the CMS "freeness" bit in the 64bVM (+COOPs).  
//  
//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread  
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased  
//  
//  - the two lock bits are used to describe three states: locked/unlocked and monitor.  
//  
//    [ptr             | 00]  locked             ptr points to real header on stack  
//    [header      | 0 | 01]  unlocked           regular object header  
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)  
//    [ptr             | 11]  marked             used by markSweep to mark an object  
//                                               not valid at any other time  
//  
//    We assume that stack/thread pointers have the lowest two bits cleared.  
   
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值