接上文:synchronized 锁的是什么?(一)
前知识-moniter 监视器
每个对象都存在着一个 Monitor 对象与之关联。执行 monitorenter 指令就是线程试图去获取 Monitor 的所有权,抢到了就是成功获取锁了;执行 monitorexit 指令则是释放了 Monitor 的所有权。
ObjectMonitor 类
monitor 是用 c++实现的叫 objectmonitor。
java 实例对象里面记录了指向这个 monitor 的地址,这个 c++的 monitor 对象里面记录了当前持有这个锁的线程 id。
在 HotSpot 虚拟机中,Monitor 是基于 C++的 ObjectMonitor 类实现的,其主要成员包括:
- _owner:指向持有 ObjectMonitor 对象的线程
- _WaitSet:存放处于 wait 状态的线程队列,即调用 wait() 方法的线程
- _EntryList:存放处于等待锁 block 状态的线程队列
- _count:约为_WaitSet 和 _EntryList 的节点数之和
- _cxq: 多个线程争抢锁,会先存入这个单向链表
- _recursions: 记录重入次数
ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; // 线程重入次数 _object = NULL; // 存储 Monitor 对象 _owner = NULL; // 持有当前线程的 owner _WaitSet = NULL; // wait 状态的线程列表 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; // 单向列表 FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁状态 block 状态的线程列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
更多源码分析 ,可以参考 这里。
moniter 对象是什么时候实例化的?
在 Java 对象实例化的时候,ObjectMonitor 对象和 Java 对象一同创建和销毁。
协作
监视器 Monitor 有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
什么时候需要协作?比如:
“一个线程向缓冲区写数据,另一个线程从缓冲区读数据,如果读线程发现缓冲区为空就会等待,当写线程向缓冲区写入数据,就会唤醒读线程,这里读线程和写线程就是一个合作关系。JVM 通过 Object 类的 wait 方法来使自己等待,在调用 wait 方法后,该线程会释放它持有的监视器,直到其他线程通知它才有执行的机会。一个线程调用 notify 方法通知在等待的线程,这个等待的线程并不会马上执行,而是要通知线程释放监视器后,它重新获取监视器才有执行的机会。如果刚好唤醒的这个线程需要的监视器被其他线程抢占,那么这个线程会继续等待。Object 类中的 notifyAll 方法可以解决这个问题,它可以唤醒所有等待的线程,总有一个线程执行。”
如上图所示,一个线程通过 1 号门进入 Entry Set(入口区),如果在入口区没有线程等待,那么这个线程就会获取监视器成为监视器的 Owner,然后执行监视区域的代码。如果在入口区中有其它线程在等待,那么新来的线程也会和这些线程一起等待。线程在持有监视器的过程中,有两个选择,一个是正常执行监视器区域的代码,释放监视器,通过 5 号门退出监视器;还有可能等待某个条件的出现,于是它会通过 3 号门到 Wait Set(等待区)休息,直到相应的条件满足后再通过 4 号门进入重新获取监视器再执行。
注意:
“当一个线程释放监视器时,在入口区和等待区的等待线程都会去竞争监视器,如果入口区的线程赢了,会从 2 号门进入;如果等待区的线程赢了会从 4 号门进入。只有通过 3 号门才能进入等待区,在等待区中的线程只有通过 4 号门才能退出等待区,也就是说一个线程只有在持有监视器时才能执行 wait 操作,处于等待的线程只有再次获得监视器才能退出等待状态。”
其他问题
notify 执行之后立马唤醒线程吗?
其实 hotspot 里真正的实现是:退出同步块的时候才会去真正唤醒对应的线程;不过这个也是个默认策略,也可以改成在 notify 之后立马唤醒相关线程。
notify() 或者 notifyAll() 调用时并不会真正释放对象锁,必须等到 synchronized 方法或者语法块执行完才真正释放锁。
synchronized 锁的是什么?
Synchronized 原理
Synchronized 是 Java 中解决并发问题的一种最常用的方法,也是最简单的一种方法。
Synchronized 的作用主要有三个:
- 确保线程互斥的访问同步代码
- 保证共享变量的修改能够及时可见
- 有效解决重排序问题。
从语法上讲,Synchronized 总共有三种用法:
- 修饰普通方法
- 修饰静态方法
- 修饰代码块
我们将下面这段代码反编译看一下:
public class SynchronizedDemo { public void syncDemoMethod(){ synchronized (this){ System.out.println("syncDemoMethod"); } } }
# 编译生成 class 文件 -g 生成所有调试信息 javac -g synchronizedDemo.java # 反编译出字节码指令 -v 输出附加信息 javap -v synchronizedDemo
synchronized 实现的原理就在上图中的两条字节码指令中。下面是这两条指令的文档:
- monitorenter (https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html#jvms-6.5.monitorenter)
- monitorexit (https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html#jvms-6.5.monitorexit)
monitorenter
根据文档所述:
每个对象有一个监视器锁(monitor)。当且仅当 monitor 被占用时才会被锁定。执行 monitorenter 指令的线程尝试获取 monitor 的所有权,过程如下:
- 如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为 monitor 的所有者。
- 如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加 1.
- 如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权。
monitorexit
根据文档所述:
- 执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。
- 指令执行时,monitor 的进入数减 1,如果减 1 后进入数为 0,那线程退出 monitor,不再是这个 monitor 的所有者。其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权。
synchronized 方法
public class SynchronizedDemo { public synchronized void syncDemoMethod() { System.out.println("syncDemoMethod"); } }
synchronized 方法的字节码指令没有中没有 monitorenter
和 monitorexit
。
syhchronized 方法的同步是一种隐式的方式来实现 :当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象
多线程访问场景总结
- 当两个并发线程访问同一个对象 object 中的这个 synchronized(this) 同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
- 当一个线程访问 object 的一个 synchronized(this) 同步代码块时,另一个线程仍然可以访问该 object 中的非 synchronized(this) 同步代码块。
- 当一个线程访问 object 的一个 synchronized(this) 同步代码块时,其他线程对 object 中所有其它 synchronized(this) 同步代码块的访问将被阻塞。
- 当一个线程访问 object 的一个 synchronized(this) 同步代码块时,它就获得了这个 object 的对象锁。其它线程对该 object 对象所有同步代码部分的访问都被暂时阻塞。
- 以上规则对其它对象锁同样适用。
需要特别说明:对于同一个类 A,线程 1 争夺 A 对象实例的对象锁,线程 2 争夺类 A 的类锁,这两者不存在竞争关系。
synchronized 阻塞线程的方式
- synchronized 同步块对同一条线程来说是可重入的,不会出现自己锁死自己的情况
- synchronized 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入,阻塞的方式是将 Java 的线程映射到操作系统的原生线程之上,通过操作系统来阻塞或唤醒一条线程。
借用操作系统意味着需要从用户态转换到核心态,状态转换会耗费很多的处理器时间,因此 synchronized 是一个重量级操作。通常,虚拟机自身会对其做一些优化,比如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁切入到核心态。
小结
JVM 规范规定 JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。
- 代码块同步是使用
monitorenter
和monitorexit
字节码指令实现 - 方法同步是 根据该
ACC_SYNCHRONIZED
标示符来实现
monitorenter
指令是在编译后插入到同步代码块的开始位置,而monitorexit
是插入到方法结束处和异常处, JVM 要保证每个monitorenter
必须有对应的 monitorexit 与之配对。
任何对象都有一个 monitor
与之关联,当且一个monitor
被持有后,它将处于锁定状态。线程执行到 monitorenter
指令时,将会尝试获取对象所对应的 monitor
的所有权,即尝试获得对象的锁。
总结
我们通过分析 JAVA 对象的内存布局了解了对象头
,顺藤摸瓜了解了 markword
的结构 以及 objectMonitor(监视器)。
从 markword
中认识了与锁
相关的重要信息,了解到锁的类型和区别以及锁相关的优化和升级过程。
从 ObjectMonitor
了解到它是 synchronized
的核心实现,以及对于线程协作上的具体逻辑。
从 synchronized
所修饰的代码的字节码指令中分析出 monitorenter
和 monitorexit
指令,它又与我们上面了解到的 objectMonitor
密不可分。
同时总结出了 synchronized
的使用场景以及线程协作时的常见问题 。利用总结的知识,围绕问题较全面地回答了 “synchronized 锁的是什么?”
为什么 wait() 和 notify() 需要搭配 synchonized 关键字使用 ?
剖析
public static void main(String[] args) { SynchronizedDemo obj = new SynchronizedDemo(); obj.notify(); }
如果我们直接执行对象的 notify/wait 等方法时会报错,报错信息如下:
这里显示异常类型为: IlleagalMonitorStateException
我们看一下 JDK 对方法的注释
意思是同一时刻只有一个线程可以获得对象的监视器锁(monitor),如果当前线程没有获得对象的监视器锁则抛出 IlleagalMonitorStateException
异常。
表明如果我们直接调用 wait/notify 等方法是不能获得监视器锁的,只有先获得监视器锁才行,所以在使用 wait/notify 等方法时要配合 synchronized
先获得监视器锁(monitor),然后调用这些方法。
而一个线程获得对象监视器锁有三种方法,也就是加 synchronized
的三种方式:
- 修饰普通方法
- synchronized 代码块
- 修饰静态方法(给类加锁)