原创

Java synchronized底层原理与底层锁优化

synchronized发生异常会自动释放锁

在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的互斥锁来实现的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

使用方式

  • 修饰实例方法方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。synchronized(this)。
  • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例。
    因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
  • 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。最好别用字符串,因为JVM中,字符串常量池具有缓存功能。

双重校验锁实现对象单例(线程安全):

通过volatile保证线程的可见性并防止指令的重新排序,synchronized加锁防止重复实例化。

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

synchronized 关键字的底层原理

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }

    public synchronized void synchronizedMethod() {
        System.out.println("synchronized 方法");
    }
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class。

synchronized代码块:

file

synchronized方法:

file

synchronized代码块

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

monitor原理及执行步骤

因为每个对象头中都存在monitor锁对象,所以java中可以用对象加锁。

对象结构大概分为对象头、实例变量和填充字节。

  • 对象头
    • Mark Word(标记字段):存储对象的hashCode、锁信息或分代年龄或GC标志等信息
    • Klass Pointer(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
    • 数组长度:只有数组对象保存了这部分数据。该数据在32位和64位JVM中长度都是32bit。
  • 实例变量:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
  • 填充字节:第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,查看部分ObjectMonitor.cpp源码:

ObjectMonitor () {
    // 指向持有ObjectMonitor对象的线程地址。
    _owner = NULL;

    //  存放调用wait方法,而进入等待状态的线程的队列。
    _WaitSet = NULL

    // 这里是等待锁block状态的线程的队列。
    _EntryList = NULL;

    // 锁的重入次数。
    _recursions = 0;

    // 线程获取锁的次数。
    _count = 0;
}

执行步骤:

  1. 当多个线程同时访问时,这些线程先被放进_EntryList队列中,线程处于blocked状态。
  2. 当其中的一个线程获取到对象的monitor后,该线程进入running状态,_owner指向当前线程,_count加1表示锁被一个线程获取到。
  3. 当running状态线程调用wait()方法,释放当前线程的monitor对,并进入waiting状态。_owner变为NULL,_count减1,将线程加入都_WaitSet队列中,当有线程notify()该线程时,将该线程加入到_EntryList队列中参与锁的竞争
  4. 当线程执行完成时,释放monitor对象,_owner为NULL,_count减1

synchronized方法

方法头部使用的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

JDK1.6之后synchronized优化

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

如果不了解,请点击学习:Java中的锁膨胀、锁消除、锁粗化、自旋锁、偏向锁、轻量级锁、重量级锁详解

正文到此结束
本文目录