Java-多线程并发之锁原理
LockSupport工具类:
主要用于挂起和唤醒线程,是创建锁和其他同步类的基础。
每个使用LockSupport的线程会和他关联一个许可证。
主要功能由Unsafe类实现:
1.park():如果已经拿到了许可证,就返回。没有拿到就阻塞。不会抛异常。
2.unpark(Thread thread):1) 无条件下调用,如果传入的线程没有许可证,就给许可证。 2) 如果线程调用了park被挂起就被唤醒。
最好对park进行判断,因为这个方法不会抛异常。也不会告诉你为什么park被返回了。
3.parkNanos(long nanos):与park类似。 拿到许可证就返回,如果没有拿到就在nanos时间后自动返回。
4.park(Object blocker):当调用park被挂起时会记录到线程内部。用诊断工具getBlock(Thread),可以把block设置成this。就可以获取有关阻塞的信息。
Thread类里边有个变量 volatile Object parkBlocker. 就是用来存放blocker对象的
5.parkNanos(Object blocker,long nanos)
相比park(Object blocker)多了一个超时的时间
6.parkUntil(Object blocker,long deadline) 这个deadline是一个随机的时间。
抽象同步队列AQS:AbstractQueueSynchronizer,同步器的基础组件,锁的底层结构。
AQS的结构:
1.AQS是一个FIFO(first in first out)的双向队列,有头尾指针(head,tail)。在AQS的类中有一个静态内部类Node。Node组成了AQS。
Node中:
thread变量:存放进入AQS队列的线程
exclusive:用来标记该线程是获取独占资源时被挂起放入AQS队列的。
shared:用来标记该线程时获取共享资源被阻塞挂起放入AQS队列的。
waitStatus:记录当前线程等待的状态(cancel线程被取消,signal需要被唤醒,condition在条件队列,propagate释放共享资源需要通知其他节点)
pre:前驱节点
next:后继节点
state:状态信息。可以通过get,set,CAS进行操作。state在不同的锁中有不同的涵义。是一个灵活的int变量。
ConditionObject内部类:结合锁实现线程的同步。可以直接访问AQS对象内部的变量,如state或者队列。
ConditionObject是条件变量,每个条件变量对应一个条件队列(单链表),用来存放调用条件变量的await方法后被阻塞的线程。
2.AQS的线程同步的关键是对state进行操作。根据state是否属于一个线程,操作state的方式分为独占和共享:
独占:acquire(int args) acquireInterruptibly(int args) release(int args)
共享:acquireShared acquireSharedInterruptibly releaseShared
1)独占方式获取资源:与具体线程绑定,其他线程尝试去操作state获取资源会获取失败阻塞。
比如独占锁ReentrantLock,线程获取到独占锁后,通过CAS把state从0->1,然后设置当前独占锁的持有者为currentThread。
当该线程再次去获取锁,会把状态值从1变成2,代表可重入次数。
当有其他线程去获取锁的时候发现该锁的持有者不是自己,就会被放入AQS阻塞队列挂起。
2)共享方式获取资源:不与具体线程绑定,通过CAS去竞争资源,如果一个线程获得资源后其他线程再去获取资源还能满足需要,CAS即可。
比如Semaphore信号量。当一个线程通过acquire获取信号量,看信号量个数是否满足,不满足就进阻塞队列,满足就通过CAS自旋获取。
3.对于独占锁:
acquire。 release.....
对于共享锁:
acquireShared releaseShared...
在acquire方法中通过tryAcquire设置state的值,失败则把Node封装


入队操作和双向链表队尾添加元素的操作一致。需要判断是否是初始化添加。
AQS条件变量的支持:
Synchronized的wait和notify对应
AQS中的signal和await操作。不同的是Synchronized同时只能与一个共享变量的notify和wait同步。而AQS的一个锁可以对应多个条件变量。
在使用signal和await之前仍然需要获取条件变量对应的锁。
lock.newCondition()的作用就是创建一个锁对应的条件变量ConditionObject。
获取独占锁。lock.lock();相当于进入Synchronized代码块(即获取共享变量的内部锁)
lock.unlock()相当于退出Synchronized代码块
代码4调用了条件变量condition.await()阻塞挂起了当前的线程。就相当于调用共享变量的wait()方法。
调用condition.signal()就相当于调用了共享变量的notify()方法。
每个条件变量都维护一个条件队列,用来存放被await阻塞挂起的线程
await():
在内部构造一个Node.Condition的Node节点,然后将该节点插入条件队列的队尾,修改state值以释放锁,阻塞挂起。如果有其他的现在再进行lock.lock获取锁并使用await,循环步骤。
signal():
把条件队列的队头的线程节点移除队列加入AQS阻塞队列,然后激活这个线程。

Sync实现lock。
并根据RenntrantLock的参数是否是fair选择公平和非公平的Sync同步,默认是非公平的

1.CAS将0->1 说明获取到了锁。
2.setExclusiveOwnerThread.设置该锁的持有线程
重写Acquire:
unlock就是先对线程进行一系列的判断。然后通过CAS把state-1,再判断减完是否为0去释放锁。
如果三个线程同时去获取独占锁ReentrantLock,加入线程1争取到了锁,那么剩下的2、3线程就进AQS阻塞队列中等待。
如果线程1获取到锁的情况下,调用了条件变量,例如条件变量用了await,这时就需要1释放锁,然后AQS阻塞队列中的2、3线程根据是否是公平的策略去获取锁。
如果是公平的情况下,通过底层的算法判断是否有前驱节点在他进来之前就在等待这个锁,如果有的话就先给他。
如果是非公平的策略下,就谁后进来谁先获取,抢占式获取。
这时候,线程1由于调用await被阻塞挂起,进入那个条件变量的条件阻塞队列中等待,条件变量1调用signal唤醒他。这就是抢占式的锁ReentrantLock.
读写锁RenntrantReadWriteLock:
写锁:
当其他线程没有读锁、写锁的时候。可以获取写锁。实现的方法主要是通过判断AQS的satate的值。
state的低16位和高16位分别代表写锁和读锁。
读锁:
首先要判断写锁是否被占用,如果写锁被占用不能获取读锁。如果没有其他线程获取写锁,读锁可以被占用。
读锁是共享锁。
如果一个线程有读锁和写锁(先写后读),要在释放读锁时候把写锁一起释放掉。
Lock过程:
SharedCount():持有读锁的线程数
readerShouldBlock():当前有别的线程在尝试获取锁的时候是否需要阻塞
cacheHoldCounter():记录最后一个获取到读锁的线程和该线程的锁可重入数
判断写锁是否被占用,如果占用阻塞>>>>>>获取持有读锁的线数>>>>>>>判断是否超出最大值,调用readerShouldBlock判断是否应该阻塞,是否可以CAS成功,如果都成功,获取读锁成功。
参数设置更新>>>>>>没有获取到读锁的自旋等待。
参数更新:记录第一个和最后一个线程,和该线程获取读锁可重入次数
释放锁的过程不再赘述。
小结:
线程进入读锁的前提条件:
没有其他线程的写锁,
没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
线程进入写锁的前提条件:
没有其他线程的读锁
没有其他线程的写锁
而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。
JDK8新增:StampedLock
不直接实现Lock和ReadWriteLock接口,内部自己维护一个双向阻塞队列。
读写都不可重入,在多读的情况下,乐观读的效率会提高很多,因为不进行CAS,而是简单的测试状态。
在获取锁时候都返回一个stamp状态值,在释放锁和转换锁的时候需要传入获取时返回的stamp。
三种模式:
独占写锁:不可重入的独占写。
悲观读:在没有独占写的情况下,多个线程可以同时获取该锁。与ReentrantReadWriteLock类似。
乐观读:最大的不同是操作数据前不进行CAS。 获取stamp后需要进行validate有效性验证判断,判断是否有其他线程持有了写锁。
操作的数据是方法栈里面的数据,是一个快照。进行validate验证及时止损。如果validate发现其他线程也持有了读锁,要么重试要么切换成悲观锁。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!