ReentrantLock解读

什么是管程? 

管程:指的是管理共享变量以及对共享变量操作得过程,让他们支持并发。

在管程得发展史上,先后出现过三种不同得管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在广泛再用得就是MESA模型

java得管程模型就是基于MESA改进得,AQS也是基于这套思想设计得。

了解ReentrantLock必须先熟悉AQS,AQS提供了一些模板方法供具体的子类实现,从而根据AQS实现公平锁,非公平锁等。

首先要明白几个概念:

 1.临界区:临界区是指进程中访问临界资源的一段需要互斥执行的代码。进入临界区之前需要判断能否进入,进入时需要改变标志阻止其他进程进入,进入的进程执行完成后退出时修改标志;

2.公平锁:先到达临界区的线程先获取到锁,后到达临界区的线程后获取到锁;

3.非公平锁:先到达临界区的线程未必先获取到锁。

4.可重入锁:当前获取到锁的线程,可以再次获取锁。

接下来我们就看一下ReentrantLock是如何实现这些功能的: 

 ReentrantLock对象中维护着一个至关重要的变量Sync,Sync是一个抽象类,有两个具体实现

 FairSync(公平锁),NonfairSync(非公平锁)

   看一下Sync 的类关系图

  

 ReentrantLock是通过变量state设置锁的状态,若为0即为五哦所状态,非0就是加锁状态

非公平于公平锁的不同之处在于:非公平锁在进入临界区时就会尝试去加锁,不管队列中是否存在排队的线程;公平锁则会先判断队列中是否存在排队的线程,不存在才会去尝试加锁。

//非公平锁实现方式
 final void lock() {
     if (compareAndSetState(0, 1))
         setExclusiveOwnerThread(Thread.currentThread());
          else
        acquire(1);
   }
//公平锁实现方式
    protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //若state==0且队列中无排队线程
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

在加锁成功后,会将变量exclusiveOwnerThread设置为当前线程,如果加锁失败,会判断exclusiveOwnerThread是否为当前线程,若为当前线程,则会将state自增加1,加锁成功。这便是可重入锁的实现方式。

         if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //可重入锁实现方式
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
          }

如果可重入尝试也加锁失败,则会尝试将当前线程加入队列中acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

这段逻辑中有两个for循环

第一个for循环在addWaiter(Node.EXCLUSIVE)方法中,EXCLUSIVE表时独占

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }


  private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

首先会将当前线程封装成Node对象,通过cas将node放入尾节点,若加入失败则进入enq(node)逻辑;这个for循环的作用就是如果队列不存在则创建一个队列,然后入队,如果存在则通过cas再次入队,for循环保证node肯定能加入到等待队列中。这就是第一个for循环的用处。

我们再来看第二个for循环

  final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

此处会判断当前节点的前一个节点是否为头节点,若是则会再次尝试加锁。加锁成功后会执行

setHead(node)方法:        

 private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

这里将头节点设置为当前节点,且把当前节点得thread设置为null,这与前面初始化同步队列时会设置一个空得头部节点compareAndSetHead(new Node())相互对应,表明有一个线程已经获取到锁了,其余线程需排在这个空节点后面

若加锁失败则进入shouldParkAfterFailedAcquire(p,node)逻辑

/**
 *pred 前一个节点
 *node 当前节点
 **/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //SIGNAL表示待唤醒状态
        if (ws == Node.SIGNAL) 
            return true;
        if (ws > 0) { 
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else { 
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

  如果前一个节点的waitStatus不是-1,则通过cas将前一个节点的状态设置成-1,waitStatus默认为0。如果返回为false,由于for循环会再次进入这里,最终这里返回为true。最终会阻塞当前线程

  private final boolean parkAndCheckInterrupt() {
        //阻塞当前线程
        LockSupport.park(this);
        return Thread.interrupted();
    }

所以第二个for循环就是保证当前线程若未获取到锁则一定会进入阻塞状态。

以上就是加锁过程,获取不到锁最终阻塞。

解锁逻辑就很简单,就是将state自减1,若减至0,则将变量exclusiveOwnerThread设置为null,

同时唤醒当前节点的下一个节点,让其去竞争锁。

补充:公平锁效率高还是非公平锁效率高?

      非公平锁效率是高于公平锁的,因为非公平锁会有机会减少线程上下文切换的次数,如果当前活跃的线程有机会获取到锁,则直接获取,不用让其阻塞,减少线程上下文切换此时;公平锁则只要队列中有排队线程则必须去入队,阻塞,可能会造成一些无意义的线程上下文切换。非公平锁带来的影响就是有可能造成活锁,就是在排队的线程可能很久都获取不到锁,但这种概率很低。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值