• Java程序员必会Synchronized底层原理剖析


    synchronized作为Java程序员最常用同步工具,很多人却对它的用法和实现原理一知半解,以至于还有不少人认为synchronized是重量级锁,性能较差,尽量少用。

    但不可否认的是synchronized依然是并发首选工具,连volatile、CAS、ReentrantLock都无法动摇synchronized的地位。synchronized是工作面试中的必备技能,今天就跟着一灯一块深入剖析synchronized的底层原理。

    1. synchronized作用

    synchronized是Java提供一种隐式锁,无需开发者手动加锁释放锁。保证多线程并发情况下数据的安全性,实现了同一个时刻只有一个线程能访问资源,其他线程只能阻塞等待,简单说就是互斥同步。

    2. synchronized用法

    先看一下synchronized有哪几种用法?

    使用位置 被锁对象 示例代码
    实例方法 实例对象 public synchronized void method() {
    ……
    }
    静态方法 class类 public static synchronized void method() {
    ……
    }
    实例对象 实例对象 public void method() {
    Object obj = new Object();
    synchronized (obj) {
    ……
    }
    }
    类对象 class类 public void method() {
    synchronized (Demo.class) {
    ……
    }
    }
    this关键字 实例对象 public void method() {
    synchronized (this) {
    ……
    }
    }

    可以看到被锁对象只要有两种,实例对象和class类。

    • 由于静态方法可以通过类名直接访问,所以它跟直接加锁在class类上是一样的。

    • 当在实例方法、实例对象、this关键字上面加锁的时候,锁定范围都是当前实例对象。

    • 实例对象上面的锁和class类上面的锁,两者不互斥。

    3. synchronized加锁原理

    当我们使用synchronized在方法和对象上加锁的时候,Java底层到底怎么实现加锁的?

    当在类对象上加锁的时候,也就是在class类加锁,代码如下:

    /**
     * @author 一灯架构
     * @apiNote Synchronized示例
     **/
    public class SynchronizedDemo {
    
        public void method() {
            synchronized (SynchronizedDemo.class) {
                System.out.println("Hello world!");
            }
        }
    
    }
    

    反编译一下,看一下源码实现:

    image

    可以看到,底层是通过monitorentermonitorexit两个关键字实现的加锁与释放锁,执行同步代码之前使用monitorenter加锁,执行完同步代码使用monitorexit释放锁,抛出异常的时候也是用monitorexit释放锁。

    写成伪代码,类似下面这样:

    /**
     * @author 一灯架构
     * @apiNote Synchronized示例
     **/
    public class SynchronizedDemo {
    
        public void method() {
            try {
                monitorenter 加锁;
                System.out.println("Hello world!");
                monitorexit 释放锁;
            } catch (Exception e) {
                monitorexit 释放锁;
            }
        }
    
    }
    

    当在实例方法上加锁,底层是怎么实现的呢?代码如下:

    /**
     * @author 一灯架构
     * @apiNote Synchronized示例
     **/
    public class SynchronizedDemo {
    
        public static synchronized void method() {
            System.out.println("Hello world!");
        }
    
    }
    

    再反编译看一下底层实现:

    image

    这次只使用了一个ACC_SYNCHRONIZED关键字,实现了隐式的加锁与释放锁。其实无论是ACC_SYNCHRONIZED关键字,还是monitorentermonitorexit,底层都是通过获取monitor锁来实现的加锁与释放锁。

    monitor锁又是通过ObjectMonitor来实现的,虚拟机中ObjectMonitor数据结构如下(C++实现的):

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; // WaitSet 和 EntryList 的节点数之和
        _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 ;
      }
    

    image

    图上展示了ObjectMonitor的基本工作机制:

    1. 当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列中等待。

    2. 当某个线程获取到对象的Monitor锁后进入临界区域,并把Monitor中的 _owner 变量设置为当前线程,同时Monitor中的计数器 _count 加1。即获得对象锁。

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

    4. 在_WaitSet 集合中的线程会被再次放到_EntryList 队列中,重新竞争获取锁。

    5. 若当前线程执行完毕也将释放Monitor并复位变量的值,以便其他线程进入获取锁。

    线程争抢锁的过程要比上面展示得更加复杂。除了_EntryList 这个双向链表用来保存竞争的线程,ObjectMonitor中还有另外一个单向链表 _cxq,由两个队列来共同管理并发的线程。

    image

    下篇再讲一下Synchronized锁优化的过程。

    我是「一灯架构」,如果本文对你有帮助,欢迎各位小伙伴点赞、评论和关注,感谢各位老铁,我们下期见

    image

  • 相关阅读:
    “碳中和”愿景下,什么样的数据中心才是我们需要的?
    Java-基于SSM+JSP的医院挂号管理系统
    【Redis】基础数据结构-skiplist跳跃表
    gson TypeAdapter 适配器
    抖 X-Bongus 参数逆向 python案例实战
    A-LEVEL经济知识点讲解:国际收支的结构
    电子战基本概念 (01)
    02 重学js-原型链,this指向及改变this指向
    Hadoop - 安装
    golang 解析oracle 数据文件头
  • 原文地址:https://www.cnblogs.com/yidengjiagou/p/16806353.html