前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文带你读懂JDK源码:synchronized

一文带你读懂JDK源码:synchronized

作者头像
后台技术汇
发布2022-05-28 12:29:54
2350
发布2022-05-28 12:29:54
举报
文章被收录于专栏:后台技术汇

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 源码和底层原理,希望对大家有所帮助。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后台技术汇 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • synchronized 的四个特征
  • synchronized支持类锁与对象锁
  • 与ReentrantLock的区别
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档