什么是锁
- 简单来说,现实世界常指用某种钥匙、密码或者电路对其他用具的封藏装置,以防被移走或者打开。
- Java中的锁思想也是类似,指的是通过计算机中类似于“独一份”的钥匙来实现对资源的安全访问
Java中都有什么样的锁
上锁态度
从对上锁态度这个角度来说的话,分为两类:悲观锁和乐观锁
- 悲观锁:认为当前场景写入较多,容易发生冲突,所以写入之前会先对数据加锁,这样别人要改必须block到我释放锁才行
- 乐观锁:认为当前场景写入较少,不易发生冲突,会先尝试去写入,如果失败,需要重新获取最新版本数据进行更新后写入
上锁顺序
从上锁顺序来讲的话,分为两类:公平锁和非公平锁
- 公平锁:等待获取锁的线程,如果按照排队顺序来获取锁
- 非公平锁:实际获锁顺序与排队顺序无关
上锁范围
从锁定范围上来划分,分为两类:互斥锁和共享锁
- 只block写的锁是读共享锁
- 读写都block是互斥锁,也叫和同步锁
上锁成本
从锁本身粒度上来划分,分为自旋锁、轻量级锁和重量级锁
- 自旋锁:为了让线程不释放CPU而执行一个慢循环
- 轻量级锁:基于CAS原子操作指令
- 重量级锁: 基于操作系统的互斥量
其他特性
- 可重入:已经获锁的线程可以在不释放的前提下继续获锁
- 可偏向:通过对象头markword标记,虚拟机可以不执行任何操作的情况下获锁
为什么用锁
- 本质通过一些标志位来完成对计算机资源的独占,从而保证临界区的数据安全
Java中如何使用锁
- Java5之前,通过synchronized来完成同步
- Java5之后提供了synchronized和Lock两种方式上锁
synchronized
Java5之前使用synchronized关键字来保证同步
- 作用于实例方法时,锁住的是对象的实例(this)
- 作用于静态方法时,锁住的是类实例Class,会锁所有调用该方法的线程
- 作用于对象实例时,锁住的是所有以该对象为锁的代码块
- 从上锁态度来看:悲观锁, 认为写冲突较高,所以操作之前先获取锁
- 从上锁顺序来看:非公平锁,等待获取锁的线程在锁可用时,谁先就绪就可以先获取到锁
- 从上锁范围来看:独占/互斥锁, 只允许当前线程进入临界区
- 从上锁代价来看:Java8 synchronized锁会从偏向 -> 轻量 -> 重量级的进行逐步升级上锁
- 从锁的特性来看:不可重入
举个栗子
package jdk;
public class SynchronizedExample {
public static void main(String[] args) {
synchronized(SynchronizedExample.class) {
System.out.println("hello,SynchronizedExample");
}
}
}
编译之后字节码如下:
可以看到monitorenter和monitorexit的指令,分别代表进入监控和退出监控,可以理解为代码块执行前的加锁和退出同步块时的解锁,那么这两个指令具体底层干了什么?
线程执行monitorenter指令时,线程会为锁分配一个ObjectMonitor对象,对应JDK中objectMonitor.cpp文件(源码地址)
从分析源码可知,ObjectMonitor封装了线程信息和锁的相关信息,包含几个重要的属性
- owner: 为当前持有锁的线程信息
- waitSet:wait方法调用后的线程等待队列
- cxq: 阻塞等待队列
- entryList: 刚进来的排队队列 流程如下:
底层是如何去实现
JDK1.5 synchronized是一个重量级锁,1.6之后开始对它进行优化,引入了偏向锁、轻量级锁和自旋锁的概念,上一节内部机制中提到synchronized的作用对象是对象实例和类对象,所以我们从对象入手,看其如何能实现互斥
JVM中,对象在内存中分为三块区域:对象头、实例数据和对齐填充:
- 对象头MarkWord:用于存储对象自身运行时的数据,如哈希码(hashcode),gc分代年龄、锁状态标识、偏向线程ID和偏向时间戳等信息,依旧对象的状态复用自己的存储空间,是实现轻量级锁和偏向锁的关键
- 类型指针:对象会指向它的类的元数据的指针,虚拟机通过这个指针确认这个对象是哪个类的实例,ArrayLength:如果对象是数组,用来记录数组长度
- 实例数据:存放类的属性数据信息,包括父类的属性信息。
- 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
下面我们以32位对象头为例,展示对象头存储结构:
- 无锁状态下, 从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同上
锁膨胀过程如下: