一文带你读懂JDK源码:synchronized

2022-05-28 12:29:54 浏览数 (1)

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中有几个关键属性,见下图:

  • _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保证最终只会让一个线程获取到锁。

synchronized 的四个特征

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

1.原子性

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

2.可见性

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

3.有序性

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

4.可重入性

synchronized和ReentrantLock都是可重入锁。

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

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

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

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

synchronized支持类锁与对象锁

例子1:类锁

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

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

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

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

例子1.2:修饰一个类

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

测试用例:

代码语言:javascript复制
/**
 * <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 存在类锁资源竞争,所以两个线程真正执行时间是不一样的
  }
}

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

代码语言:javascript复制
lock1 executeTime = 1616499560230
lock2 executeTime = 1616499561230
lock1 executeTime = 1616499562231
lock2 executeTime = 1616499563231

例子2:对象锁

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

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

例子2.1:修饰一个方法

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

例子2.2:修饰一个代码块

代码语言:javascript复制
  //对象锁:普通方法代码块
  public void lock2() throws InterruptedException {
    synchronized (this){
      //延时1s执行日志输出
      TimeUnit.SECONDS.sleep(1);
      System.out.println("lock2 executeTime = "   System.currentTimeMillis());
    }
  }

测试用例:

代码语言:javascript复制
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 不存在对象锁资源竞争,所以两个线程真正执行时间一样
  }
}

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

代码语言:javascript复制
lock1 executeTime = 1616499668823
lock1 executeTime = 1616499668823
lock2 executeTime = 1616499669823
lock2 executeTime = 1616499669823

与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标识锁的获得/争抢。

总结

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

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

0 人点赞