并发ReentrantLock中公平锁和非公平锁的理解

Posted 赵晓东-Nastu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发ReentrantLock中公平锁和非公平锁的理解相关的知识,希望对你有一定的参考价值。

【前言】

ReentrantLock 是在JavaSE5之后,并发包中新增了Lock接口用来实现锁功能,它提供了与synchronized关键字类似的同步功能。同时ReentrantLock也是重入锁,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,该锁还支持获取锁时的公平和非公平性选择。

【公平锁】

在这里插入图片描述

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

从代码中可以看出FairSync继承了Sync实现了同步器,也就是我们所说的AQS,如果我们调用lock方法,也就是我们调用了acquire的方法并且传入的参数为1

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

首先我们进入tryAcquire方法

        protected final boolean tryAcquire(int acquires) {
        // 获取当前的线程
            final Thread current = Thread.currentThread();
       //获取状态
            int c = getState();
       //判断状态是否为0 状态为0表示没有加锁
            if (c == 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;
        }
    }

进入这个线程我们首先获取当前线程,然后判断状态是否为加锁状态,如果是没有进行加锁的状态,则进入到hasQueuedPredecessors方法判断队列是否有值,如果没有值,则进行加锁,如果队列中有值,tryAcquire则返回false

  if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

!tryAcquire为true ,并且进入到了addWaiter方法中

// 将该线程new到一个节点里面
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //将tail赋值给pred
        Node pred = tail;
        //如果tail 不为null
        if (pred != null) {
        //那么就将新new出来结点的前指针指向pred
            node.prev = pred;
            //将新new的节点设置为尾节点
            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
            //直接new一个新的节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

进入到这个方法中是进入入队的操作

    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);
        }
    }

首先我们判断前一个节点是否为头结点,如果为头结点我们进入到了tryAcquire方法中继续进行获取锁,如果获取锁成功了,
那么设置结点为头结点。
如果该结点的上一个结点不是头结点,那么执行下面的代码

     if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())

shouldParkAfterFailedAcquire 方法主要逻辑是使用compareAndSetWaitStatus(pred,ws,node.SIGNAL)使用CAS将节点状态由 INITIAL 设置成 SIGNAL,表示当前线程阻塞。当 compareAndSetWaitStatus 设置失败则说明 shouldParkAfterFailedAcquire 方法返回 false,然后会在 acquireQueued 方法中死循环中会继续重试,直至compareAndSetWaitStatus 设置节点状态位为 SIGNAL 时 shouldParkAfterFailedAcquire 返回 true 时才会执行方法 parkAndCheckInterrupt 方法。

parkAndCheckInterrupt 该方法的关键是会调用 LookSupport.park 方法(关于LookSupport会在以后的文章进行讨论),该方法是用来阻塞当前线程。

如果获取锁失败的话,先将节点状态设置成SIGNAL,然后调用LookSupport.park方法使得当前线程阻塞。

【非公平锁】

在这里插入图片描述

      final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

非公平锁上来就进行获取锁

如果获取锁失败了,接下来走acquire方法

   public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            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;
            }
            return false;
        }

如果状态为0,则直接进行获取锁,其他的逻辑判断一样。

【公平锁和非公平锁的对比】

非公平锁会进行两次的强锁
非公平锁的效率要高
ReentrantLock默认为非公平锁

以上是关于并发ReentrantLock中公平锁和非公平锁的理解的主要内容,如果未能解决你的问题,请参考以下文章

java多线程20 : ReentrantLock中的方法 ,公平锁和非公平锁

Java并发多线程编程——锁

深入了解ReentrantLock中的公平锁和非公平锁的加锁机制

java中ReentrantLock实现,公平锁和非公平锁,AQS并发队列,

8.Java锁之公平锁和非公平锁

第237天学习打卡(知识点回顾 公平锁和非公平锁 可重入锁 自旋锁 读锁和写锁)