• 欢迎访问搞代码网站,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站!
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏搞代码吧

关于SegmentFault:啃碎JDK源码八ReentrantLock

java 搞代码 4年前 (2022-02-19) 28次浏览 已收录 0个评论
文章目录[隐藏]

前言

上一次咱们曾经讲了AQS,如果对其不相熟的话倡议先去看看其实现原理,看完再来看ReentrantLock就很简略了。

啃碎JDK源码(一):String
啃碎JDK源码(二):Integer
啃碎JDK源码(三):ArrayList
啃碎JDK源码(四):HashMap
啃碎JDK源码(五):ConcurrentHashMap
啃碎JDK源码(六):LinkedList
啃碎JDK源码(七):AbstractQueuedSynchronizer(AQS)

ReentrantLockSynchornized 在面试中常常被用来比拟,如果想理解Synchronized的话能够看我另外一篇文章:死磕Synchronized

注释

先来理解一下一些外围属性:

public class ReentrantLock implements Lock, java.io.Serializable {
// 实现AQS的外部类
private final Sync sync;
  ......
}

没错,ReentrantLock没有什么值得注意的属性,因为曾经在AQS中定义好了,咱们只须要继承它而后进行简略的实现即可。

先看下 ReentrantLock 的用法:

  public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            // 执行业务
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

只有调用 lock 办法就能够进行加锁操作,示意接下来的这段代码曾经被以后线程锁住,其余线程须要执行时须要拿到这个锁能力执行,而以后线程在执行完之后要显式的调用 unlock 开释锁。

留神:看源码之前你必须要对AQS比拟相熟才行,能够参考我上一篇博客:
啃碎JDK源码(七):AbstractQueuedSynchronizer(AQS)

咱们来跟进源码看一下,先来看咱们的加锁lock办法:

  public void lock() {
      sync.lock();
  }
  
  // Sync继承了AQS
  abstract static class Sync extends AbstractQueuedSynchronizer {
  
     abstract void lock();
     ......
  }
  

能够看到是调用外部类的lock办法,而它是一个形象办法,咱们看下谁继承了这个形象接口:

FairSyncNonfairSyncReentrantLock 的另外两个外部类。顾名思义一个是偏心锁,一个是非偏心锁。(偏心锁就是永远都是队列的第一位能力失去锁

AQS有一个同步队列(CLH),是一种先进先出队列。偏心锁的意思就是严格依照这个队列的程序来获取锁,非偏心锁的意思就是不肯定依照这个队列的程序来。

在new对象的时候便会对sync初始化,如下:

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

能够看出默认是非偏心锁,如果传true则初始化为偏心锁。

那咱们首先来看看非偏心锁:

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

        final void lock() {
            // CAS批改状态
            if (compareAndSetState(0, 1))
                // 设置独占线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 进入队列期待
                acquire(1);
        }
        // tryAcquire是AQS的形象办法,咱们这里对其实现
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }    

首先用 compareAndSetState 办法应用CAS批改state状态变量的值,如果批改胜利的话应用 setExclusiveOwnerThread(Thread.currentThread()) 办法将以后线程设置为独占锁的持有线程,否则调用AQS的 acquire 办法进去队列期待解决。

接下来看一下acquire办法:

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

该办法是AQS里的办法,咱们上次曾经介绍过了,这里间接截过去看下:

这次咱们次要关注由子类ReentrantLock实现的tryAcquire办法:

<code class="java">    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) {
                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");
                // 间接批改state遍历,因为曾经持有锁,不须要用CAS去批改
                setState(nextc);
                return true;
            }
            return false;
        }

下面代码和咱们在上次手动实现一个可重入锁的代码差不多,这里就不再开展。

那接下来看一下 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;
    }

release 办法在AQS类中定义好了,咱们子类次要实现 tryRelease 办法:

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

这段代码上篇文章咱们也曾经讲过了,如果遗记的同学能够回头看看。

看完非偏心锁的最初来看看偏心锁的加锁办法:

     protec<p style="color:transparent">来源gao!%daima.com搞$代*!码网</p>ted 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;
        }
    }

其实代码根本和后面一样,只是多了hasQueuedPredecessors办法用来判断是否存在比期待更久的线程,因为要依照等待时间程序获取资源,其它的这里就不再细说了。

其它疑难

以下问题来自 从源码角度了解ReentrantLock

为什么基于FIFO的同步队列能够实现非偏心锁?

由FIFO队列的个性知,先退出同步队列期待的线程会比后退出的线程更凑近队列的头部,那么它将比后者更早的被唤醒,它也就能更早的失去锁。从这个意义上,对于在同步队列中期待的线程而言,它们取得锁的程序和退出同步队列的程序统一,这显然是一种偏心模式。然而,线程并非只有在退出队列后才有机会取得锁,哪怕同步队列中已有线程在期待,非偏心锁的不偏心之处就在于此。回看下非偏心锁的加锁流程,线程在进入同步队列期待之前有两次抢占锁的机会:

  • 第一次是应用compareAndSetState办法尝试批改state变量,只有在以后锁未被任何线程占有(包含本身)时能力胜利。
  • 第二次是在进入同步队列前应用tryAcquire(arg)尝试获取锁。

只有这两次获取锁都失败后,线程才会结构结点并退出同步队列期待。而线程开释锁时是先开释锁(批改state值),而后才唤醒后继结点的线程的。试想下这种状况,线程A曾经开释锁,但还没来得及唤醒后继线程C,而这时另一个线程B刚好尝试获取锁,此时锁恰好不被任何线程持有,它将胜利获取锁而不必退出队列期待。线程C被唤醒尝试获取锁,而此时锁曾经被线程B抢占,故而其获取失败并持续在队列中期待。
那咱们在开发中为什么大多应用非偏心锁?很简略,因为它性能好啊。

为什么非偏心锁性能好

  1. 线程不用退出期待队列就能够取得锁,不仅免去了结构结点并退出队列的繁琐操作,同时也节俭了线程阻塞唤醒的开销,线程阻塞和唤醒波及到线程上下文的切换和操作系统的零碎调用,是十分耗时的。。
  2. 缩小CAS竞争。如果线程必须要退出阻塞队列能力获取锁,那入队时CAS竞争将变得异样强烈,CAS操作尽管不会导致失败线程挂起,但一直失败重试导致的对CPU的节约也不能漠视。

总结

无关 ReentrantLock的常识就介绍到这里了,有什么不对的中央请多多指教。


搞代码网(gaodaima.com)提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发送到邮箱[email protected],我们会在看到邮件的第一时间内为您处理,或直接联系QQ:872152909。本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:关于SegmentFault:啃碎JDK源码八ReentrantLock

喜欢 (0)
[搞代码]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址