天天看点

一文带你读懂JDK源码:synchronized

Java提供的常用同步手段之一就是sychronized关键字,synchronized 是利用锁的机制来实现同步的。

下文我们从3个角度深入剖析synchronized的应用原理:

  • synchronized的四个特点(原子性/有序性/互斥性/可重入)
  • synchronized的两种锁分类(类锁/对象锁)
  • synchronized与ReentrantLock的区别

winter

必须先提及一个重要的基础概念:Monitor监听机制。Monitor是什么?Monitor 是Java中实现 synchronized关键字的基础,可以将它理解为一个监听器,是用来实现同步的工具,monitor与每一个Java对象与class字节码相关联。monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因。(参考:《JVM锁优化》)Monitor的本质?

Monitor 在JVM中是基于C++的实现的,ObjectMonitor中有几个关键属性,见下图:

一文带你读懂JDK源码:synchronized
  • _owner:指向持有ObjectMonitor对象的线程

  • _WaitSet:存放处于wait状态的线程队列

  • _EntryList:存放处于等待锁block状态的线程队列

  • _recursions:锁的重入次数

  • _count:用来记录该线程获取锁的次数

加锁过程:当多个线程(A/B/C)同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程A获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1,即(该线程A)获得锁。线程B/C都进入了_EntryList里面,线程A进入了_Owner。

    

释放锁过程:

若持有monitor的线程A调用wait()方法,将释放它当前持有的monitor,_owner变量恢复为null,_count自减1,同时线程A进入_WaitSet集合中等待被唤醒。

此时在_EntryList的线程B/C会竞争获取monitor,假设结果是B线程竞争成功并进入了_Owner。线程C留在了_EntryList里面,线程A进入了_WaitSet。

若当前线程B执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。线程B可以通过notify/notifyAll 来唤醒 _WaitSet 的线程A,此时_WaitSet 的线程A 与 _EntryList 的线程C会同时进行锁资源竞争。注意:1、由于notify唤醒线程具有随机性,甚至导致死锁发生;因此一般建议使用notifyAll。2、不管唤醒一个线程,还是唤醒多个线程,最终获得对象锁的,只有一个线程。如果_EntryList同时存在竞争锁资源的线程,那么被唤醒的线程还需要和_EntryList中的线程一起竞争锁资源。但是JVM保证最终只会让一个线程获取到锁。

一文带你读懂JDK源码:synchronized
一文带你读懂JDK源码:synchronized
一文带你读懂JDK源码:synchronized

synchronized 的四个特征

基于 monitor 机制,引出了 synchronized 的四个特征:

1.原子性

基于monitor监视器,被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断(除了已经废弃的stop()方法),即保证了原子性。

2.可见性

基于monitor监视器,synchronized 对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的。在释放锁之前会将对变量的修改刷新到主内存当中,从而保证资源变量的可见性。

3.有序性

基于monitor监视器,有效解决重排序问题:指令重排并不会影响单线程的顺序和结果,它影响的是多线程并发执行的顺序性。而 synchronized 保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

4.可重入性

synchronized和ReentrantLock都是可重入锁。

当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态;

当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

synchronized 的效果:可以具体体现为 “monitorenter”与“monitorexit”两条指令(一个monitor exit指令之前都必须有一个monitor enter),下面是编译文件的例子:

一文带你读懂JDK源码:synchronized

对 synchronized 的优化,参考《JVM的锁优化》可知,锁升级的过程是:偏向锁 -> 轻量级锁 -> 重量级锁。

一文带你读懂JDK源码:synchronized
一文带你读懂JDK源码:synchronized
一文带你读懂JDK源码:synchronized

synchronized支持类锁与对象锁

例子1:类锁对于类锁,我们必须理解两种使用场景:

  • 修饰一个静态的方法:其作用的范围是整个方法,作用的对象是这个类的所有对象
  • 修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象

例子1.1:修饰一个静态的方法

//类锁:静态方法 - 修饰一个静态的方法              public static synchronized void lock() throws InterruptedException {              //延时1s执行日志输出              TimeUnit.SECONDS.sleep(1);              System.out.println("lock1 executeTime = " + System.currentTimeMillis());              }           

例子1.2:修饰一个类

//类锁:类名 - 修饰一个类              public static void lock2() throws InterruptedException {              synchronized (ClassLock.class){              //延时1s执行日志输出              TimeUnit.SECONDS.sleep(1);              System.out.println("lock2 executeTime = " + System.currentTimeMillis());              }              }           

测试用例:

/**              * <p>              *     类锁资源竞争例子:              *      1、修饰一个静态的方法              *      2、修饰一个类              * </p>              */              public class ClassLock {              public static void main(String[] args) throws InterruptedException{              Thread t1 = new Thread(new Runnable() {              @Override              public void run() {              ClassLock classLock = new ClassLock();              try {              //          classLock.lock();              classLock.lock2();              } catch (InterruptedException e) {              e.printStackTrace();              }              }              });              Thread t2 = new Thread(new Runnable() {              @Override              public void run() {              ClassLock classLock = new ClassLock();              try {              //          classLock.lock();              classLock.lock2();              } catch (InterruptedException e) {              e.printStackTrace();              }              }              });              t1.start();              t2.start();              //由于t1 和 t2 存在类锁资源竞争,所以两个线程真正执行时间是不一样的                }              }           

输出结果:类锁的两种锁都存在竞争互斥,因此代码段都不是同时被执行。

lock1 executeTime = 1616499560230              lock2 executeTime = 1616499561230              lock1 executeTime = 1616499562231              lock2 executeTime = 1616499563231           

例子2:对象锁对于对象锁,我们必须理解两种使用场景:

  • 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  • 修饰一个代码块:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象;

例子2.1:修饰一个方法

//对象锁:普通方法              public synchronized void lock() throws InterruptedException {              //延时1s执行日志输出              TimeUnit.SECONDS.sleep(1);              System.out.println("lock1 executeTime = " + System.currentTimeMillis());              }           

例子2.2:修饰一个代码块

//对象锁:普通方法代码块              public void lock2() throws InterruptedException {              synchronized (this){              //延时1s执行日志输出              TimeUnit.SECONDS.sleep(1);              System.out.println("lock2 executeTime = " + System.currentTimeMillis());              }              }           
public class ObjectLock {              public static void main(String[] args) throws InterruptedException{              Thread t1 = new Thread(new Runnable() {              @Override              public void run() {              ObjectLock classLock = new ObjectLock();              try {              classLock.lock();              //          classLock.lock2();              } catch (InterruptedException e) {              e.printStackTrace();              }              }              });              Thread t2 = new Thread(new Runnable() {              @Override              public void run() {              ObjectLock classLock = new ObjectLock();              try {              classLock.lock();              //          classLock.lock2();              } catch (InterruptedException e) {              e.printStackTrace();              }              }              });              t1.start();              t2.start();              //由于t1 和 t2 不存在对象锁资源竞争,所以两个线程真正执行时间一样              }              }           

输出结果:多线程使用的对象锁不存在互斥竞争,因此都是同时被执行了。

lock1 executeTime = 1616499668823              lock1 executeTime = 1616499668823              lock2 executeTime = 1616499669823              lock2 executeTime = 1616499669823           
一文带你读懂JDK源码:synchronized
一文带你读懂JDK源码:synchronized

与ReentrantLock的区别

下面分别从六个角度阐述两者(synchronized 与 ReentrantLock)的区别:

底层实现/可中断机制支持/释放锁方式(手动/非手动)/锁类型(公平锁/非公平锁)/等待线程的精确唤醒/锁对象。

1、底层实现

synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法;同时涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁(参考另一篇文章:JVM锁的升级)ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁,是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。

    2、不可中断执行synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成;

ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

    3、jvm底层释放资源

synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用;ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。

    4、是否公平锁 synchronized为非公平锁(参考开头的Monitor锁资源竞争策略);ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。

    5、锁是否可绑定条件Condition进行准确的线程唤醒synchronized不能绑定并精确唤醒某一个线程资源,通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程;ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized。

    6、锁的对象synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程,和int类型的state标识锁的获得/争抢。

一文带你读懂JDK源码:synchronized
一文带你读懂JDK源码:synchronized

总结

上文结合synchronized的底层原理 -- Monitor机制,分别从3个角度(synchronized的特点/锁分类/与ReentrantLock的区别)剖析了 synchronized 的原理与应用。

后续我们会继续探讨 volatile 重排序 与 ReentranLock 源码和底层原理,希望对大家有所帮助。

扫描二维码

获取技术干货

一文带你读懂JDK源码:synchronized

继续阅读