Synchronized 是通过实例对象的monitor来实现的。
Synchronized 能锁住的两种方式:
-
锁对象
锁对象,只锁定当前对象本身,其他的实例对象,不能同时锁定 在方法上
//锁对象方法public void synchronized sync()
在对象上
//锁this对象public void sync(){ synchronized(this){ }}
上面两者是等价的,都是锁在当前对象上。只锁自己的对象
-
锁类
锁类上,是对当前类所有的实例对象加锁
//锁静态方法public static void synchronized sync()
//锁类字节public void sync(){ synchronized(Students.class){ }}
对类字节进行锁定,则锁定的是JVM内,此类的所有对象。 这个是怎么处理的呢? 以什么为标识来锁的呢?
Synchronized 是通过monitor来实现锁定的,每一个对象都有自己的monitor,
monitor中维护了两个列表,_EntrySet和_WaitSet, 这两个是做什么的呢?结合下图来说明。
_EntrySet: 当 A 线程拿到 monitor锁,对 monitor 进行计数加 1,B 线程再去获取锁是获取不到的。此时 B 线程进入 _EntrySet 列表中等待。
当然,在等待的时候 B 也是不什么都不干,因为没有其他线程通知它 锁什么时候会释放,只能自己去尝试拿锁。能拿的到就OK,拿不到就继续等着。这个时候B 就是在自旋,一遍遍的去尝试获取锁,旋转跳跃永不停歇的那种。如果 B 一直在自旋,那么也会占用很多的CPU资源,也干不了什么事。在Hotspot里,这里是有一个【智能自旋】,参考:.......
_WaitSet: 当 A 线程在锁中,调用了 wait() 方法,wait方法会释放掉 monitor 锁,然后 A 线程就进入了 _WaitSet 列表中。
wait 方法 就是让出资源,让别人先走,自己停一下。自己休息够了,再去尝试获取锁资源。
那么处在 _WaitSet 中的线程,有两种方式可以醒来:
- 被别人 notify()
- 设置时间,自己休息够了就醒来
醒来后还是在 _WaitSet 中,但是可以去获取锁资源了。
官方描述,notify() 是从 _WaitSet中随机唤醒一个线程,其实每次都是从 Set 的第一个线程,因为Set是无序的,所以才会是随机唤醒。
那么还有个问题,已经被唤醒的线程,还在 _WaitSet 中,还会不会被 notify() 呢 ? ?
Synchronized 是怎么做到同步的 ?
Synchronized 当多个线程竞争同一个 monitor 时,加锁的动作,就是对monitor进行计数加 1 , 每有一次加锁成功,就计数加 1。
那么如何保证不会有多个线程的冲突操作,A 从0加1,B也是从0加1. 同时操作,最后结果也是1,A,B都算是成功。是怎么避免这种问题的呢?
这里就引入了CAS。最后在写入到内存中时,会先判断一下,内存中的值是不是操作前的值。是就操作,不是就再来一次。
Synchronized 是不是可重入锁 ?
是可重入锁。为什么会有重入锁? 锁套锁,里面的锁就是要重入的锁。
monitor 里维护了得到锁的线程对象。A 线程已经持有 monitor 锁,再次获取锁时,会先判断,进来的这个线程是不是已有的线程,是的话就计数再加 1,表示再一次获得了锁。
Synchronized 是如何释放锁的 ?
通过 monitorexit 指令。monitorexit 会对 锁计数进行 【减 1】操作。加锁和解锁是成对出现的,加几次锁,就要解几次锁。
wait() 方法也是会解锁的
Synchronized 为什么说它是重量锁 ?
说它是重量锁,肯定是相对而言的,相对的是Lock。 即 AQS。
这个时候就要有对比了。
- Synchronized 获取不到锁的时候,会阻塞线程 , AQS则不会,要不要阻塞线程自己决定。(这是重点)
- Synchronized 是底层实现的(JVM),AQS是上层实现的(JDK)。底层逻辑实现,为了保证健壮,有很多保护逻辑在里面。(我也说不上来,但就是有。C我已经看不懂了。)
- 还有就是 Synchronized 限定死了锁的逻辑流程,
- 没有拿到锁就要等;
- wait了之后不能就不处理了,还得等再获取锁,再继续流程,不能断。
- 就一定是重入锁了,想不重入都不行。
- 就锁的逻辑二者差不大,都是通过CAS操作来实现并发锁。就以上而言,重不重看自己的需求了。
欢迎指正,想到哪写到哪,也没个顺序排版啥的。这个也得学着梳理下。