Synchronized底层
Synchronized底层如何实现
相关的CSDN博客:
synchronized俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能够持有对象锁,其他线程再想获取这个对象锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切
换所打断
Synchronized有三种使用方法:
- 修饰实例方法,作用相当于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
- 修饰静态方法,相当于在当前类对象加锁,进入同步代码前要获得当前类对象实例的锁。
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库的时候要获得给定对象的锁。
synchronized关键字的实现
synchronized不论是修饰代码块还是修饰方法都是通过持有对象锁来实现同步的。而这个对象的markword就指向了一个Monitor(锁/监视器)
1、java对象头的markword结构:
对象大致可以分为三个部分,分别是对象头,实例变量和填充字节,对象头分成两个部分:mark word和 klass word
锁的类型和状态在对象头Mark Word中都有记录。在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。对于重量级锁对象的markword包含两个部分:指向重量级锁的指针和标志位
由此看来,monitor锁对象地址存在于每个Java对象的对象头中
2、Monitor结构:
每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)
ObjectMonitor() { _header = NULL; _count = 0; //记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
3、synchronized底层原理 = java对象头markword + 操作系统对象monitor:
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
- synchronized无论是加在同步代码块还是方法上,效果都是加在对象上,其原理都是对一个对象上锁
- 如何给这个obj上锁呢?当一个线程Thread-1要执行临界区的代码时,首先会通过obj对象的markword指向一个monitor锁对象
- 当Thread-1线程持有monitor对象后,就会把monitor中的owner变量设置为当前线程Thread-1,同时计数器count+1表示当前对象锁被一个线程获取。
- 当另一个线程Thread-2想要执行临界区的代码时,要判断monitor对象的属性Owner是否为null,如果为null,Thread-2线程就持有了对象锁可以执行临界区的代码,如果不为null,Thread-2线程就会放入monitor的EntryList阻塞队列中,处于阻塞状态Blocked。
- 当Thread-0将临界区的代码执行完毕,将释放monitor(锁)并将owner变量置为null,同时计算器count-1,并通知EntryList阻塞队列中的线程,唤醒里面的线程
Synchronized 上锁原理
实现原理: JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。
具体实现是在编译之后在同步方法调用前加入一个monitor.enter
指令,在退出方法和异常处插入monitor.exit
的指令。
对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit
之后才能尝试继续获取锁。
流程图如下:
monitorenter 指令:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.(可重入锁的原因)
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit指令:
执行monitorexit的线程必须是持有obj锁对象的线程
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程释放monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出IllegalMonitorStateException的异常的原因。
使用javap -c Synchronize
可以查看编译之后的具体信息。
可以看到在同步块的入口和出口分别有monitorenter
和monitorexit
指令。当执行monitorenter
指令时,线程试图获取锁也就是获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit
指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。