Java ynchronized重量级锁的核心原理详解
作者:小小茶花女
在JVM中,每个对象都关联一个监视器,这里的对象包含Object实例和Class实例。监视器是一个同步工具,相当于一个许可证,拿到许可证的线程即可进入临界区进行操作,没有拿到则需要阻塞等待。重量级锁通过监视器的方式保障了任何时间只允许一个线程通过受到监视器保护的临界区代码。
1. monitor原理
jvm中每个对象都会有一个监视器Monitor,监视器和对象一起创建、销毁。监视器相当于一个用来监视这些线程进入的特殊房间,其义务是保证(同一时间)只有一个线程可以访问被保护的临界区代码块。
每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)
//部分属性 ObjectMonitor() { _count = 0; //锁计数器 _owner = NULL; _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 }
本质上,监视器是一种同步工具,也可以说是一种同步机制,主要特点是:
- 同步。监视器所保护的临界区代码是互斥地执行的。一个监视器是一个运行许可,任一线程进入临界区代码都需要获得这个许可,离开时把许可归还。
- 协作。监视器提供Signal机制,允许正持有许可的线程暂时放弃许可进入阻塞等待状态,等待其他线程发送Signal去唤醒;其他拥有许可的线程可以发送Signal,唤醒正在阻塞等待的线程,让它可以重新获得许可并启动执行。
每个java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的mark word中就被设置指向 Monitor 对象的指针。
(1) 如果使用 synchronized 给obj对象上锁,obj对象的markword就会指向一个monitor锁对象;
(2) 刚开始 Monitor 中 Owner 为 null ;
(3) 当Thread-2线程持有monitor对象后,就会把monitor中的owner变量设置为当前线程Thread-2;
(4) 当Thread-3线程想要执行临界区的代码时,要判断monitor对象的属性Owner是否为null,如果为null,Thread-3线程就持有了对象锁,如果不为null,Thread-3线程就会放入monitor的EntryList
阻塞队列中,处于阻塞状态Blocked。
(5) 在 Thread-2 上锁的过程中,如果Thread-4,Thread-5 也来执行 synchronized(obj),也会进入EntryList
BLOCKED
;
(6) Thread-2 执行完同步代码块的内容,就会释放锁,将owner变量置为null,并唤醒EntryList
中阻塞的线程来竞争锁,竞争时是非公平的 ;
(7) 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析
2. snychronized同步代码块原理
public class TestSynchronized { static final Object obj = new Object(); static int i=0; public static void main(String[] args) { synchronized (obj){ i++; } } }
将上面的代码反编译为字节码文件:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: getstatic #2 // 获取obj对象 3: dup 4: astore_1 5: monitorenter //将obj对象的markword置为monitor指针 6: getstatic #3 9: iconst_1 10: iadd 11: putstatic #3 14: aload_1 15: monitorexit //同步代码块正常执行时,将obj对象的markword重置,唤醒EntryList 16: goto 24 19: astore_2 20: aload_1 21: monitorexit //同步代码块出现异常时,将obj对象的markword重置,唤醒EntryList 22: aload_2 23: athrow 24: return Exception table: from to target type 6 16 19 any //监测6-16行jvm指令,如果出现异常就会到第19行 19 22 19 any
这两条指令的作用,我们直接参考JVM规范中描述:
monitorenter 指令:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
(1) 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
(2) 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
(3) 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit指令:
执行monitorexit的线程必须是持有obj锁对象的线程
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程释放monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出IllegalMonitorStateException的异常的原因。
3. synchronized同步方法原理
public class TestSynchronized { static int i=0; public synchronized void add(){ i++; } }
对应的字节码指令:
public synchronized void add(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field i:I 3: iconst_1 4: iadd 5: putstatic #2 // Field i:I 8: return
从反编译的结果来看,方法的同步并没有通过指令monitorenter
和monitorexit
来完成不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED
标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
4. 重量级锁的开销
处于ContentionList
、EntryList
、WaitSet
中的线程都处于阻塞状态,线程的阻塞或者唤醒都需要操作系统来帮忙,Linux内核下采用pthread_mutex_lock系统调用实现,进程需要从用户态切换到内核态。
用户态是应用程序运行的空间,为了能访问到内核管理的资源(例如CPU、内存、I/O),可以通过内核态所提供的访问接口实现,这些接口就叫系统调用。
pthread_mutex_lock系统调用是内核态为用户态进程提供的Linux内核态下互斥锁的访问机制,所以使用pthread_mutex_lock系统调用时,进程需要从用户态切换到内核态,而这种切换是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
由于JVM轻量级锁使用CAS进行自旋抢锁,这些CAS操作都处于用户态下,进程不存在用户态和内核态之间的运行切换,因此JVM轻量级锁开销较小。而JVM重量级锁使用了Linux内核态下的互斥锁,这是重量级锁开销很大的原因。
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!