内衣网站建设详细方案,接广告推广,wordpress支付宝支付宝,湖北 商城网站建设CLH自旋锁
JUC中显式锁基于AQS抽象队列同步器#xff0c;而AQS是CLH锁的一个变种。队列头结点可以获得锁#xff0c;其他节点排队等候。 在争夺锁激烈的情况下#xff0c;为了减少CAS空自旋#xff08;CAS需要CPU进行内部通信保证缓存一致性造成流量过大引起总线风暴…CLH自旋锁
JUC中显式锁基于AQS抽象队列同步器而AQS是CLH锁的一个变种。队列头结点可以获得锁其他节点排队等候。 在争夺锁激烈的情况下为了减少CAS空自旋CAS需要CPU进行内部通信保证缓存一致性造成流量过大引起总线风暴Java轻量级锁会升级为重量级锁那么JUC基于CAS实现的轻量级锁如何避免总线风暴呢答案是使用队列对抢锁线性排队最大程度上减少CAS操作数量。
CLH锁其实就是一种是基于队列具体为单向链表排队的自旋锁申请加锁的线程首先会通过CAS操作在单向链表的尾部增加一个节点之后该线程只需要在其前驱节点上进行普通自旋 等待前驱节点释放锁即可。由于CLH锁只有在节点入队时进行一下CAS的操作在节点在加入队列之后抢锁线程不需要进行CAS自旋只需普通自旋即可。因此在争用激烈的场景下 CLH锁能大大减少的CAS操作的数量以避免CPU的总线风暴。
面试回答抢锁线程在队列尾部加入一个节点然后仅在前驱节点上做普通自旋它不断轮询前一个节点状态如果发现前一个节点释放锁当前节点抢锁成功。
CLH 加锁过程
首先明确CLH是指向前节点的单链表每个节点Node包括至少三个参数前向指针、locked 状态变量、线程引用。 当一个线程加入抢锁队列时创建新Node然后通过CAS加入到CLH队列的尾部前向指针指向前一个节点尾指针指向新加入的节点。然后这个节点的线程会对前向的Node进行普通自旋循环判断前驱节点的locked属性是否为false如果为false就表示前驱节点释放了锁 当前线程抢锁成功。此线程抢到锁后locked一直为true直到释放锁为false。 注意以上普通自旋与CAS的区别是 while 循环中是否有 Thread.yield(); 让出CPU时间片CAS是没有yield的。另外还有一个尾指针tail属性使用AtomicReference类型是为了使得多个线程并发操作tail时不会发生线程安全问题。 locked 为 true 表示此线程自旋等待中或者正在执行临界区代码下一个节点需要等待直到释放了锁 locked 为false。
//CAS自旋将当前节点插入到队列的尾部
while (!tail.compareAndSet(preNode, curNode)){preNode tail.get();
}// 普通自旋监听前驱节点的locked变量直到其值为false
// 若前继节点的locked状态为true则表示前一个线程还在抢占或者占有锁
while (curNode.getPrevNode().isLocked()){//让出CPU时间片提高性能Thread.yield();
}CLH 释放锁的过程
当一个线程执行完临界区代码后释放锁首先将前向指针指向null然后将locked设置为false。此时前面的节点没有引用将会被GC。此时它后面的节点捕获了前面的locked为false立即抢占锁执行临界区代码。
AQS抽象同步器核心原理
为什么需要AQS
CAS 恶性空循环浪费大量CPU资源SMP架构CPU会导致总线风暴
在独占锁中竞争资源在一个时间点只能被一个线程锁访问队列的队首节点队列的头部表示占有锁的节点新加入的抢锁线程则需要等待会插入到队列的尾部。AQS是JUC提供的一个用于构建锁和同步容器的基础类。 AQS是CLH队列的一个变种主要原理和CLH队列差不多。AQS队列内部维护的是一个FIFO的双向链表这种结构的特点是每个数据结构都有两个指针分别指向直接的前驱节点和直接的后驱节点。每个节点其实是由线程封装的当线程争抢锁失败后会封装成Node加入到AQS队列中去当获取锁的线程释放锁以后会从队列中唤醒一个阻塞的节点。 AQS核心成员
状态标志位 state
volatile 修饰的 int state 任何线程都可以回去state的最新值通过getState() 和 setState() 设置同步状态值。一般通过CAS设置state值。调用的是Unsafe的compareAndSwapInt()方法实现CAS。以ReentrantLock为例初始时 state 0表示未锁定状态。当一个线程调用tryAcquire() 独占该锁并将state1此后其他线程再tryAcquire()时就会失败直到A线程unlock()到state0即释放锁为止其他线程才有机会获取该锁。当然释放锁之前 A线程自己是可以重复获取此锁的state会累加这就是可重入的概念。但要注意获取多少次就要释放多么次这样才能保证state是能回到零态。
Node节点状态 waitStatus
在Node节点中定义了waitStatus来表示节点的状态
cancelled取消的一般中断的或者超时的线程是这个状态需要从等待队列中删除节点此状态不参与竞争。signal表示后驱节点处于等待状态一旦此节点释放锁或者被取消了通知后驱节点运行。condition表示该节点处在条件队列中阻塞表示此节点处于等待队列中当调用了condition的singal方法才会从等待队列转移到同步队列中去竞争锁。propagate共享锁的延续当自己的状态是这个就通知后续节点也共享状态获取锁。为0表示当前节点处于初始状态。
**注意**有人不知道等待队列和同步队列等待队列就是阻塞了的线程例如调用wait()方法就进入到了等待队列。只有在同步队列中的线程才有资格竞争锁处于等待队列中的线程如果被 notify 或者 signal 唤醒进入到同步队列并不是被唤醒了就立马能获得锁运行同步队列简单讲就是已经具备运行条件只差一把锁的资源抢到了立马就运行。而等待队列中的线程没有抢锁资格。
注意以上两个都是状态但是state是全局唯一的标识的是AQS队列的锁状态而waitStatus是每个节点都有的内部属性。
thread成员
Node 的thread用来存放线程的引用next指针指向后续节点。
抢占式常量标识
shared表示线程因为获取共享资源时阻塞而添加到队列中。exclusive表示线程因为获取独占资源阻塞而被添加到队列中。
双向同步队列
AQS的内部队列是CLH队列的变种每当线程通过AQS获取锁失败时线程将被封装成一个Node节点通过CAS原子操作插入队列尾部。当有线程释放锁时 AQS会尝试让队首的后驱节点占用锁。 AQS是一个同步器实现了所得基本功能JUC的的显式锁如ReentrantLock、 ReentrantReadWriteLock线程同步工具如Semaphore内部都使用了AQS作为等待队列。
AQS抢占锁的原理
新线程来了使用AQS的Acquire尝试获取锁如果失败了则构造独占式节点Node通过CAS方式自旋地将节点加入到同步队列队尾。构造Node时需要设置 thread 为当前线程。当节点进入到了队列后本来应该自旋地判断前置节点是否为头节点并尝试获取锁。但是为了不浪费资源如果前置节点不是头节点则自旋过程中会阻塞线程。而只有它的前驱节点成为头节点后才会唤醒后置线程尝试获取锁。自旋是节点中的线程自己完成的。AQS不像CLH节点那样做空旋转浪费资源而是会被挂起park进入阻塞状态。如果头节点获取了锁那么此线程会停止自旋而去执行临界区的代码。
其实判断前置节点还需要判断节点的状态例如只有前驱节点状态为signal则它的后续节点进行自我阻塞。一开始一个线程加入到队尾waitStatu肯定是0表示初始状态此时会进行CAS自旋获取锁。在自旋中检测是否挂起找到自己的有效前驱指的是不是取消的节点一般为初始状态0或者共享状态然后将其设置为signal之后自己马上进入阻塞。可以理解为当前面的节点成为头节点后记得唤醒我。 如果前面的节点为取消状态的节点就继续往前找并建立唤醒关系。只有是signal状态才会唤醒后续节点。 释放锁的过程当头节点释放锁后就唤醒后面的节点后面的节点成为新的头结点。如果遇见某个节点是无效节点则直接删除也就是说无效节点的出队操作是在唤醒后驱节点的线程之后。
ReentrantLock 如何借助AQS实现的
ReentrantLock把所有Lock接口的操作都委派到一个Sync类上该类继承了AQS。NonfairSync为非公平或者不公平同步器 FairSync为公平同步器。这两个同步器都继承Sync然后Sync继承AQS。ReentrantLock的lock()和unlock()调用的其实是Sync的lock()和release()方法。ReentrantLock的显式锁操作是委托或委派给一个Sync内部类的实例完成的。而Sync内部类只是AQS的一个子类所以本质上ReentrantLock的显式锁操作是委托或委派给AQS完成的。
ReentrantLock AQS 抢锁原理
公平锁与非公平锁区别
加锁区别**加锁时是否判断前面有节点在排队。**公平锁是先判断是否有节点在排队如果有则加入到队列后面CAS方式。非公平的是新节点来了直接去抢锁两次抢锁失败了才加入到队列中并且阻塞。解锁区别如果新来了一个线程Thread-4此时直接抢占锁如下图1所示如果没有新来的线程抢占则按照队列的顺序公平地唤醒头结点后面的节点并持有锁。如下图图2所示。 非公平式抢锁 (同步器是NonfairSync)
首先用一个CAS操作判断state是否是0表示当前锁未被占用如果是0就调用AQS的acquire方法以CAS方式把它置为1并且设置当前线程为该锁的独占线程表示获取锁成功。当多个线程同时尝试占用同一个锁时 CAS操作只能保证一个线程操作成功剩下的只能乖乖去排队。如果发现当前线程和独占锁的线程是同一个就重入state如果两次抢锁都失败了那么就老实入队
ReentrantLock“非公平”性即体现在这里如果占用锁的线程刚释放锁 state置为0而排队等待锁的线程还未唤醒新来的线程就直接抢占了该锁那么就“插队”了。
非公平同步器ReentrantLock.NonfairSync的核心思想就是当前进程尝试获取锁的时候如果发现锁的状态位是0就直接尝试将锁拿过来然后执行setExclusiveOwnerThread()根本不管同步队列中的排队节点。
公平式抢锁 (同步器是FairSync)
首先获取锁的状态如果state为0则判断头节点是否有后驱节点如果有后驱节点就立即去排队。否则CAS将state设置为1进行抢占抢占成功后将当前线程设置为锁占有线程。
AQS条件队列
Condition与Object的wait()/notify()作用是相似的都是使得一个线程等待某个条件 Condition只有当该条件具备signal()或者signalAll()方法被调用时等待线程才会被唤醒从而重新争夺锁。不同的是 Object的wait()/notify()由JVM底层实现而Condition接口与实现类完全使用Java代码实现。通过Condition的await()和signal()方法进行线程间的阻塞与唤醒。
一个Condition对象是一个单条件的等待队列 在一个显式锁上我们可以创建多个等待任务队列这点和内置锁不同 Java内置锁上只有唯一的一个等待队列。
await()等待方法原理
当线程调用await()方法时说明当前线程的节点为当前AQS队列的队首节点正好处于占有锁的状态 await()方法需要把该线程从AQS队列挪到Condition等待队列里。然后执行while循环将该节点的线程阻塞直到该节点离开等待队列重新回到同步队列成为同步节点后线程才退出while循环。 signal()唤醒方法原理
上调用signal()方法后等待队列中的firstWaiter会被加入到同步队列中等待节点被唤醒。 提一嘴
AQS 是一个同步容器与队列锁的模板类用户可以通过AQS模板类自定义自己的锁和同步队列。只需要实现核心的获取锁、释放锁的排队和出队过程。分为模板方法和构造方法钩子方法用户的子类自己实现。