Java中的锁(四)

基于前面讲解的CAS内容,本章主要内容是Lock以及Lock的常见实现加锁原理和方式。

一、Lock接口

Java1.5以后官方在concurrent包下引入了Lock接口和其对应实现。Lock接口是显示的锁,加锁和解锁都需要手动实现,接口内容有:

public interface Lock {

    void lock();

    //可中断获取锁,在获取锁的过程中可中断,synchronized在获取锁时是不可中断的
    void lockInterruptibly() throws InterruptedException;

    //尝试非阻塞获取锁,调用该方法后立即返回结果,如果能够获取则返回true,否则返回false
    boolean tryLock();

   //根据时间段获取锁,在指定时间内没有获取锁则返回false,如果在指定时间内当前线程未被中并断 
 获取到锁则返回true
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    // 与当前锁绑定,当前线程只有获得了锁才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
    Condition newCondition();

    void unLock();
}

二、可重入锁ReetrantLock

ReetrantLock本身也是一种支持重进入的锁,即该锁可以支持一个线程对资源重复加锁,同时也支持公平锁与非公平锁。ReeTrantLock底层是通过AbstractQueuedSynchronize队列同步器实现的,提供了如下一些方法:

//查询此锁是否由任意线程保持。        
boolean isLocked()     

//如果此锁的公平设置为 true,则返回 true。     
boolean isFair()  

//查询当前线程是否保持此锁。      
boolean isHeldByCurrentThread() 

//查询当前线程保持此锁的次数。
int getHoldCount() 

//返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。      
protected Thread getOwner(); 

//返回一个 collection,它包含可能正等待获取此锁的线程,其内部维持一个队列,这点稍后会分析。     
protected Collection<Thread> getQueuedThreads(); 

//返回正等待获取此锁的线程估计数。   
int getQueueLength();

// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition); 

//返回等待与此锁相关的给定条件的线程估计数。       
int getWaitQueueLength(Condition condition);

// 查询给定线程是否正在等待获取此锁。     
boolean hasQueuedThread(Thread thread); 

//查询是否有些线程正在等待获取此锁。     
boolean hasQueuedThreads();

//查询是否有些线程正在等待与此锁有关的给定条件。     
boolean hasWaiters(Condition condition); 

三、AQS

3.1 AQS的模型

AQS是AbstractQueuedSynchronize简写,核心思想是通过volatile int state这样的volatile变量,配合Unsafe类的原子操作来实现对当前锁状态的修改。当state=0时,说明没有线程占用共享资源;state=1时,说明有线程正在使用共享资源,其他的线程需要进入同步队列等待。AQS内部通过内部类Node构成FIFO的双向同步队列来完成线程获取锁的排队工作,利用ConditionObject构建等待队列,当Condition调用wait()方法时,线程会加入等待队列;当Condition调用signal()方法后,线程会从等待队列加入同步队列竞争锁。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;
...
}

 AQS的模型如图:

其中head是头结点,是一个空节点,不存储数据;tail是尾节点。state是同步状态,state的值为0,说明当前线程可以获取到锁,同时将state设置为1,表示获取成功。如果state已为1,也就是当前锁已被其他线程持有,那么当前执行线程将被封装为Node结点加入同步队列等待。

3.2 Node节点说明

static final class Node {
 static final Node SHARED = new Node();
 static final Node EXCLUSIVE = null;
 // 表示线程已结束
 static final int CANCELLED =  1;
 // 等待被唤醒状态
 static final int SIGNAL    = -1;
 // 条件状态
 static final int CONDITION = -2;
 // 在共享模式中,表示获得的同步状态会被传播
 static final int PROPAGATE = -3;

 // node本身的等待状态, 1表示
 volatile int waitStatus;
 // 前一个节点
 volatile Node prev;
 // 后一个节点
 volatile Node next;

 // 对应的等待线程
 volatile Thread thread;
 // 下一个等待着者
 Node nextWaiter;
}

共享模式和独享模式:

共享模式是一个锁允许多条线程同时操作,如信号量Semaphore采用的就是基于AQS的共享模式实现的,而独占模式则是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如ReentranLock。

Node中waitStatus有四种状态:

  • CANCELLED:1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点。
  • SIGNAL:-1,处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
  • CONDITION:-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

四、ReeTrantLock的底层实现

4.1 ReeTrantLock和AQS的关系图:

ReentrantLock内部存在3个实现类,分别是Sync、NonfairSync、FairSync,其中Sync继承自AQS实现了解锁tryRelease()方法,而NonfairSync(非公平锁)、 FairSync(公平锁)则继承自Sync,实现了获取锁的tryAcquire()方法,ReentrantLock的所有方法调用都通过间接调用AQS和Sync类及其子类来完成的。

4.2 ReentrantLock的非公平锁实现

//默认构造,创建非公平锁NonfairSync
public ReentrantLock() {
    sync = new NonfairSync();
}
//根据传入参数创建锁类型
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

//加锁操作
public void lock() {
     sync.lock();
}
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    final void lock() {
        if (compareAndSetState(0, 1))  // 通过CAS方式,获取锁,设置状态为1
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);  // 再次请求同步锁
    }
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

acquire(int arg)是AQS的方法,用于再次请求同步锁。它对中断不敏感,即使线程获取同步状态失败,进入同步队列,后续对该线程执行中断操作也不会从同步队列中移出,方法如下

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
static void selfInterrupt() {
    Thread.currentThread().interrupt();
}
//Sync类
abstract static class Sync extends AbstractQueuedSynchronizer {
   //nonfairTryAcquire方法
   final boolean nonfairTryAcquire(int acquires) {
      final Thread current = Thread.currentThread();
      int c = getState();
      //判断同步状态是否为0,并尝试再次获取同步状态
      if (c == 0) {
         //执行CAS操作
         if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
         }
      }
      //如果当前线程已获取锁,属于重入锁,再次获取锁后将status值加1
      else if (current == getExclusiveOwnerThread()) {
         int nextc = c + acquires;
         if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
         //设置当前同步状态,当前只有一个线程持有锁,因为不会发生线程安全问题,可以直接执行 setState(nextc);
         setState(nextc);
         return true;
      }
      return false;
   }
   //省略其他代码

当tryAcquire(arg)返回false,则会执行addWaiter(Node.EXCLUSIVE)进行入队操作。

private Node addWaiter(Node mode) {
   //将请求同步状态失败的线程封装成结点
   Node node = new Node(Thread.currentThread(), mode);

   Node pred = tail;
   //如果是第一个结点加入肯定为空,跳过。
   //如果非第一个结点则直接执行CAS入队操作,尝试在尾部快速添加
   if (pred != null) {
      node.prev = pred;
      //使用CAS执行尾部结点替换,尝试在尾部快速添加
      if (compareAndSetTail(pred, node)) {
         pred.next = node;
         return node;
      }
   }
   //如果第一次加入或者CAS操作没有成功执行enq入队操作
   enq(node);
   return node;
}

如果第一次CAS没有成功或者头结点为空,会用enq(node)方式,不停的用CAS加入尾节点。

private Node enq(final Node node) {
   //死循环
   for (;;) {
      Node t = tail;
      //如果队列为null,即没有头结点
      if (t == null) { // Must initialize
         //创建并使用CAS设置头结点
         if (compareAndSetHead(new Node()))
            tail = head;
      } else {//队尾添加新结点
         node.prev = t;
         if (compareAndSetTail(t, node)) {
            t.next = node;
            return t;
         }
      }
   }
}

enq方法非常重要的一点,使用CAS原子操作进行头结点设置和尾结点tail替换可以保证线程安全,可以多线程同时竞争执行。如果有一个线程修改head和tail成功,其它线程将继续循环,直到修改成功。head结点本身不存在任何数据,它只是作为一个牵头结点,而tail永远指向尾部结点(前提是队列不为null)。

添加到同步队列后,节点会进入自旋,是在acquireQueued方法中实现。当且仅当前驱结点为头结点才尝试获取同步状态,这符合FIFO的规则,即先进先出,其次head是当前获取同步状态的线程结点,具体有:

final boolean acquireQueued(final Node node, int arg){
   boolean failed = true;
   try{
      boolean interrupted = false;
      // 不停的循环自旋,等待前驱节点是head
      for(;;){
         final Node p = node.predecessor();
         // 当前节点的前驱节点时head的时候,才开始尝试获取同步状态
         if(p == head && tryAcquire(arg)){
             // 获取到同步状态后,设置为head
             setHead(node);
             p.next = null;
             failed = false;
             return interrupted;
         }

         // 前驱节点不是head,判断是否需要挂起
         if(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){
             interrupted = true;
         }
      }
    
   }finally{
      if(failed){
          //最终都没能获取同步状态,结束该线程的请求
          cancelAcquire(node);
      }
   }

}

如果前驱节点不是head,线程是否挂起的方法有:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node){
    //获取当前结点的等待状态
    int ws = pred.waitStatus;
    if(ws == Node.SIGNAL){
        // signal 表示前置节点处于等待被唤醒的状态,所以返回true
        return true;
    }

    if(ws >0){
        // 如果waitStatus > 0 表示前置节点时结束,则遍历之前未结束的节点
        do{
            pred = pred.prev;
            node.prev = pred;
        }while(pred.waitStatus > 0);
        pred.next = node;
    }else{
        // 如果不是等待被唤醒的状态,则将其设置为SIGNAL状态,代表该结点的线程正在等待唤醒
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;

} 

private final boolean parkAndCheckInterrupt() {
    //将当前线程挂起
    LockSupport.park(this);
    //获取线程中断状态,interrupted()是判断当前中断状态,
    //并非中断线程,因此可能true也可能false,并返回
    return Thread.interrupted();
}

若shouldParkAfterFailedAcquire()方法返回true,即前驱结点为SIGNAL状态同时又不是head结点,那么使用parkAndCheckInterrupt()方法挂起当前线程,称为WAITING状态,需要等待一个unpark()操作来唤醒它,到此ReetrantLock内部间接通过AQS的FIFO的同步队列就完成了lock()操作。

对于lockInterruptibly()或者tryLock()方法,最终它们都间接调用到doAcquireInterruptibly(),其实现和acquireQueued唯一的区别是:判断线程需要被挂起时,直接抛出中断异常。

if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
    //直接抛异常,中断线程的同步状态请求
    throw new InterruptedException();

unLock操作有:

public void unlock(){
    sync.release(1);
}

//AQS类的release()方法
public final boolean release(int arg){
    if(tryRelease(arg)){
        Node h = head;
        if(h != null && h.waitStatus != 0){
            unparkSuccessor(h);
        }
        return true;
    }
    return flase;
}

//ReentrantLock类中的内部类Sync实现的tryRelease(int releases) 
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;

    if (Thread.currentThread() != getExclusiveOwnerThread())
          throw new IllegalMonitorStateException();

      boolean free = false;
      //判断状态是否为0,如果是则说明已释放同步状态
      if (c == 0) {
          free = true;
          //设置Owner为null
          setExclusiveOwnerThread(null);
      }
      //设置更新同步状态
      setState(c);
      return free;
}
private void unparkSuccessor(Node node){
    int ws = node.waitStatus;
    if (ws < 0){
        //置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);
    }

    // 找到下一个节点
    Node s = node.next;
    if(s == null || s.waitStatus > 0){
        s = null;
        // 从尾结点开始倒叙查找最前的一个有效节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }

     if (s != null)
        LockSupport.unpark(s.thread);//唤醒
        
}

加锁的过程表述为:如果前置节点不是head,并且前置节点时等待被唤醒的SIGNAL状态,当前节点的线程就进入park。解锁的过程则是:首先释放当前节点的锁,并对head的后一个有效节点做线程唤醒park操作。

4.3 ReetrantLock的公平锁实现:

非公平锁获取的方法是nonfairTryAcquire(int acquires),公平锁实现的方法是tryAcquire(int acquires).

protected final boolean tryAcquire(int acquires){
    final Thread current = Thread.currentThread();
    int c = getState();
    if(c == 0){
        // 这里和非公平锁不同,非公平锁不会做是否有前置的节点存在
        if(!hasQueuedPredcessor() && compareAndSetState(0, acquires)){
            setExclusiveOwnerThread(current);
            retur true;
        }
    }
    else if(current == getExclusiveOwnerThread()){
        int nextc =  acquires+c;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }

    return false;
}

公平锁借助hasQueuedPredecessors()判断同步队列是否存在结点,如果存在必须先执行完同步队列中结点的线程,当前线程进入等待状态。公平锁在线程请求到来时先会判断同步队列是否存在结点,如果存在先执行同步队列中的结点线程,当前线程将封装成node加入同步队列等待。非公平是当线程请求到来时,不管同步队列是否存在线程结点,直接尝试获取同步状态,获取成功直接访问共享资源。

参考来自:https://blog.csdn.net/javazejian/article/details/75043422

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值