深度分析synchronized实现原理

305 阅读4分钟

前言 在Java开发过程中,我们为了提高效率往往采用多线程的方式,然而多线程也伴随着一些问题,比如:存在多个线程操作共享资源导致的数据问题。

当然Java也为我们提供了此类问题的解决办法:synchronized关键字,今天我主要和大家分享一下关于synchronized的一些原理探寻。

一、同步锁的作用范围

既然是原理探寻,概念性的东西我就不再多说,大家都知道synchronized是一种同步锁机制,它保证同一时刻,只允许一个线程访问被synchronized修饰的方法或者代码块,那么它的作用范围又是怎样的呢?

其实在synchronized中,提供了两种锁:类锁与对象锁

  • 类锁:全局性质,当多个线程调用不同实例的同步方法会互斥
public class Test{
    public void method(){
        synchronized(Test.class){
            //代码
        }
    }
    
    public static void main(String[] args){
       Test test1 = new Test();
       Test test2 = new Test();
       new Thread(()->test1.method(),"thread1").start();
       new Thread(()->test2.method(),"thread2").start();
    }

}
  • 对象锁:针对实例对象,当多个线程调用用一个对象实例的同步方法会互斥
public class Test{
    static Object object = new Object();
    public void method(){
        synchronized(object){
            //代码
        }
    }
    
    public static void main(String[] args){
       Test test1 = new Test();
       Test test2 = new Test();
       new Thread(()->test1.method(),"thread1").start();
       new Thread(()->test2.method(),"thread2").start();
    }

}

二、对象头是中如何记录锁的标记

synchronized既然保证了多线程访问同一资源的互斥性,那肯定会有一个标记来识别当前锁的状态,是直接拿锁执行还是等待锁的释放。这个状态就存在对象头中。 一个 对象是由对象头实例数据以及对齐填充三部分组成的。

image.png 其中对象头里的Mark Word便记录了锁的相关信息。

锁状态是否是偏向锁锁标记位
无锁001
偏向锁101
轻量级锁00
重量级锁10

2.1 锁升级

其实针对一个对象而言,也并非一开始就采用重量级锁,在这其中也会存在一个锁升级过程:

无锁-->偏向锁-->轻量级锁-->重量级锁

image.png

三、JDK1.6前后的synchronized

  • jdk1.6之前:

    synchronized只有重量级锁机制,没有获取锁的线程会通过park方法进行阻塞等待。底层依赖于操作系统的Lock来实现,然而涉及到操作系统层面将线程阻塞,就得进行用户态与内核态的切换,对于性能来说是影响很大的。

  • jdk1.6以后:

    synchronized进行了一定程度上的优化,新增了偏向锁与轻量级锁,让线程在不阻塞的情况下达到线程安全的目的。从而在性能与线程安全之间做好一个平衡。

3.1偏向锁原理

偏向锁其实就是在没有线程竞争(单线程情况下)的情况去访问synchronized修饰的代码块,先尝试通过偏向锁来获取访问资格的这么一个过程。

当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01” (默认标志位就是01),即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁 相关的同步块时,虚拟机都可以不再进行任何同步操作。

-XX:+UseBiasedLocking //启动参数
-XX:+BiasedLockingStartupDelay=0 //设置延迟启动时间

image.png

3.2轻量级锁原理

既然偏向锁是单线程情况下,那么多线程访问同步方法,那些没有抢占到锁资源的线程不能像重量级锁一样去阻塞等待,很影响性能。所以就有一种新的方案:轻量级锁,也就是让没有抢占到锁的线程进行一定次数的自旋

image.png 当然也不可能让其一直自旋,自旋的线程会浪费CPU资源,所以默认是10次,当到达10次后,还未能获取锁资源,则会升级为重量级锁;不过jdk1.6以后也出现了自适应自旋锁,自旋次数不是固定的,会根据上一次的自旋次数自己调整决定。

3.3重量级锁原理

自然自旋10次都未能获取锁资源,就不能一直再这样消耗CPU资源,此时就只能是升级为重量级锁,从操作系统层面将线程挂起,等待获取锁资源时再唤醒。

image.png

参考书籍《Java并发编程深度解析与实战》