决战圣地玛丽乔亚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是什么?限流可以用吗?

  1. 创建一个Semaphore对象,设置初始值为限流的阈值,例如,如果希望限制每秒钟的请求数为100,可以创建一个初始值为100的Semaphore对象。

  2. 在接受请求之前,使用Semaphore的acquire()方法尝试获取许可证。如果许可证数量已达到阈值,则该方法会被阻塞,直到有许可证可用。

  3. 如果acquire()方法成功返回,表示已经获得了许可证,可以开始处理请求。

  4. 处理完请求后,使用Semaphore的release()方法释放许可证,以便其他请求可以继续获取许可证。

  5. 可以考虑将步骤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自带的。

  1. Condition可以与多个等待线程关联,而wait()notify()只能与一个对象关联。
  2. Condition可以防止虚假唤醒,即使没有signal()signalAll()方法的调用,等待线程也不会自动唤醒。
  3. Condition可以支持超时等待,可以设置等待的最长时间,避免因为等待时间过长而导致的线程阻塞问题。

因此,condition可以更加灵活地控制线程的等待和唤醒,并可以避免死锁和竞争产生的问题。

3.为什么finally释放?

 因为为了防止某些异常的发生导致锁没有即使释放导致死锁的情况。如果是在ReetrantLock中用,注意调用return时该线程是否持有锁,如果持有锁不主动释放会一直持有导致死锁的发生。

posted @   NobodyHero  阅读(51)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
点击右上角即可分享
微信分享提示