多线程系列2-锁
锁的分类:
1、重入锁和不可重入锁
当前线程是否可以重入锁。synchronized、ReentrantLock、ReentrantReadWriteLock都是可重入锁。
2、乐观锁和悲观锁
用户态、内核态,jvm如果可以直接发指令让cpu去执行,不需要操作系统调度,那么这个就是用户态需要借助操作系统才能去执行指令。
悲观锁:获取不到锁资源时,会将当前线程挂起(进入BLOCKED、WAITING),线程挂起会涉及到用户态和内核太的切换,而这种切换是比较消耗资源的
线程的挂起和唤醒都需要借助操作系统去实现。所以就是从用户态切换到内核态。切换是有消耗的。
3、公平锁和非公平锁
一般获取方式:公平锁-> 先看线程state是否可以直接拿到锁。如果不可以,则看自己是不是链表的第一个,如果是则立即开始竞争。如果不是就排队。
非公平锁-> 先看线程state是否可以直接拿到锁。如果不可以,则再去竞争一次,如果还是竞争不上就去排队,排队时,看自己是不是链表的第一个,如果是则立即开始竞争。如果不是就排队。

4、互斥锁和共享锁

深入synchronized关键字:
基于对象的
同步方法、同步代码块

举例:

synchronized优化:
无非就是锁消除、锁膨胀(锁粗化),升级过程如下:
单线程是可重入锁,是偏向锁。
如果有其他线程假如,那么就升级为轻量级锁(cas),如果获取不到,触犯为重量级锁。


MarkWord的实现,对象头信息有锁的标志位。

为了看到对象的markword对象头信息:
需要导入依赖:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>

在偏向锁升级为轻量级锁时,需要等一个安全点,假如jvm才开始启动的时候,那么这时候锁标志位是轻量级锁,假如已经是5秒过后,那么这个时候是偏向锁。

hpp文件(应该是c++定义类的文件),即C++代码的源码。



实现了线程的挂起、唤醒、排队的一些列操作的方法。
代码结构图,主要有state、双向链表Node。

非公平锁的效率更高,高于公平锁。
非公平锁的效率更高,高于公平锁。
非公平锁的效率更高,高于公平锁。
非公平锁的效率更高,高于公平锁。
AQS加锁流程:

线程C来了也会立即做CAS操作。所以叫非公平锁。
当线程获取锁失败,需要将自身挂起的时候的操作:
锁的waitStatus状态:
线程挂起过后,需要前一个节点来唤醒下一个节点
当waitStatus=Signal,表示下一个节点可以挂起。
如果waitStatus不正常,那么需要将自己的头结点指针往前跨一步。
1:表示线程已取消等待(如超时或中断),需从队列中移除
-1:表示下一个线程可以挂起,自己执行完过后唤醒下一个线程
0:初始状态
-2:节点处于条件队列(与Condition相关,非同步队列)
-3:共享模式下传播唤醒状态(ReentrantLock为独占模式,不涉及此状态)
UNSAFE.park(),表示挂起线程,将自身线程挂起,将running线程状态变成waiting状态。
在AQS取消节点时,需注意:

保证前面的节点的waitStatus是-1,表示需要唤醒下一个节点。


释放锁:

释放锁时需满足以下条件:
1、释放锁必须是要持有锁的当前线程去释放。比如t1持有的锁,main线程去释放就会失败
2、需判断state的值,state等于0表示释放干净,才会去释放,可重入的锁state会加1
3、去唤醒下一个节点的时候,需要满足waitStatus=-1的条件,这个是有效条件。
唤醒方法:UNSAFE.unpark(thread)。

线程可以重复唤醒,不会报错。
唤醒方法注意:需判断head节点的waitStatus是否为-1,如果不为-1则终止操作。
当唤醒线程时,是要重新竞争锁

这里是个死循环,会重复去获取锁。
reentrantLocal ConditionObject 介绍:
像synchronized提供了wait和notify的方法实现线程在持有锁时,可以实现挂起,已经唤醒的操作。
ReentrantLock也拥有这个功能。
ReentrantLock提供了await和signal方法去实现类似wait和notify的功能。
想执行await或者是signal就必须先持有lock锁的资源。


非常重要
非常重要
非常重要
condition.await():是将自身线程await过后,意思是从running状态进入waitting状态,进入单链表nextWaitor里,当condition.singal()这个方法过后,是将线程放入AQS双链表中,唤醒了需要重新竞争锁。
这里让线程等待的方法都是用的LockSupport.park(this);
在condition.singal()方法过后,恢复执行后,需要重新获取锁资源,走AQS那一套逻辑。当锁释放过后队列里的线程重新竞争。


写锁的加锁流程:
1、写线程来竞争写锁资源
2、写线程会直接通过tryAcquire获取写锁资源(公平锁&非公平锁)
3、获取state值,并且拿到低16位的值。
4、如果state值为不为0,判断是否是锁重入操作,判断当前持有写锁的线程是否是当前线程
5、如果state值为0:
5.1、判断是否是公平锁:公平锁 -> 看自己是否是头结点.next,如果是我就抢,不是就排队。
5.2、判断是否是非公平锁: 非公平锁 -> 看自己是否是头结点,直接抢一手
6、如果拿到锁资源,直接告辞,如果没有拿到去排队,而排队的逻辑和ReentrantLock一样
偶然一看:

LockSupport是Java并发包中用于线程阻塞和唤醒的核心工具类,其底层实现基于sun.misc.Unsafe类和操作系统级线程调度46。以下是关键实现细节:
1. 核心方法实现
-
park()
通过Unsafe.park()调用操作系统原语(如Linux的futex)阻塞当前线程,线程状态变为WAITING68。- 若线程已有许可(通过
unpark()预先设置),则立即返回6。 - 否则阻塞直到被唤醒或中断48。
- 若线程已有许可(通过
-
unpark(Thread thread)
通过Unsafe.unpark()唤醒指定线程,设置线程的许可标志为可用68。- 若线程尚未阻塞,则下次调用
park()时不会阻塞6。
- 若线程尚未阻塞,则下次调用
-
带超时的方法(如
parkNanos())
类似park(),但通过纳秒级超时参数控制阻塞时长,底层依赖系统计时器68。
2. 许可机制
- 每个线程关联一个二元许可状态(类似信号量)6:
unpark()设置许可为可用(最多累积1次)68。park()消耗许可并立即返回,否则阻塞6。
3. 底层依赖
-
sun.misc.Unsafe
直接操作线程内存和系统调用,绕过JVM限制48。 - 操作系统原语
如Linux的futex(快速用户空间互斥锁)实现高效线程挂起/唤醒68。
4. 与synchronized对比
- 灵活性:无需绑定对象监视器,可直接控制线程48。
- 可靠性:
unpark()可先于park()调用,避免线程永久阻塞6。
5. 应用场景
- 实现锁(如
ReentrantLock)、条件变量(Condition)等高级同步工具46。 - 构建自定义并发结构(如线程池任务调度)8。
完整实现可参考LockSupport类源码,其通过Unsafe和操作系统协作完成线程控制46。
LockSupport的底层实现机制主要分为以下几个层面:
- 许可机制核心设计
- 采用二元信号量模型,每个线程关联一个虚拟的permit(许可证)
- permit只有0/1两种状态,unpark()置1,park()消费后置023
- 允许先unpark后park,解决传统wait/notify的时序问题28
- 用户态与内核态协作
- 优先尝试用户态解决方案:
- park()先检查permit状态,若为1则直接返回35
- 通过自旋短暂等待避免立即进入内核阻塞3
- 最终可能进入内核态:
- 长时间阻塞时通过pthread_cond_wait等系统调用实现38
- Unsafe类桥接
- 核心方法通过Unsafe本地方法实现:
// HotSpot关键实现 public native void park(boolean isAbsolute, long time); public native void unpark(Thread thread); - 参数说明:
- isAbsolute:时间单位是否为绝对时间(false表示相对时间纳秒)8
- time:超时时间或截止时间戳58
- 与synchronized对比优势
- 不依赖对象监视器,直接操作线程46
- 精准唤醒指定线程(notify随机唤醒)26
- 无死锁风险(许可机制避免资源占用)25
- 典型应用场景
- AQS(AbstractQueuedSynchronizer)的底层支持16
- 线程池工作线程的阻塞/唤醒4
- StampedLock等高级锁的实现基础
浙公网安备 33010602011771号