JAVA锁
JAVA锁
文章目录
参考博客: https://www.cnblogs.com/jyroy/p/11365935.html.
极度建议去看参考博客,这个文章写得很棒(๑•̀ㅂ•́)و✧,基本是我见过所有解析java锁最全面的文章了。我这里只是抽取的一些概念出来,并不细节解析这些锁是什么,如果你只是单纯想了解一下,请继续阅读,当然我还是建议你去看参考博文。
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低, 每次去拿数据的时候都认为别人不会修改,所以不会上锁。 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作。
CAS机制就是对乐观锁的一种实现。CAS,全称是Compare And Swap,即比较并交换,是一种乐观锁的实现。
悲观锁
悲观锁是就是悲观思想,与乐观锁相反,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以 每次在读写数据的时候都会上锁 ,这样别人想读写这个数据就会 block 直到拿到锁。
悲观锁的实现有:Synchronized等
自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就 避免用户线程和内核的切换的消耗 。说白了就是等待锁的线程不会立即阻塞,会自旋一段时间,等待其他线程释放锁。
关于自旋锁的自旋时间:因为本身自旋就消耗CPU(不断询问是否能获得锁),如果设置自旋时间过长同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗。所以 JDK1.6中引入了适应性自旋锁 。
JDK1.6 后对Synchronized进行了优化,加入了自旋。
公平锁和非公平锁
公平锁
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁
非公平锁
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。如:Synchronized同步锁。JVM 按随机、就近原则分配锁的机制则称为不公平锁。
- 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
- Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。
- 这些锁都是一种思想,不必过于深究,只要记得 公平锁要排队,非公平锁在排队之前能跟别的线程抢锁,抢不到就排队。
可重入锁 VS 非可重入锁
可重入锁
可重入锁,也叫做递归锁, 指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响 。就是指: 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。 说可能不清楚,看代码:
public synchronized void fun1(){
System.out.println("执行function1");
/*
重入锁允许内部代码获取锁,
因为当前this对象已被当前线程占用,会自动获取锁,
能再次获取锁就能执行fun2了
*/
fun2();
}
public synchronized void fun2(){
System.out.println("执行function2");
}
- Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
非可重入锁
跟可重入锁干好相反,指的是同一线程 外层函数获得锁之后 ,内层递归函数无法获得该锁。 这样常常会出现死锁问题。
public synchronized void fun1(){
System.out.println("执行function1");
/*
fun1执行到这里就卡住了,它无法获得锁去执行fun2,因为不可重入,
只能释放当前this锁,再重新获得当前this锁,
想执行fun2必须先释放当前线程持有的锁,但是,fun1还没
执行完,无法释放当前的锁,使用就死锁卡死了。
*/
fun2();
}
public synchronized void fun2(){
System.out.println("执行function2");
}
独享锁 VS 共享锁
独享锁
独享锁也叫排他锁 ,独占锁模式下,每次只能有一个线程能持有锁, ReentrantLock 就是以独占方式实现的互斥锁 。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
- JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
共享锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
四种锁状态
Java的对象头和对象组成
Java对象部分参考博客:https://www.jianshu.com/p/3d38cba67f8b
在了解锁状态前先了解Java对象组成,并且这四种锁专门针对synchronized。
Java对象
Java对象分成三部分:
- 对象头:用于存储对象的关键描述信息
- 实例数据:对象的数据,实例化对象时的数据。
- 对齐填充字节:JVM要求 java的对象占的内存大小应该是8bit的倍数 ,补齐长度而已,没实际用图。
Java对象头
Java对象头有分为:
- Mark Word:Mark Word记录了对象有关的信息
- 指向类的指针:这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。
- 数组长度:只有数组对象有,记录数组长度。
Mark Word
这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word
的位长度为JVM的一个Word大小。
Mark Word(32bit)长这样:其实就是一堆标记信息
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
Mark Word(64bit)长这样:跟32差不多
|------------------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal |
|------------------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|------------------------------------------------------------------------------|--------------------|
因为这里介绍的是锁等级,我就直接上网找张表,不继续过多解析,通过这个对象头这些标记位我们就可以获知改对象当前的状态了。
biased_lock | lock | 状态 |
---|---|---|
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC标记 |
无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。 CAS原理及应用即是无锁的实现。 无锁只是一种锁等级而已。通俗点: 无锁就是所有线程都能访问同一个资源,但是访问时需要使用CAS的方式访问 , 有兴趣的可以去查一下CAS。
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。 偏向锁用在单线程访问,在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。 通俗一点:偏向锁就是在对象头中的Mark Word的标志位打上标签,表示该对象现在被当前单一线程占有,如果出现其他线程开始抢夺,那么就需要升级偏向锁,变轻量级锁。
但出现多线程访问同一个资源时,偏向锁就会升级,变成轻量级锁。
轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。前面也说到了,自旋会消耗CPU,当多个线程自旋的消耗大于线程阻塞挂起操作的消耗,这时就要对轻量级锁进行升级了,变成重量级锁。
重量级锁
当锁升级到重量级锁,这时就惊动操作系统了,前面的三个锁都是普遍的贴标签,而重量级锁就要动用到操作系统维护的线程队列了。当锁升级到重量级锁,那些拿不到锁的线程就会进入阻塞队列,等待拿到锁的线程释放锁,并由操作系统通知其他线程并唤醒阻塞队列中有资格获得锁的线程(就是排队)。当然要操作系统管理这些线程,肯定消耗资源的,因为要做用户态到核心态的切换。所以才叫重量级锁。
想了解synchronized可以看下我另一篇博客,了解完这些Java锁,看synchronized的实现原理就变得十分简单了。
https://blog.csdn.net/qq_43203949/article/details/115546273
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)