想会用 synchronized 锁,先掌握底层核心原理
本文分享自华为云社区《Synchronized底层核心原理》,作者: 小威要向诸佬学习呀 。
synchronized 锁用于同步实例方法,同步静态方法和同步代码块。自从 Java1.6 开始,就对 synchronized 锁进行了很多方面的优化。对其引入了偏向锁,轻量级锁,适应性自旋锁,锁粗化,锁消除等各种技术方面的优化。
synchronized 锁是基于 monitor 锁实现的,因此在讲解 synchronized 锁之前,有必要了解一下 monitor 锁。
monitor 锁的原理
monitor,在中文中有监视器的意思,当创建对象时,每一个创建出来的对象都会关联一个 monitor 对象,对于一个 java 对象,当拿到这个 monitor 对象时,这个 monitor 对象就会处于锁定的状态,其他对象不会再获取,synchronized 锁的本质就是基于进入和退出 monitor 对象实现的同步方法和同步代码块。
这里首先解释一下 wait,notify,notifyAll 等方法的各个作用:
wait 方法会让进入 object 监视器的线程进入到 WaitSet 集合中等待;notify 方法会使在 object 上正在 WaitSet 集合上等待的线程中挑一个唤醒线程;notifyAll 方法会让正在 WaitSet 集合中等待的线程全部唤醒。
而对于 monitor,它是基于 ObjectMonitor 实现的,ObjectMonitor 的主要数据结构包括:
owner:owner 原本的值为 null,它用来指向获取到 ObjectMonitor 对象的线程。当一个线程获取到 ObjectMonitor 对象时,这个 ObjectMonitor 对象就会存储在当前对象的对象头中的 MarkWord 中。
WaitSet,这个是 ObjectMonitor 中的一个集合,同时 WaitSet 与 wait()方法有关。当 Owner 线程发现条件不满足时,会调用 wait 方法,使线程进入 WaitSet 集合中变为 WAITING 状态。
EntryList,也是 ObjectMonitor 中的一个集合,同时 EntryList 与 notify(),notifyAll()方法有关。WAITING 状态下的线程会在 Owner 线程调用 notify()或 notifyAll()等方法时唤醒,但是唤醒之后并不代表着线程会立即拿到锁资源,而是需要进入 EntryList 集合中进行竞争。
模拟多线程情况下,同时访问一个被 synchronized 锁修饰方法时,在 JVM 底层中的流程如下:
线程进入 EntryList 集合时,如果某个线程获取到 monitor 对象时,这个线程会进入 owner 中,同时会把 monitor 对象中的 owner 变量复制为当前的线程(拿到 monitor 对象的这个),并且会把 monitor 对象中的 count 变量值+1。
如果线程调用 wait 方法,当前的线程就会释放拿到的 monitor 对象,并且会把 monitor 对象中的 owner 变量值设为 null,并且 count 的值-1。最后,当前线程会进入到 WaitSet 集合中等待,等候再次被唤醒。
如果是获得 monitor 对象的线程执行任务完成后,也会进行上面的一系列操作,但不会到 WaitSet 集合中等待了,因为任务已经执行完了。
synchronized 修饰方法
前面说到 synchronized 锁是基于 monitor 锁实现的。当 synchronized 锁修饰方法时,被此锁修饰的方法会比普通方法的常量池中多一个 ACC_SYNCHRONIZED 标识符。当线程调用了被 synchronized 锁修饰的方法时,会检查方法中是否设置了此标识符。
如果设置了 ACC_SYNCHRONIZED 标识符,那么当前的线程会首先获取 monitor 锁对象,然后执行同步代码中的方法,完成后会释放 monitor 对象。当然,在多线程情况下,只有一个线程能够获取此 monitor 对象,并且在该线程释放 monitor 对象之前,其他线程无法获取此 monitor 对象。因此在同一时刻,只能有一个线程拿到相同对象的 synchronized 锁资源。
而当 synchronized 锁修饰代码块时,与 synchronized 修饰方法略有不同,接下来详细讲解 synchronized 修饰代码块的情况。
synchronized 修饰代码块
当 synchronized 锁修饰代码块时,synchronized 关键字会被编译成 monitorenter 和 monitorexit 两条指令,其中,monitorenter 会放在代码块的前面,而 monitorexit 会放在代码块的后面。
对于 monitorenter 指令:
每个对象都拥有一个 monitor,当 monitor 被占用时,就会处于锁定状态,线程执行 monitorenter 指令时会获取 monitor 的所有权。
当 monitor 计数为 0 时,说明该 monitor 还未被锁定,此时线程会进入 monitor 并将 monitor 的计数器设为 1,并且该线程就是 monitor 的所有者。
如果此线程已经获取到了 monitor 锁,再重新进入 monitor 锁的话,那么会将计时器 count 的值加 1。
如果有线程已经占用了 monitor 锁,此时有其他的线程来获取锁,那么此线程将进入阻塞状态,待 monitor 的计时器 count 变为 0,这个线程才会获取到 monitor 锁。
对于 monitorexit 指令:
首先,只有拿到了 monitor 锁对象的线程才会执行 monitorexit 指令。
其次就是,在执行 monitorexit 指令时,计时器 count 的值会减 1,当 count 的值减到 0 时,当前的线程才会退出 monitor,此时的线程不再是 monitor 的所有者,当然执行后,其他线程可以获取当前 monitor 锁的所有权。
通过对简单代码进行反编译来举例:
执行 javap -c SynchronizedTest.class 指令得到以下字节码:
由上述编码可以看出,在 synchronized 修饰的代码块中,存在有 monitorenter 指令和 monitorexit 指令。
synchronized 锁总结
因此,由以上可以得出,synchronized 锁修饰方法和代码块时底层实现上是一样的,但是在修饰方法时,不需要 JVM 编译出的字节码完成加锁操作,而 synchronized 在修饰代码块时,是通过编译出来的字节码生成的 monitorenter 和 monitorexit 指令来实现的。
版权声明: 本文为 InfoQ 作者【华为云开发者联盟】的原创文章。
原文链接:【http://xie.infoq.cn/article/6128dee3cfd08e4152db741ce】。文章转载请联系作者。
评论