Java面试必问之-锁底层原理

本文深入探讨了Java中synchronized的锁底层原理,包括对象锁、方法锁、锁对象、锁升级优化,以及与ReentrantLock、AQS、CLH同步队列的关系。讲解了synchronized如何实现线程同步,以及与Lock的差异,重点剖析了锁的优化策略,如偏向锁、轻量级锁、重量级锁,展示了Java锁机制的高效性和灵活性。
摘要由CSDN通过智能技术生成

Java锁底层原理

当多个线程需要访问某个公共资源的时候,我们知道需要通过加锁来保证资源的访问不会出问题。java提供了两种方式来加锁

一种是关键字:synchronized,一种是concurrent包下的lock锁。

 

在这里插入图片描述

 

 

synchronized

  1. synchronized的作用:保证了原子性、可见性、有序性
为什么synchronized无法禁止指令重排,却能保证有序性?
为了进一步提升计算机各方面能力,在硬件层面做了很多优化,如处理器优化和指令重排等,但是这些技术的引入就会导致有序性问题。
我们也知道,最好的解决有序性问题的办法,就是禁止处理器优化和指令重排,就像volatile中使用内存屏障一样。
虽然很多硬件都会为了优化做一些重排,但是在Java中,不管怎么排序,都不能影响单线程程序的执行结果。这就是as-if-serial语义,所有硬件优化的前提都是必须遵守as-if-serial语义。
再说下synchronized,他是Java提供的锁,可以通过他对Java中的对象加锁,并且他是一种排他的、可重入的锁。
所以,当某个线程执行到一段被synchronized修饰的代码之前,会先进行加锁,执行完之后再进行解锁。在加锁之后,解锁之前,其他线程是无法再次获得锁的,只有这条加锁线程可以重复获得该锁。
synchronized通过排他锁的方式就保证了同一时间内,被synchronized修饰的代码是单线程执行的。所以呢,这就满足了as-if-serial语义的一个关键前提,那就是单线程,因为有as-if-serial语义保证,单线程的有序性就天然存在了。
复制代码
  1. Synchronized可以把修饰的任何一个非null对象作为"锁",synchronized的用法有以下三种:
  • 修饰实例方法:锁住的是对象实例this,属于对象锁
  • 修饰静态方法:锁住的是对象class实例,属于类锁
  • 修饰代码块:锁住的是括号里面的对象实例,属于对象锁

注意,synchronized内置锁是一种对象锁(锁的是对象而非引用变量),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。其可重入最大的作用是避免死锁,如:子类同步方法调用了父类同步方法,如没有可重入的特性,则会发生死锁;

synchronized的底层同步原理

synchronized是在软件层面依赖于JVM,而j.u.c下的lock是依赖于硬件层面。

Synchronized底层原理分为2中:对象锁和方法锁,即修饰对象和修饰方法

1.Synchronized修饰对象

如果synchronized修饰的是对象,那么它是依赖于monitor对象—监视器锁来实现锁的机制的。

public class SynchronizeTest {
    public void method(){
        synchronized (this) {  // 锁的是调用该method方法的实例对象this
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "+++++++" + i);
            }
        }
    }
}
复制代码

先编译上述文件为class文件:javac -encoding utf-8 类名.java

反编译class类文件: javap -c 类名.class

 

在这里插入图片描述

 

 

编译结果解析:

  • monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被其他线程占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
  1. 如果monitor的进入数为0,则该线程将进入monitor,然后将进入monitor的进入数设置为1,该线程即为monitor的所有者;
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
  • monitorexit:执行monitorexit的线程必须是objecter所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

    monitorexit指令如果出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;

通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理:

Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,

这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法, 否则会抛出 java.lang.IllegalMonitorStateException 的异常的原因。

Synchronized修饰方法

如果synchronized修饰的是方法,那么则是通过ACC_SYNCHRONIZED 标示符来进行加锁的。

public class SynchronizeTest {
   public synchronized void method() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "+++++++" + i);
        }
    }
}
复制代码

反编译如下:

 

在这里插入图片描述

 

 

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 标志符是否被设置,如果设置了,线程将会先获取monitor,获取成功之后才能执行方法体,方法执行完之后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,但会导致 “用户态和内核态” 两个态之间来回切换,对性能有较大影响。

Synchronized的锁对象

无论是实例对象(包括实例this和方法)还是类对象。在JVM中,每个对象都是由三部分组成的:对象头、实例数据、数据填充。synchronized的锁的信息都是存储在对象头里。对象组成结构如下:

 

img

 

 

  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充:由于虚拟机要求,对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
  • 对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是,如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

Synchronized用的锁就是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。其中 Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。 Java对象头具体结构描述如下:

在这里插入图片描述

 

 

其中Mark Word在默认情况下存储着对象的哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,以下是32位JVM的Mark Word默认存储结构:

在这里插入图片描述

 

 

对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,可能变化为存储以下4种数据:

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值