系列文章目录 java
前面两篇文章咱们介绍了synchronized同步代码块以及wait和notify机制,大体知道了这些关键字和方法是干什么的,以及怎么用。git
可是,知其然,并不知其因此然。github
例如:segmentfault
wait set
?本篇咱们未来解答这些问题。数据结构
总的来讲,锁有两种不一样的实现方式,一种是自旋,一种是挂起。并发
(suspend-lock不知道怎么翻译,感受叫"挂起锁"或"悬挂锁"都太难听了,后面就直接不翻译了) 性能
自旋锁是一种乐观锁,它乐观地认为锁资源没有被占用,或者即便被占用了,也很快就会被释放, 因此当它发现锁已经被占用后,大多会在原地忙等待(通常是在死循环中等待,这也就是自旋的由来), 直到锁被释放,咱们在以前分析AQS的文章中提过,AQS处在阻塞队列头部的线程用的就是自旋的方式来等待锁。lua
suspend-lock是一种悲观锁,它悲观地认为锁竞争老是常常发生的,若是锁被占用了,基本短期内不会释放,因此他会让出CPU资源,直接挂起,等待条件知足后,别人将本身唤醒。 spa
自旋锁的优势是实现简单,只须要很小的内存,在竞争很少的场景中性能很好。可是若是锁竞争不少,那么大量的时间会浪费在无心义的自旋等待上,形成CPU利用率下降。操作系统
suspend-lock的优势是CPU利用率高,由于在发现锁被占用后,它会当即释放本身剩下的CPU时间隙(time-slice)给其余线程,以指望得到更高的CPU利用率。可是由于线程的挂起与唤醒须要经过操做系统调用来完成,这涉及到用户空间和内核空间的转换,线程上下文的切换,因此即便在竞争不多的场景中,这种锁也会显得很慢。可是若是锁竞争很激烈,则这种锁就能够得到很好的性能。
因而可知,自旋锁和suspend-lock
各有优劣,他们分别适用于竞争很少和竞争激烈的场景中。
在实际的应用中,咱们能够综合这两种方式的优势,例如AQS中,排在阻塞队列第一位的使用自旋等待,而排在后面的线程则挂起。
而咱们今天要讲的synchronized,使用的是suspend-lock方式。
既然前面提到了synchronized用的是suspend-lock
方式,在看synchronized的实现原理以前,咱们不妨来思考一下: 若是要咱们本身设计,该怎么作?
前几篇咱们提到过:
每一个java对象均可以用作一个实现同步的锁, 这些锁被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock).
要实现这个目标,则每一个java对象都应该与某种类型的锁数据关联。
这就意味着,咱们须要一个存储锁数据的地方,而且每个对象都应该有这么个地方。
在java中,这个地方就是对象头。
其实Java的对象头和对象的关系很像Http请求的http header
和http body
的关系。
对象头中存储了该对象的metadata
, 除了该对象的锁信息,还包括指向该对象对应的类的指针,对象的hashcode, GC分代年龄等,在对象头这个寸土寸金的地方,根据锁状态的不一样,有些内存是你们公用的,在不一样的锁状态下,存储不一样的信息,而对象头中存储锁信息的那部分字段,咱们称做Mark Word
, 这个咱们就不展开了讲了。咱们只须要知道:
锁信息存储在对象头的
Mark Word
中
在synchronized锁中,这个存储在对象头的Mark Word
中的锁信息是一个指针,它指向一个monitor对象(也称为管程或监视器锁)的起始地址。这样,咱们就经过对象头,将每个对象与一个monitor关联了起来,它们的关系以下图所示:
(图片来源: Evaluating and improving biased locking in the HotSpot virtual machine)
图片的最左边是线程的调用栈,它引用了堆中的一个对象,该对象的对象头部分记录了该对象所使用的监视器锁,该监视器锁指向了一个monitor对象。
那么这个monitor对象是什么呢? 在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构以下: (源码在这里)
ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
上面这些字段中,咱们只须要重点关注三个字段:
wait set
在java中,每个等待锁的线程都会被封装成ObjectWaiter对象,当多个线程同时访问一段同步代码时,首先会被扔进 _EntryList 集合中,若是其中的某个线程得到了monitor对象,他将成为 _owner
,若是在它成为 _owner
以后又调用了wait
方法,则他将释放得到的monitor对象,进入 _WaitSet集合中等待被唤醒。
(图片来源: Inter-thread communication in Java)
另外,由于每个对象均可以做为synchronized的锁,因此每个对象都必须支持wait()
,notify
,notifyAll
方法,使得线程可以在一个monitor对象上wait, 直到它被notify。这也就解释了这三个方法为何定义在了Object类中——这样,全部的类都将持有这三个方法。
Mark Word
中。因此,说是每个java对象均可以做为锁,实际上是指将每个java对象所关联的ObjectMonitor做为锁,更进一步是指,你们都想成为 某一个java对象所关联的
、ObjectMonitor对象的
、_owner
,因此你能够把这个_owner
看作是铁王座,全部等待在这个监视器锁上的线程都想坐上这个铁王座,谁拥有了它,谁就有进入由它锁住的同步代码块的权利。
其实,了解到上面这个程度已经足够用了,若是你想再深刻的了解,例如synchronized在字节码层面的具体语义实现,这里推荐几篇博客:
另外,若是你想深刻了解偏向锁,轻量级锁,以及锁膨胀的过程,强烈建议看下面这篇论文:
该篇论文的介绍很是详细,关键是有不少图示,对于Mark Word在不一样锁状态的描述很清晰。
(完)
查看更多系列文章:系列文章目录