Synchronized重量级锁的原理分析
Synchronized
前言
今天又重新看了下Synchronized的原理, 以前觉得这不就是八股文吗?背就完了,最近看了concurrenthashmap的源码以后,加深了对cas的理解之后,又有了新的收获,才发现自己真的是见识太浅薄了,该掌嘴啊!学这些东西,尽量细节, 是为了在以后工作中,能够复用前人这些优秀的设计思路。所以记录下自己学习的内容和思考。写完这篇文章之后,你要问问你自己以下问题。
- Synchronized中重量级锁的实现原理是什么?
- 实现过程中那里用到了CAS原理 ?
- 线程在获取这把锁的时候,到底采用的是何种算法
- 如果叫你自己用java实现一个synchronized锁,你会如何优化?
原理
下面我写的内容是Synchronized重量级锁的实现,轻量级锁, 偏向锁不在其中。
锁是什么?
我们知道,一般来说,我们的重量级锁对象一般是声明一个Object. 其实,任何对象都可以锁对象,因为我们关注的是对象头的信息。但是最好还是使用Object, 怕的是其他对象直接重写Object的一些方法,比如notify(),wait()方法。
Monitor是什么?
Monitor 是操作系统实现的一个对象,Java中每个锁都会关联一个Monitor对象,由此来实现Synchronized。
Monitor的大致结构如下
- owner 用来关联java中的线程,表示这个Monitor当前归哪个线程所有
- entrylist 队列,用来存在获取不到锁时的线程
- waitset 当调用了wait方法后,线程会放到这个里面
加锁过程分析
// thread-0
Object lock = new Object();
synchronized(lock) {
// code ...
}
-
当代码执行到synchronized那一行时,将对象头的markword替换为monitor对象
-
thread-0首先看owner是否为null, 如果为null, 就把owner 设置为当前线程对象,实现了线程和owner的绑定, 加速成功。
请注意,这里有一个很关键的东西,就是比较并设置 , 当你看到这个字眼时,应该是马上想到这必须是原子操作,不然要出问题。最简单的例子就是thread-0刚刚比较了,发现owner为null, 这时候操作系统发生上下文切换,thread-1也发现这个owner是null, 于是设置owner为1, 这时thread0又分到时间片,将owner改为0, 问题就出现了。按道理讲,我们只允许一个线程设置成功,现在两个线程都成功了。原因在于这里的比较并交换不是原子操作,比较之后,容易发生线程的上下文切换,导致另一个现场设置成功。所以这个地方是cas的比较并替换。
-
执行完临界区的代码后,开始解锁,将owner设置为null, 这个解锁不用CAS, 因为没人来和我争。就像是你这个渣男和一堆屌丝追求女神,只有一个人可以追到,你追到了,最后分手的时候别人都不会来抢,分手权只有你有!
-
锁对象的markword进行复原。
锁竞争分析
- thread-1执行到synchronized代码块的时候,通过对象的markword找到monitor对象
- 通过CAS设置owner为当前线程对象,发现owner不为null, 此时被thread-0占着。
- 加锁失败,当前线程放到entrylist中进行阻塞。
- thread-0释放锁以后,把monitor里面所有的thread唤醒去争抢锁,通过CAS去抢这把锁。
开头的问题自己的一点思考
-
Synchronized中重量级锁的实现原理是什么?
本质上就是锁对象关联monitor, 我们常说java的线程和操作系统的线程是一对一绑定的,通过观察字节码也可以知道,是通过monitorenter和monitorexit来处理的。同时也要知道,是如何防止死锁的,主要是做了异常检测,如果发生了异常会进行检测。好比加锁时我们必须把业务代码放到try 最后用finally来解锁。如果担心客户端不遵循这种规则,可以定义业务逻辑接口,只用业务方实现接口即可,不用关心加锁,解锁流程。
-
实现过程中那里用到了CAS原理 ?
在判断owner是否null, 并设置为当前线程这个地方用来CAS, 加锁的时候用了CAS, 解锁的时候不用。
-
entrylist里的线程在获取这把锁的时候,到底采用的是何种算法
entrylist里的线程被唤醒后,采用CAS去抢锁,这是不公平的,属于抢占式调度。其他算法还有先来先服务,最短时间优先等等。
-
如果叫你自己用java实现一个synchronized锁,你会如何优化?
如果叫我用java实现一个的话,我会对CAS这个地方做优化,我们知道线程上下文的切换,会从用户态转向内核态,保存现场,造成一定的时间开销,如果线程执行的逻辑代码本身很短的话,我们就可以自旋几次,从而不用直接阻塞。