zoukankan      html  css  js  c++  java
  • ReentrantLock的原理解析

    重入锁(ReentrantLock)是一种可重入无阻塞的同步机制。性能同synchronized接近(老版本jdk中性能很差)。

    下面重点看下常用的lock()和unlock()方法的实现原理。

    lock()

    首先看下源代码

        public void lock() {
            sync.lock();  // 有公平同步和非公平同步两种机制
        }

    它的实现很简单,调用了一行sync的lock()方法,由于sync有两种实现方式:公平同步和非公平同步,默认是非公平同步,继续看代码:

    final void lock() {
                if (compareAndSetState(0, 1))  // CAS机制,如果处于无锁状态,就直接锁定。lock锁中维护一个计数,大于0表示加锁了,值表示重入加锁次数
                    setExclusiveOwnerThread(Thread.currentThread());  // 设置锁被本线程持有,有什么用呢?用于可重入时检查使用
                else    
                    acquire(1);  // 如果锁引用计数不是0,说明已经上锁,检查是否可以重入
    }

     继续看"acquire(1)"的实现:

        
        public final void acquire(int arg) {
            if (!tryAcquire(arg) && // 判断是否可以重入,即持有锁的线程是否是当前线程,如果是就把锁引用计数加1,返回lock成功
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 否则尝试把自己挂起
                selfInterrupt();
        }
       // 再次尝试加锁
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    
        final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();  // 锁引用计数值
                if (c == 0) {  // 为0,说明没有锁了
                    if (compareAndSetState(0, acquires)) {  // 尝试加锁
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) { // 持有锁的正是当前线程,那么就把锁引用计数加1,加锁成功
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }

    上面看到tryAcquire中会再次尝试加锁,或者如果持有锁的是当前线程,则把锁引用计数加1,返回加锁成功。

    否则执行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;  // 增加一个等待node带队尾
            if (pred != null) {
                node.prev = pred;
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
           // 前面使用CAS方式设置失败,则进入enq,循环使用CAS方式把Node加入队尾
            enq(node);
            return node;
        }
      // 把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;
                    }
                }
            }
        }
    // 加入队尾后,再执行acquireQueued(即
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    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);
            }
        }
    
    

     上面的代码,重点看下这两个方法:

    shouldParkAfterFailedAcquire
     1 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
     2         int ws = pred.waitStatus;  // 第一次进来 ws是0,是初始值
     3         if (ws == Node.SIGNAL)   // 是否已经设置为等待唤醒状态,如果是,就可以挂起了
     4             /*
     5              * This node has already set status asking a release
     6              * to signal it, so it can safely park.
     7              */
     8             return true;
     9         if (ws > 0) {
    10             /*
    11              * Predecessor was cancelled. Skip over predecessors and
    12              * indicate retry.
    13              */
    14             do {
    15                 node.prev = pred = pred.prev;
    16             } while (pred.waitStatus > 0);
    17             pred.next = node;
    18         } else { // 设置自己的状态为Node.SIGNAL,但本次不允许挂起。下次再进来的时候,就可以返回true,并可以挂起了
    19             /*
    20              * waitStatus must be 0 or PROPAGATE.  Indicate that we
    21              * need a signal, but don't park yet.  Caller will need to
    22              * retry to make sure it cannot acquire before parking.
    23              */
    24             compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    25         }
    26         return false;
    27     }
    parkAndCheckInterrupt

    1     private final boolean parkAndCheckInterrupt() {
    2         LockSupport.park(this);  // 挂起自己
    3         return Thread.interrupted();
    4     }
    再看下park的实现:
    1     public static void park(Object blocker) {
    2         Thread t = Thread.currentThread();
    3         setBlocker(t, blocker);  // 设置要挂起的线程,和挂起使用的对象
    4         unsafe.park(false, 0L); // 挂起,jdk内置的挂起方法
    5         setBlocker(t, null);      // 唤醒后,取消挂起的对象
    6     }

    再看下setBlocker:

    1     private static void setBlocker(Thread t, Object arg) {
    2         // Even though volatile, hotspot doesn't need a write barrier here.
    3         unsafe.putObject(t, parkBlockerOffset, arg);
    4     }  // 设置了阻塞线程和使用的阻塞对象
     
     

     从上面代码可以看到,lock的主要流程有:

    1. 检查锁引用计数,如果为0,表示可以锁定,就使用CAS方式把当前Thread对象设置为持有锁的对象,并把锁引用计数加1.

    2. 检查锁引用计数,如果大于0,需要:

        i. 检查当前持有锁的线程对象是否和本线程是同一个,如果是就对锁引用计数加1,加锁成功,这里体现了"可重入"特性。

        ii. 否则,创建一个等待的node对象并加入到等待链接的队尾,然后调用系统的unsafe.park方法,把自己挂起。

     Unlock

    unlock就比较简单了,下面看下代码:

        public void unlock() {
            sync.release(1); // 解锁
        }
    
    public final boolean release(int arg) {
            if (tryRelease(arg)) {  // 解锁
                Node h = head;
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h); // 解锁成功后,唤醒其他的等待线程
                return true;
            }
            return false;
        }

    这里面重点有两个地方,一个解锁,另外一个唤醒其他等待的线程。

    先看解锁:

     1 // 索引用计数减releases,实现了重入
     2 protected final boolean tryRelease(int releases) {
     3             int c = getState() - releases;
     4             if (Thread.currentThread() != getExclusiveOwnerThread())
     5                 throw new IllegalMonitorStateException();
     6             boolean free = false;
     7             if (c == 0) {  // 如果锁引用计数为0,说明是无锁状态了,需要把持有锁的线程变量置为空,并返回true
     8                 free = true;
     9                 setExclusiveOwnerThread(null);
    10             }
    11             setState(c);
    12             return free;
    13         }

    再看唤醒其他线程代码:

     1 // 当前面方法返回true,说明当前处于无锁状态了,这时候就可以唤醒其他的等待线程了
     2 private void unparkSuccessor(Node node) {
     3         /*
     4          * If status is negative (i.e., possibly needing signal) try
     5          * to clear in anticipation of signalling.  It is OK if this
     6          * fails or if status is changed by waiting thread.
     7          */
     8         int ws = node.waitStatus;
     9         if (ws < 0)
    10             compareAndSetWaitStatus(node, ws, 0);
    11 
    12         /*
    13          * Thread to unpark is held in successor, which is normally
    14          * just the next node.  But if cancelled or apparently null,
    15          * traverse backwards from tail to find the actual
    16          * non-cancelled successor.
    17          */
    18         Node s = node.next;
    19         if (s == null || s.waitStatus > 0) {
    20             s = null;
    21             for (Node t = tail; t != null && t != node; t = t.prev)
    22                 if (t.waitStatus <= 0)  // 找到等待的线程
    23                     s = t;
    24         }
    25         if (s != null)
    26             LockSupport.unpark(s.thread);  // 唤醒线程
    27     }

    相对来说,unlock就简单许多了,两步:1.解锁,减引用计数。2.唤醒其他线程。

  • 相关阅读:
    php 类 成员变量 $this->name='abc'
    php类的实现
    php 生成类的对象 $a=new test();
    php 对象的执行
    php 对象调用方法
    php 连接字符串. ZEND_ASSIGN_CONCAT/ZEND_CONCAT原理
    function 的声明
    vim用法
    ubuntn系统下将文件拷贝到优盘中及挂载概念理解
    windows远程连接linux-安装xfce界面,ubuntn添加新用户
  • 原文地址:https://www.cnblogs.com/xinghebuluo/p/8535958.html
Copyright © 2011-2022 走看看