JDK系列—聊聊java中的那些琐

151 阅读5分钟

什么是锁

  • 简单来说,现实世界常指用某种钥匙、密码或者电路对其他用具的封藏装置,以防被移走或者打开。
  • Java中的锁思想也是类似,指的是通过计算机中类似于“独一份”的钥匙来实现对资源的安全访问

Java中都有什么样的锁

Java中的锁.png

上锁态度

从对上锁态度这个角度来说的话,分为两类:悲观锁和乐观锁

  • 悲观锁:认为当前场景写入较多,容易发生冲突,所以写入之前会先对数据加锁,这样别人要改必须block到我释放锁才行
  • 乐观锁:认为当前场景写入较少,不易发生冲突,会先尝试去写入,如果失败,需要重新获取最新版本数据进行更新后写入

上锁顺序

从上锁顺序来讲的话,分为两类:公平锁和非公平锁

  • 公平锁:等待获取锁的线程,如果按照排队顺序来获取锁
  • 非公平锁:实际获锁顺序与排队顺序无关

上锁范围

从锁定范围上来划分,分为两类:互斥锁和共享锁

  • 只block写的锁是读共享锁
  • 读写都block是互斥锁,也叫和同步锁

上锁成本

从锁本身粒度上来划分,分为自旋锁、轻量级锁和重量级锁

  • 自旋锁:为了让线程不释放CPU而执行一个慢循环
  • 轻量级锁:基于CAS原子操作指令
  • 重量级锁: 基于操作系统的互斥量

其他特性

  • 可重入:已经获锁的线程可以在不释放的前提下继续获锁
  • 可偏向:通过对象头markword标记,虚拟机可以不执行任何操作的情况下获锁

为什么用锁

  • 本质通过一些标志位来完成对计算机资源的独占,从而保证临界区的数据安全

Java中如何使用锁

  • Java5之前,通过synchronized来完成同步
  • Java5之后提供了synchronized和Lock两种方式上锁

synchronized

Java5之前使用synchronized关键字来保证同步

  1. 作用于实例方法时,锁住的是对象的实例(this)
  2. 作用于静态方法时,锁住的是类实例Class,会锁所有调用该方法的线程
  3. 作用于对象实例时,锁住的是所有以该对象为锁的代码块
  • 从上锁态度来看:悲观锁, 认为写冲突较高,所以操作之前先获取锁
  • 从上锁顺序来看:非公平锁,等待获取锁的线程在锁可用时,谁先就绪就可以先获取到锁
  • 从上锁范围来看:独占/互斥锁, 只允许当前线程进入临界区
  • 从上锁代价来看:Java8 synchronized锁会从偏向 -> 轻量 -> 重量级的进行逐步升级上锁
  • 从锁的特性来看:不可重入

举个栗子

package jdk;

public class SynchronizedExample {
    public static void main(String[] args) {
        synchronized(SynchronizedExample.class) {
            System.out.println("hello,SynchronizedExample");
        }
    }
}

编译之后字节码如下:

SynchronizedExample.jpg

可以看到monitorenter和monitorexit的指令,分别代表进入监控和退出监控,可以理解为代码块执行前的加锁和退出同步块时的解锁,那么这两个指令具体底层干了什么?

线程执行monitorenter指令时,线程会为锁分配一个ObjectMonitor对象,对应JDK中objectMonitor.cpp文件(源码地址

从分析源码可知,ObjectMonitor封装了线程信息和锁的相关信息,包含几个重要的属性

  • owner: 为当前持有锁的线程信息
  • waitSet:wait方法调用后的线程等待队列
  • cxq: 阻塞等待队列
  • entryList: 刚进来的排队队列 流程如下:

内部原理流程图.png

底层是如何去实现

JDK1.5 synchronized是一个重量级锁,1.6之后开始对它进行优化,引入了偏向锁、轻量级锁和自旋锁的概念,上一节内部机制中提到synchronized的作用对象是对象实例和类对象,所以我们从对象入手,看其如何能实现互斥

JVM中,对象在内存中分为三块区域:对象头、实例数据和对齐填充:

  1. 对象头MarkWord:用于存储对象自身运行时的数据,如哈希码(hashcode),gc分代年龄、锁状态标识、偏向线程ID和偏向时间戳等信息,依旧对象的状态复用自己的存储空间,是实现轻量级锁和偏向锁的关键
  2. 类型指针:对象会指向它的类的元数据的指针,虚拟机通过这个指针确认这个对象是哪个类的实例,ArrayLength:如果对象是数组,用来记录数组长度
  3. 实例数据:存放类的属性数据信息,包括父类的属性信息。
  4. 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

下面我们以32位对象头为例,展示对象头存储结构:

image.png

  • 无锁状态下, 从1-25bit, 是对象的hashcode,26-29bit是分代年龄,30bit代表是否是偏向锁,31~32bit代表是当前锁状态
  • 偏向锁状态下, 从1-23bit,线程id,24-25bit是锁时间戳epoch,26-29bit是分代年龄,30bit同上,31-32bit同上
  • 轻量锁状态下, 从1-30bit,指向栈中锁记录的指针,31-32bit同上
  • 轻量锁状态下, 从1-30bit,指向互斥量的指针,31-32bit同上
  • CG标记:前30位都是空,31-32bit同上

锁膨胀过程如下:

锁膨胀.drawio.png