所谓锁是用来控制多个线程访问共享资源的**方式**,所以锁在Java中并不仅仅指代的就是Java中的对象,例如*自旋锁*用的就是指令的方式。通常的,一个锁能够防止多个线程同时访问共享资源(也称为临界区)。
在JDK5之前,Java中的锁是通过关键字Synchronized来实现的,而在JDK5之后Lock接口的出现使得锁的使用更加的灵活。同时在JDK6开始对Synchronized关键字进行了锁升级的优化,使其能够适用于更多的场景,不再是严格意义上的*重量级锁*。
## Synchronized关键字
意为同步的,Synchronized关键字可以隐性的将一个java对象设置为锁,在《Java虚拟机规范》中并没有对Synchronized做特定的约束,synchronized的实现依照各虚拟机产商而定。在Hotspot(常说的Java虚拟机基本就是指这个)中是在对象的头部信息中使用mark word域(32位或者64位)来表示对象的锁信息。在JDK6的优化之后,Synchronized关键字被引入了一个`锁升级`的概念,其升级过程如下:
:-: 偏向锁 --轻量级锁 --重量级锁
Synchronized锁的升级与锁对象的头部信息的mark word字段息息相关,对象头结构及各种状态下的mark word域的结构如下图:
:-: ![](https://img.kancloud.cn/fa/aa/faaa72468848c9c6aec3bd3e114c4779_504x310.png)
> 参考:《Java并发编程的艺术》P12
其中mark word可以用来储存对象的hashcode、锁信息、垃圾回收时的分代年龄等信息。Class Metadata Address保存对象的类型(class类)数据的指针。32位的Array Length用在数组对象中保存数组对象的长度。
不同状态下mark word的内容:
:-: ![](https://img.kancloud.cn/06/86/0686961c77c48da265cd108ad39ccada_1067x370.png)
> 参考:《黑马程序员-并发编程》
因此一个对象有如下四种锁状态:“无锁状态”、“偏向锁状态”、“轻量级锁状态”、“重量级锁状态”。其中:
* baised\_lock表示是否是偏向锁,0表示不是*偏向锁*,1表示是*偏向锁*;
* age表示垃圾回收时的分代年龄;
* epoch用在批量重偏向中;
* threadID表示获取*偏向锁*的线程ID;
* ptr\_to\_lock\_record指向栈帧中的lock record记录;
* ptr\_to\_monitor指向monitor对象。
### 偏向锁状态
*偏向锁*指的是当锁不存在多个线程竞争,并且经常由一个线程获取锁对象时,为了让线程获取锁的代价更低,在锁对象的mark word中保存了当前获取锁对象线程的线程ID,下次该线程要获取锁的时候可以不使用CAS的方式获取锁,而是直接通过比较线程ID来再次获取锁对象。
例如:
<iframe src="https://carbon.now.sh/embed?bg=rgba%2831%2C129%2C109%2C1%29&t=night-owl&wt=none&l=auto&ds=false&dsyoff=20px&dsblur=68px&wc=true&wa=true&pv=19px&ph=23px&ln=true&fl=1&fm=Hack&fs=14.5px&lh=143%25&si=false&es=2x&wm=false&code=public%2520class%2520Test%2520%257B%250A%2520%2520%2520%2520private%2520static%2520final%2520Object%2520lock%2520%253D%2520new%2520Object%28%29%253B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%250A%2520%2520%2520%2520%250A%2520%2520%2520%2520public%2520static%2520void%2520m1%28%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520synchronized%28lock%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520m2%28%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%250A%2520%2520%2520%2520public%2520static%2520void%2520m2%28%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520synchronized%28lock%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520m3%28%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%250A%2520%2520%2520%2520public%2520static%2520void%2520m3%28%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520synchronized%28lock%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%252F%252F%2520...%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%250A%2520%2520%2520%2520public%2520static%2520void%2520main%28String%255B%255D%2520args%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520m1%28%29%253B%250A%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%250A%257D" style="width: 960px; height: 632px; border:0; transform: scale(1); overflow:hidden;" sandbox="allow-scripts allow-same-origin"> </iframe>
:-: ![](https://img.kancloud.cn/61/ea/61ea81b21e5a62429c8e19fa6836df7e_938x306.png)
判断线程ID一样之后就会直接获取锁,而不会采用cas的方式来自旋获取锁。同时采用这种方式*偏向锁*即是可重入的锁。
一个对象在刚被创建的时候处于无锁状态(**mark word后三位为:001**,其余全为0),但是几秒之后会变成*偏向锁*状态(mark word后三位为:**101**,其余全为0),可以使用虚拟机参数:“**\-XX:BiasedLockingStartupDelay=0**”来关闭这种延迟的特性,同时如果想要关闭*偏向锁*的话可以使用虚拟机参数:“**\-XX:-UseBiasedLocking**”来进行关闭。
例如:
<iframe src="https://carbon.now.sh/embed?bg=rgba%2831%2C129%2C109%2C1%29&t=night-owl&wt=none&l=auto&ds=false&dsyoff=20px&dsblur=68px&wc=true&wa=true&pv=19px&ph=23px&ln=true&fl=1&fm=Hack&fs=14.5px&lh=143%25&si=false&es=2x&wm=false&code=%252F**%250A%2520*%2520%25E4%25BD%25BF%25E7%2594%25A8%25E7%25AC%25AC%25E4%25B8%2589%25E6%2596%25B9%25E5%25B7%25A5%25E5%2585%25B7jol%25E6%259F%25A5%25E7%259C%258B%25E5%25AF%25B9%25E8%25B1%25A1%25E7%259A%2584%25E5%25A4%25B4%25E9%2583%25A8%25E4%25BF%25A1%25E6%2581%25AF%250A%2520*%252F%250A%250Apublic%2520class%2520TestBaisedLock%2520%257B%250A%2520%2520%2520%2520public%2520static%2520void%2520main%28String%255B%255D%2520args%29%2520%257B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%250A%2520%2520%2520%2520%2520%2520%2520%2520Object%2520object%2520%253D%2520new%2520Object%28%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520ClassLayout%2520classLayout%2520%253D%2520ClassLayout.parseInstance%28object%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%250A%2520%2520%2520%2520%2520%2520%2520%2520new%2520Thread%28%28%29%2520-%253E%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520System.out.println%28classLayout.toPrintable%28object%29%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520synchronized%2520%28object%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520System.out.println%28classLayout.toPrintable%28object%29%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520System.out.println%28classLayout.toPrintable%28object%29%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%29.start%28%29%253B%250A%250A%2520%2520%2520%2520%257D%250A%257D" style="width: 1024px; height: 492px; border:0; transform: scale(1); overflow:hidden;" sandbox="allow-scripts allow-same-origin"> </iframe>
输出的mark word的主要内容如下:
~~~
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000296f840c005 (biased: 0x00000000a5be1030; epoch: 0; age: 0)
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000296f840c005 (biased: 0x00000000a5be1030; epoch: 0; age: 0)
~~~
说明:value字段的内容为16进制,最后的5用二进制位表示为:0101,即为*偏向锁*状态。(注意这里在测试的时候并没有添加关闭偏向锁延迟加载的虚拟机参数,但是可能由于程序启动太慢了导致第一个显示的也是偏向锁状态?)
添加了虚拟机参数“-XX:-UseBiasedLocking”关闭*偏向锁*后的输出:
~~~
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000004bd2bff420 (thin lock: 0x0000004bd2bff420)
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
~~~
说明:关闭偏向锁之后在第一次调用synchronized时候直接变成了轻量级锁状态。
备注:jol依赖引入如下,注意scope属性不要provider!
~~~
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
~~~
### 轻量级锁
*轻量级锁*状态发生在当有多个线程访问同步代码块,但是访问时间是错开的,彼此之间没有竞争的时候。(注意*偏向锁*是发生在**同一个线程**反复访问同步代码块的时候。)
*轻量级锁*状态的记录不再跟偏向锁一样在对象的mark word域中记录thread id,而是使用了线程栈中的**栈帧的锁记录结构**来记录*轻量级锁*状态。
![](https://img.kancloud.cn/41/b2/41b2d5680ac6071fc3602cf686ec44ec_1176x644.png)
1. 加锁过程:让锁记录中的对象引用指向对象地址,同时采用CAS的方式将锁记录的地址替换到对象的mark word域中,对象的hashcode和分代年龄等信息则放到锁记录中进行暂存。CAS操作有如下两种情况:
- CAS成功则当前线程获取锁,对象头的mark down信息存储`锁记录地址和最后面的锁状态改为00`。
![](https://img.kancloud.cn/d4/06/d406ab709016ac853c4b0039c6380028_1288x653.png)
- 如果失败,有两种情况,一种是确实是当前线程获取了锁对象,这个时候就发生了锁的重入,只需要多添加一条Lock Record作为锁的重入计数即可。
![](https://img.kancloud.cn/8a/8d/8a8d06bafe636c9da6fa0da3837115c7_1146x639.png)
另外一种是别的线程获取了锁对象正在访问同步代码块,这个时候就会发生锁膨胀,将轻量级锁膨胀为为重量级锁。
2. 解锁过程:当退出同步代码块发现有取值为null的锁记录,则证明有重入现象,只要去掉即可。如果取值不为null,则用CAS操作将对象的mark word内容重置。该操作有两种情况:
* 成功:则解锁成功。
* 失败:说明轻量级锁已经膨胀成了重量级锁,因为这个时候对象头已经不再是指向锁记录了,而是保存了重量级锁的monitor信息,进入重量级锁的解锁流程。
### 重量级锁
#### Monitor对象
重量级锁和一个Monitor对象有关。每个Java对象都可以关联一个Monitor对象,如果使用synchronized关键字给对象加上重量级锁的时候,锁对象的mark word域就会指向Monitor对象。
Monitor对象的结构如下:
![](https://img.kancloud.cn/37/0c/370c50ffa6bed891cbb3738bd045818c_1192x519.png)
* Monitor刚创建的时候Owner为null。
* 当Thread-0执行synchronized(object)时,Monitor就会将Owner设置为Thread-0,Monitor只能有一个Owner。
* 在Thread-0上锁的过程中,如果有其他线程Thread-1、Thread-2等线程来竞争锁,就会进入EntryList Block中。
* 在Thread-0中执行完同步代码块后,会唤醒EntryList中的等待线程来竞争锁,竞争的时候是非公平的。
* WaitSet的内容与wait-notify有关(等待/通知模型中要用到的等待队列)。
> 注意:
> synchronized必须是进入同一个对象的monitor才有上述效果。
用下面代码为例查看monitor起的作用
~~~
static final Object obj = new Object();
static int count = 0;
public static void main(String[] args) {
synchronized (obj) {
count++;
}
}
~~~
对应的字节码:
~~~
Code:
stack=2, locals=3, args_size=1
0: getstatic #7 // Field obj:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter // 插入monitorenter指令
6: getstatic #13 // Field count:I
9: iconst_1
10: iadd
11: putstatic #13 // Field count:I
14: aload_1
15: monitorexit // 插入monitorexit指令
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
~~~
当synchronized处于重量级锁的状态的时候,JVM会在进入同步代码块之前插入monitorenter指令,保证线程获取锁;在退出代码块之前插入monitorexit指令,保证锁被释放。
当synchronized处于重量级锁的状态的时候,从源码上来看会先自旋一定次数的CAS,然后再进入阻塞队列中,并将线程阻塞住。
> 注意jdk1.8中轻量级锁并不会自选,重量级锁才会自选。
## Volatile关键字
Volatile是轻量级的synchronizd,能够保证共享变量的”可见性“。所谓可见性:当一个线程修改一个共享变量的时候,另外一个线程能够读取到其修改的值。当一个字段被声明成volatile时,Java线程内存模型确保所有的线程看到这个变量是一致的。
底层实现的相关术语:
* 内存屏障:一组处理器指令,用于实现对内存操作的顺序限制。
* 原子操作:不可中断的一个或一系列操作。
**Volatile保持可见性的原理:**
在被volatile声明了的变量中,当要进行写操作的时候,jvm会添加一条lock前缀的cpu的指令,该指令可实现如下两个目的:
* 将当前处理器的缓存行写回内存中。
* 这个写回的操作使得其他cpu缓存了该内存地址的数据无效。通过嗅探技术监控自己缓存的数据是不是过期了。具体的协议就是MESI协议。
# Lock接口
从上面我们知道了synchronized关键字能够隐性的获取锁,同时介绍了synchronized锁升级的概念,锁升级可以使得synchronized的性能得到大大的优化,不在是严格意义上的重量级锁。但是,synchronized这种隐性获取锁的方式虽然简化了同步的方式,同时也给访问共享资源的扩展带来了一定的困难。
而Lock接口(JDK5后提供)所代表的锁就能够在某些场景下具有较大的灵活性,能够让程序员显式的来操作锁对象。Lock锁的使用方式大致如下:
~~~
Lock lock = new ReentrantLock();
lock.lock(); // 加锁
try {
// 同步代码块
} finally {
lock.unlock(); // 解锁
}
~~~
**Lock接口的特性:**
1. 尝试非阻塞的获取锁:当有多个线程竞争锁的时候,如果这一时刻锁没有被其他线程获取,则当前线程能够成功并持有锁;
2. 能被中断的获取锁:synchronized是不能响应中断,而使用了Lock接口的锁是可以响应中断的;
3. 超时获取锁:可以在创建锁对象的时候设置超时时间,如果截止时间到了仍旧无法获取锁则返回。
**基本API**
1. 获取锁,在没有其他线程获取锁的时候调用该方法的线程会获取锁。
~~~
void lock();
~~~
2. 可中断获取锁,该方法可以响应中断,在锁的获取中可以响应中断当前线程。
~~~
void lockInterruptibly() throws InterruptedException
~~~
3. 尝试非阻塞的获取锁,能够成功获取返回true,否则返回false。
~~~
boolean tryLock();
~~~
4. 超时获取锁
~~~
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
~~~
5. 释放锁
~~~
void unlock();
~~~
6. 获取等待通知组件
~~~
Condition newCondition();
~~~
- 第一章 Java基础
- ThreadLocal
- Java异常体系
- Java集合框架
- List接口及其实现类
- Queue接口及其实现类
- Set接口及其实现类
- Map接口及其实现类
- JDK1.8新特性
- Lambda表达式
- 常用函数式接口
- stream流
- 面试
- 第二章 Java虚拟机
- 第一节、运行时数据区
- 第二节、垃圾回收
- 第三节、类加载机制
- 第四节、类文件与字节码指令
- 第五节、语法糖
- 第六节、运行期优化
- 面试常见问题
- 第三章 并发编程
- 第一节、Java中的线程
- 第二节、Java中的锁
- 第三节、线程池
- 第四节、并发工具类
- AQS
- 第四章 网络编程
- WebSocket协议
- Netty
- Netty入门
- Netty-自定义协议
- 面试题
- IO
- 网络IO模型
- 第五章 操作系统
- IO
- 文件系统的相关概念
- Java几种文件读写方式性能对比
- Socket
- 内存管理
- 进程、线程、协程
- IO模型的演化过程
- 第六章 计算机网络
- 第七章 消息队列
- RabbitMQ
- 第八章 开发框架
- Spring
- Spring事务
- Spring MVC
- Spring Boot
- Mybatis
- Mybatis-Plus
- Shiro
- 第九章 数据库
- Mysql
- Mysql中的索引
- Mysql中的锁
- 面试常见问题
- Mysql中的日志
- InnoDB存储引擎
- 事务
- Redis
- redis的数据类型
- redis数据结构
- Redis主从复制
- 哨兵模式
- 面试题
- Spring Boot整合Lettuce+Redisson实现布隆过滤器
- 集群
- Redis网络IO模型
- 第十章 设计模式
- 设计模式-七大原则
- 设计模式-单例模式
- 设计模式-备忘录模式
- 设计模式-原型模式
- 设计模式-责任链模式
- 设计模式-过滤模式
- 设计模式-观察者模式
- 设计模式-工厂方法模式
- 设计模式-抽象工厂模式
- 设计模式-代理模式
- 第十一章 后端开发常用工具、库
- Docker
- Docker安装Mysql
- 第十二章 中间件
- ZooKeeper