Synchronized锁详解

在Java中,synchronized锁可能是我们最早接触的锁了,在 JDK1.5之前synchronized是一个重量级锁,相对于juc包中的Lock,synchronized显得比较笨重

庆幸的是在 Java 6 之后 Java 官⽅对从 JVM 层⾯对synchronized进行⼤优化,所以现在的 synchronized 锁效率也优化得很不错。

为什么呢?

因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,后续都有细节说明。
 

1. synchronized的类别

synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。

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;
    }
}

2. synchronized作用

  • (1)、原子性所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。
  • (2)、可见性:**可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。 **synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。
  • (3)、有序性有序性值程序执行的顺序按照代码先后执行。 synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

3. synchronized底层实现原理

synchronized底层实现主要依赖java对象头和monitor:

java对象头

java实例对象存储在Hotspot虚拟中主要包括三个模块,如下图:

这里主要讲解一下对象头的组成,如下图:

对象头中,涉及锁部分的markWord我们这里着重讲解一下,构成如下图:

 

监视器(Monitor)

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

  1. MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁;
  2. MonitorExit指令:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit;

那什么是Monitor?可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。

与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁

也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的。

JVM底层原理

synchronized同步代码块的情况

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

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

image.png

从上面我们可以看出:

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

当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。

另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因(抛出该异常,表示一个线程已经尝试等待一个对象的监视器,或者通知正在等待一个对象的监视器的其他线程,而没有拥有指定的监视器)。

public static void main(String[] args) {
        Object o = new Object();
        try {
            o.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        o.notify();
    }
//Exception in thread "main" java.lang.IllegalMonitorStateException
	//at java.lang.Object.wait(Native Method)
	//at java.lang.Object.wait(Object.java:502)
	//at com.pingfa.two.lock.Test.main(Test.java:15)


synchronized 必须是进入同一个对象的monitor才有上述的效果。每一个对象会有一个monitor,不加synchronized的对象,不会关联监视器,不遵从以上规则。

在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchronized同步方法的情况

public class SynchronizedDemo2 {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}


synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

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

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

不过两者的本质都是对对象监视器 monitor 的获取。

monitor的结构:

image.png

刚开始 Monitor 中 Owner 为 null
当 Thread-2 执行 synchronized(obj) 就会将Monitor的所有者 Owner 置为Thread-2 ,Monitor中只能有一个Owner
在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED
Thread-2执行完同步代码块中的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的
图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,在wait-notify中
synchronized 必须是进入同一个对象的monitor才有上述的效果。每一个对象会有一个monitor,不加synchronized的对象,不会关联监视器,不遵从以上规则。

4. synchronized锁状态及升级过程

 

 汇总及转换流程图

   

5. 额外补充

 hotspot对锁进行升级,不仅可以锁消除,还可以锁粗化:

参考文章:

synchronized详解_万里长江雪的博客-CSDN博客_synchronized

synchronized详解 - 三分恶 - 博客园

  • 5
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
Java中,synchronized的升级是指在不同的场景下,的实现方式会有所不同,以提高性能和并发控制的效率。下面是synchronized的升级详解: 1. 偏向(Biased Locking):当一个线程访问同步块时,首先会尝试获取偏向。如果偏向未被其他线程占用,则当前线程会获得偏向,并标记为偏向线程ID。这样,在后续进入同步块时,无需再进行的竞争,提高了性能。只有当其他线程尝试获取偏向时,才会撤销偏向状态。 2. 轻量级(Lightweight Locking):当多个线程尝试竞争同一个时,偏向会升级为轻量级。轻量级使用CAS操作来实现加和解,避免了线程阻塞和唤醒的开销。如果CAS操作失败,表示存在竞争,会升级为重量级。 3. 重量级(Heavyweight Locking):当轻量级竞争失败时,会升级为重量级。重量级使用操作系统的互斥量(Mutex)来实现,被阻塞的线程会进入等待状态,直到被释放。重量级的竞争会导致线程的上下文切换和调度开销增加,性能较低。 synchronized的升级过程是根据实际情况和竞争情况动态进行的。在大多数情况下,的升级是逐级升级的,即从偏向到轻量级,再到重量级。这种的升级机制是为了在减少竞争时提供更好的性能,并在存在竞争时保证线程安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值