决战圣地玛丽乔亚Day30---java并发之CLH、AQS
1.CLH队列的实现
SMP(对称多处理器结构),保证CPU的内存一致性,但是可能内存访问冲突和资源浪费。
NUMA(非一致存储访问),将CPU分模块,每个模块多CPU组成,具有独立本地内存和插槽进行互联互通。本地内存的速度远高于系统其他节点的内存速度。
普通的自旋锁:
对某一原子变量,例如当前线程的原子变量,进行循环执行CAS操作,直到成功。
但是!自旋锁在激烈的锁竞争下,性能差,且如果竞争剧烈可能会被插队导致饥饿。
CLH锁其实是对自旋锁的一种改良。
CLH锁的结构类似于一个链表队列,每个元素拥有(Thread,state)两个属性,state用boolean的形式来记录线程是否拥有锁。还有一个Tail指针指向队尾用来加入新的线程。
如果有线程想要获取CLH锁,把线程通过Tail指针加入,然后轮询前一节点的state状态。如果前一指针拥有锁, 需要不断的进行轮询。
需要有一个来获取前驱节点的Node。可以通过Node pre = this.tail.getAndSet(node)来获取 本身的结构是类似链表,但是没有进行连接操作,不算链表。
java实现:
1.这里对于节点使用ThreadLocal,为了让每个节点维护自己的节点对象。
2.对于节点中是否持有锁的状态变量locked使用了volatile修饰。我们知道,volatile主要是为了保证可见性和有序性,由于该状态节点是标识是否持有锁,update的对象只有当前节点,读的对象也只有后继节点会去读。由于一开始线程的状态一定是true
如果是false的话,后继线程就会直接获取锁不会循环等待了。所以如果true变成false没有被立刻感知也没有关系。所以保证可见性不是重要的。但是可以解决指令重排序问题,保证线程之间的通信的有序性,保证了CLH锁正常执行。
3.这里释放锁的过程,用this.node.set(new Node());
是为了防止当前释放的Node被复用导致死锁。
和自旋锁相比:
自旋锁是共同争抢一个原子变量,CLH是分散在每个节点的各自的状态值,减少了争抢的开销。释放锁不需要CAS(因为有序链表没有竞争,不需要使用严谨的CAS) 公平锁。
1.CLH有了等待队列的概念。自旋锁只是一味地检查锁是否可用。
2.自旋锁的每个线程不断自旋,cpu消耗大。CLH锁使用volatile保证每个线程能够看到前驱节点的状态变化,从而减小了对处理器缓存的影响。
可以这么理解,CLH锁只自旋自己的前继节点的lock状态,而自旋锁共同自旋一个原子遍历的状态。通过这种降低自旋范围的方式可以避免线程之间的竞争和干扰,提高了并发性能。
AQS:
结构:(pre、next指针、waitstatus、thread线程、nextWaiter)
在CLH的基础上进行更改:
1.节点状态更丰富: signal(表示该节点正常等待)
propagate(应将releaseShared传播到其他节点)
condition(该节点位于条件队列,不能用于同步队列节点)
cancelled(由于超时、中断或其他原因,该节点被取消)
2.通过阻塞等待替代了自旋获取锁。并显示的维护前驱节点和后继节点。
通过阻塞唤醒的方式来加锁释放锁。
入队列的过程?
1.acquire 请求锁,如果锁当前未被占用直接获得并返回。如果被占用,加入到等待队列自旋直到获取锁。
2.addWaiter 如果当前线程没能获取锁,创建一个Node对象表明线程在等待队列的位置,把Node插入到队尾,返回Node (实际是通过enq方法,通过CAS保证线程安全)
3.aquireQueued 用于在等待队列自旋获取锁。
性能方面?
AQS相比CLH,在高并发的场景下表现更优异。因为线程一多,如果大量线程自旋开销是很大的,不如阻塞住。但是如果严格要求先入先出,还是用CLH好一些。
AQS基于阻塞队列,可以实现可重入锁和读写锁等,功能更丰富。
支持多种同步机制。
AQS的实现方式天然适用于多核处理器,因为它通过自旋和阻塞的方式来解决线程竞争问题,可以让不同的线程在不同的处理器核心上执行,从而避免了多核处理器的争用问题。
自旋等待比线程进入和退出临界区阻塞的效率高,避免了上下文切换和内核用户态之间的切换,大大提高并发效率。
4.Semopher是什么?限流可以用吗?
-
创建一个Semaphore对象,设置初始值为限流的阈值,例如,如果希望限制每秒钟的请求数为100,可以创建一个初始值为100的Semaphore对象。
-
在接受请求之前,使用Semaphore的acquire()方法尝试获取许可证。如果许可证数量已达到阈值,则该方法会被阻塞,直到有许可证可用。
-
如果acquire()方法成功返回,表示已经获得了许可证,可以开始处理请求。
-
处理完请求后,使用Semaphore的release()方法释放许可证,以便其他请求可以继续获取许可证。
-
可以考虑将步骤2-4封装在一个公共的限流函数中,以便可以在需要时调用它。
java代码的大致实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import java.util.concurrent.Semaphore; public class RateLimiter { // 创建Semaphore对象,设置初始值为100,表示最多允许100个请求并发执行 private static final Semaphore semaphore = new Semaphore( 100 ); // 定义一个被限流的函数 public static void handleRequest(String request) { try { // 尝试获取许可证 semaphore.acquire(); // 处理请求 Thread.sleep( 1000 ); System.out.println( "处理请求:" + request); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放许可证 semaphore.release(); } } public static void main(String[] args) { // 模拟1000个请求 for ( int i = 0 ; i < 1000 ; i++) { final int request = i; new Thread(() -> handleRequest(String.valueOf(request))).start(); } } } |
5.ReentrantLock?与Synchronized有什么区别?释放阻塞的指定线程?为什么finally释放?
可重入锁,基于AQS(状态值state代表重入次数)。
首先他搞了一个Sync继承自AQS。公平和非公平的实现就是通过Sync来实现。
Sync: 搞了一个抽象的lock留给子类自己实现具体细节。然后实现AQS的tryrelease释放锁,还自己写了一个非公平获取的算法。
tryrelease的实现主要是通过state的数值来判断,如果-1不为0就更新state的值,如果为0就把state设置为null返回代表锁的释放。
非公平/公平算法的实现:
在获取资源时,锁被其他线程持有,如果当前线程是持有锁的线程,state进行+1,否则返回。锁被别的线程持有,被阻塞这一点是无法被改变的。
在锁被释放的情况下,可以获取到锁,如果是公平模式:会按CLH的队列顺序进行拿锁
如果是非公平模式,是不会按顺序,各凭本事。 如果拿到锁,state+1.否则返回false。
以上就是可重入锁的大致。
问题来了:
1.和Synchronized相比,性能如何?为什么?
java5之前,ReentrantLock是用unsafe类实现,性能不如Synchronized。java5之后,由于ReentrantLock使用CAS和锁的分离,性能超过Synchronized。
在需要读写分离的情况下, ReentrantLock的性能要更好,因为他有ReentrantReadWriteLock
。
2.释放阻塞的指定线程?
可以用condition来进行释放阻塞的指定线程。
condition是和锁的概念类型的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); // 线程A获取锁并执行任务 lock.lock(); try { // 执行任务 ... } finally { lock.unlock(); } // 线程B等待锁的释放 lock.lock(); try { while (someCondition) { condition.await(); // 线程B阻塞 } } finally { lock.unlock(); } // 在某个时刻,线程A执行完任务并释放锁 lock.lock(); try { condition.signal(); // 唤醒线程B } finally { lock.unlock(); } |
这里,线程A和线程B用同一个condition,condition在第二段时候,调用了await被阻塞住,然后在后面线程A调用signal唤醒B线程。达到了释放阻塞的指定线程的作用。
为什么用condition不用wait和notify?同样是阻塞和唤醒。 那个是Object自带的。
Condition
可以与多个等待线程关联,而wait()
和notify()
只能与一个对象关联。Condition
可以防止虚假唤醒,即使没有signal()
或signalAll()
方法的调用,等待线程也不会自动唤醒。Condition
可以支持超时等待,可以设置等待的最长时间,避免因为等待时间过长而导致的线程阻塞问题。
因此,condition可以更加灵活地控制线程的等待和唤醒,并可以避免死锁和竞争产生的问题。
3.为什么finally释放?
因为为了防止某些异常的发生导致锁没有即使释放导致死锁的情况。如果是在ReetrantLock中用,注意调用return时该线程是否持有锁,如果持有锁不主动释放会一直持有导致死锁的发生。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!