线程的基本方法-重入锁ReentrantLock(二)

 这一部分重点解析一下公平锁和非公平锁:

背景:默认情况下,ReentrantLock采用的是非公平锁,即不排队的方式。当一个线程释放锁之后,其他线程是随机获取这把锁。synchronized不支持公平的设定,而ReentrantLock提供方法设置线程的公平性。

1.创建

   
    public ReentrantLock() {
        sync = new NonfairSync();
    }


    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

  2.非公平锁

 static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

        CAS操作:Compare and Swap,比较并替换。有三个参数内存地址V,旧的预期值A,要修改的新值B。每次都是比较A是否和当前实际值相同,相同则替换,不同则替换失败。原子操作的底层用此操作实现。

       多个线程调用lock()方法的时候, 如果当前state为0, 说明当前没有线程占有锁, 那么只有一个线程会通过CAS操作获得锁, 并设置此线程为独占锁线程

       如果更新失败,那么其它线程会调用acquire方法来竞争锁,后续会全部加入同步队列中自旋或挂起。有一种情况会出现不公平的现象,当有其它线程A进来想要获取锁时,恰好此前的某一线程释放锁, 那么A会抢先获取锁。而同步队列中未被取消获取锁的线程是按顺序获取锁的,因为A还没有被插入到队列中。

       个人认为这样设计的目的是尽量不调整双向链表,因为不check的时候,要把当前线程加到队尾。同时,如果此时没有别的线程,还要把队首的线程再拿出来。这样调整算是一个优化的细节。

       acquire方法:

  public final void acquire(int arg) {
         //在这里会再尝试做一次acquire,也就是上文提到的抢占
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

  //Sync
 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()) {
              //判断获取锁的线程是否是当前线程
             // 因为ReentrantLock是可重入锁,可以累加重入的次数
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        acquire分为tryAcquire和acquireQueued两个步骤。

        tryAcquire调用sync的nonfairTryAcquire方法。在调用nonfairTryAcquire方法的时候,如果state为0,也就是刚好锁在lock()和acquire()中间被释放了(虽然几率很小,但也是普遍存在这种现象),再走一遍lock()中的流程;如果锁没被释放,判断获取锁的线程是否是当前线程,因为ReentrantLock是可重入锁,可以累加重入的次数,充当计数器的作用。

       addWaiter方法:

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

 

   等待队列是双向队列实现的,并且设置了head和tail节点。生成新的Node之后,如果尾指针不为null,即队列不为空,则CAS操作添加当前节点到尾结点。如果队列为空, 或CAS设置失败, 则调用enq强制入队。

   enq方法:

 private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { 
                //新生成head节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                //尝试把node的位置赋给tail
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

 看enq方法中,当队列为空的时候,初始化头节点并且头节点和尾节点指向同一node。当队列不为空时,又做了一遍CAS操作。(所以,head是没有存线程的,tail是存了线程的?)这里个人理解是,队列不为空的时候,enq强制了一次CAS操作,防止addWaiter中的CAS操作失败。因为多线程的情况,当一个线程去插入节点到队列的时候,可能正好别的线程刚好结束一次插入队列操作。所以这个死循环会一致持续到插入完成为止。

  看的出来,重入锁是一个基于CAS操作的同步控制。

 private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long stateOffset;
    private static final long headOffset;
    private static final long tailOffset;
    private static final long waitStatusOffset;
    private static final long nextOffset;

    static {
        try {
            stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));

        } catch (Exception ex) { throw new Error(ex); }
    }

 AbstractQueuedSynchronizer在类加载的时候,执行静态代码块,取出了关键字段的内存地址放到对应的offset中,方便CAS操作。

   acquireQueued方法:

  final boolean acquireQueued(final Node node, int arg) {
        //failed 标记acquire是否成功, interrupted标记当前线程是否需要中断
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //第三次tryAcquire,如果前置节点是head,并且尝试获取锁成功了
                //只有前置节点是head的时候,才能有资格去尝试获取锁。p == head代表了
                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);
        }
    }

   循环调用,获取前置结点。如果第一次循环就获取成功那么返回的interrupted是false,不需要自我中断。否则说明在获取到同步状态之前经历过挂起(返回true)那么就需要自我中断。

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //判断前置节点的等待状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            //前置结点是SIGNAL状态,代表当前置结点是执行完成可以唤醒后续结点
            return true;
        if (ws > 0) {
            //跳过取消的节点,找到第一个未取消的,作为node的前置节点。
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果是其他状态,强行设置前置结点为SIGNAL;前置节点可能会被其他线程操作,所以用CAS。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

  释放锁的过程:(公平锁的释放过程和非公平锁是一样的。因为是独占锁,只有当前拿锁的线程有释放锁的资格)

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

   release的具体过程,会先调用tryRelease方法,然后当头节点不为null的时候,唤醒头节点。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

      只有当前拿锁的线程有释放锁的资格,否则会抛出异常。在具体的代码中,lock和unlock次数不匹配,且unlock数大于lock次数的时候,会报这个错误。因为unlock次数等于lock次数的时候,当前线程已经释放锁了,此时exclusiveOwnerThread是null,就会出现值不相等的情况。

    Q:这边没有对新的state的值做范围判断?A:很依赖独占机制,只有当前的独占锁才可以释放。源码一句废话都不写,逻辑很完整。

 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

    unparkSuccessor方法唤醒node的后继有效节点。 为什么这边寻找节点的时候是从后往前找,我的理解是,插入队列的操作,是在enq中,是用尾插法,而且步骤是先搭尾节点的pre,再搭前置节点的next。所以当前查找如果是从前往后,有可能在某一个节点就找不到next了,但实际上后面是有尾节点的,只是刚搭了pre,还没来得及搭前置节点的next。这样从后往前查找,只要有节点存在,就会找到其pre节点,能优化“从前往后”的问题。

 private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        //当前node的状态小于0时,强制设置状态为0,表示后续节点将被唤醒
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        //从后往前找,找到head后面的有效节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

  假设有三个线程t1,t2,t3分别尝试获取锁,

 (1)一开始,t1获取锁,t2, t3在队列中:

   每次尾插一个节点,都会把前置节点的waitStatus的值设置为-1,代表当前节点可以被唤醒。

(2)t1执行完之后,释放锁,做完释放的一系列操作后,头节点状态为0,t2被唤醒:

(3)t2开始了自旋操作,获取锁,并且成为head节点:

  

3.公平锁

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

        final void lock() {
            acquire(1);
        }
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            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;
        }
    }

  (1)公平锁在lock的时候,没有尝试CAS操作,这样的话,新来的线程没有插队的机会,所有来的线程必须扔到队列尾部。

  (2)在tryAcquire方法中,拿同步锁的数量时,如果是0,或多一个hasQueuedPredecessors判断。

 public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

 分析一下结果为false的情况:

(1)如果是h==t ,说明此时队列为空

(2)当前线程是同步队列中的head结点的后继节点。

这也就意味着不满足这两个条件,就无法进行CAS抢占,只能走队列去获取锁。

公平锁的释放过程和非公平锁是一样的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值