ReentrantLock源码——获取公平锁、非公平锁、释放锁

Stella981
• 阅读 517

一般用ReentrantLock的方式如下:

// 新建非公平锁实例
Lock lock = new ReentrantLock();
// 新建非公平锁实例
// Lock lock = new ReentrantLock(true);

// 加锁
lock.lock();
try {
    // 业务逻辑
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 归还锁
    lock.unlock();
}

先看看继承关系,没啥好说的。

image.png

锁创建过程

可以看到代码中调用构造方法时加上一个true就可以创建公平锁。

private final Sync sync;

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

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

在大多数情况下,锁的申请都是非公平的。也就是说,线程1和线程2都需要申请锁A,那么当锁A可用的时候,哪个线程获得锁呢?一般情况都是系统随机挑选一个,因此不能保证其公平性。而公平锁就不是这样的,按照先后顺序来,谁先申请,就谁先获得锁。公平锁带来的一个问题就是需要维护一个有序队列,性能相对低。所以默认情况下,锁都是非公平的。

其中公平锁和非公平锁的类图如下:

image.png

image.png

从图中可以看出 FairSyncNonfairSync的区别,就是两者重写的 lock()tryAcquire()方法。

获取锁

看下ReentrantLock#lock

public void lock() {
    sync.lock(); // 调用NonfairSync#lock或者FairSync#lock
}

概述

先简单概述一下公平锁和非公平锁获取锁的过程:

  1. 获取锁的过程就是设置AbstractQueuedSynchronizer#state属性的过程。
  2. 公平锁因为维护了一个队列,只有当前线程位于队列头部时,才能获取锁(设置state的状态)。
  3. 非公平锁就是一个不断插队的过程,不care现在队列是什么情况,抓住一切机会设置state的状态,设置成功了,就代表获得了锁,几次尝试插队失败以后,才加入到队尾,等待获得锁。

源码

  1. NonfairSync

    final void lock() { // 快速尝试将state从0设置成1 // state=0代表当前没有任何一个线程获得了锁 // Unsafe#compareAndSwapT系列compareAndSwapT(Object o, long offset, T expected, T x)代表 // 如果实例o的offset属性的值为expected时,将offset属性的值设置成x // 实例o的offset对应的是某个属性,通过反射获得某个属性的offset值 if (compareAndSetState(0, 1)) // state设置成1代表获得锁成功 // 将独占锁标识设置为当前线程,在重入锁的时候需要 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 父类实现,AbstractQueuedSynchronizer#acquire }

    // AbstractQueuedSynchronizer#acquire public final void acquire(int arg) { // tryAcquire将调用子类的实现 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

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

    // ReentrantLock.Sync#nonfairTryAcquire // 这又是一个尝试插队的过程 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState();// 获取state值 if (c == 0) {// 当前没有任何一个线程获取锁 if (compareAndSetState(0, acquires)) {// 尝试将state的值从0设置成acquires(即1) setExclusiveOwnerThread(current);// 设置成功标识独占锁 return true; } } // 虽然state!=0,但是当前获取锁的是本线程 else if (current == getExclusiveOwnerThread()) { // 锁状态数量加上acquires,即锁状态数量加1 int nextc = c + acquires; // 重入次数太多,大过Integer.MAX if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

    // AbstractQueuedSynchronizer#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; // 如果pred还是尾部(即没有被其他线程更新),则将尾部更新为node节点(即当前线程快速设置成了队尾) if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 快速设置不成功,正常入队 enq(node); return node; }

    private Node enq(final Node 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; } } } }

    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)) {// 通过tryAcquire获得锁 setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 中断 if (shouldParkAfterFailedAcquire(p, node) && // 是否需要阻塞 parkAndCheckInterrupt()) // 阻塞,返回线程是否被中断 interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

    // 确保当前结点的前驱结点的状态为SIGNAL,SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程。毕竟,只有确保能够被唤醒,当前线程才能放心的阻塞。 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL)// 前驱节点状态为SIGNAL,表明当前线程需要阻塞,因为前置节点承诺执行完之后会通知唤醒当前节点 /* * This node has already set status asking a release * to signal it, so it can safely park. / return true; if (ws > 0) {// ws > 0代表前驱节点取消了 / * Predecessor was cancelled. Skip over predecessors and * indicate retry. / do { node.prev = pred = pred.prev;// 不断的把前驱取消了的节点移除队列 } while (pred.waitStatus > 0); pred.next = node; } else {// 初始化状态,将前驱节点的状态设置成SIGNAL / * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

    // 从方法名可以看出这个方法做了两件事 private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//挂起当前的线程 // 如果当前线程已经被中断了,返回true,否则返回false // 有可能在挂起阶段被中断了 return Thread.interrupted(); }

  2. FairSync
    FairSync相对来说就简单很多,只有重写的两个方法跟NonfairSync不同。

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

    /**

    • Fair version of tryAcquire. Don't grant access unless
    • recursive call or no waiters or is first.

    */ 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. 尝试快速获取锁(本质就是插队);
  2. 快速获取不成功的话,调用acquire方法获取锁
    2.1. 获取锁状态量state值,如果state=0,快速获取锁(本质也是插队)
    2.2. 如果锁状态不为0,但是是当前线程获取到了锁,代表可重入,锁状态值state+1
    2.3. 上述两步都失败,返回false,并将线程加入到队列中阻塞
  3. addWaiter主要作用就是将当前线程封装成Node实例,加入到队尾中
    3.1. 快速入队列尾(本质还是插队)
    3.2. 快速入队列尾失败,调用enq方法,在死循环里一直尝试加入队列尾
  4. 通过acquireQueued方法阻塞
    4.1. 不停的通过循环等待当前节点变成队列头,并再次通过tryAcquire获取锁(又是插队)
    4.2 不成功的话,判断是否需要阻塞
  5. shouldParkAfterFailedAcquire处理是否需要阻塞,只要当前节点的前驱节点中的其中某个节点waitStatus == -1即可
  6. parkAndCheckInterrupt方法负责阻塞,并且在阻塞结束后判断线程是否被中断了,如果被中断了,调用selfInterrupt方法。

释放锁

源码

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

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;// 重入次数减少releases次(即1次)
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {// 完全释放锁
        free = true;
        setExclusiveOwnerThread(null);// 解除独占标识
    }
    setState(c);
    return free;
}

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从后往前找到离head最近,而且waitStatus <= 0 的节点
        // 其实在ReentrantLock中,waitStatus应该只能为0和-1,需要唤醒的都是-1(Node.SIGNAL)
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);// 唤醒挂起线程
}
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
Python3:sqlalchemy对mysql数据库操作,非sql语句
Python3:sqlalchemy对mysql数据库操作,非sql语句python3authorlizmdatetime2018020110:00:00coding:utf8'''
Wesley13 Wesley13
2年前
Java并发包小结
1、Lock  Lock功能对应关键字synchrozied功能,lock和unlock方法用于加锁和释放锁。等待锁的线程加入到等待链表中,同时阻塞线程,锁释放时,从等待链表中取出等待的线程执行,取等待的线程分公平与非公平两种方式,公平方式取第一个等待的线程,非公平方式当前正在获取锁的线程可能立刻执行,而不用加入到等待队列中,排队执行。2、Con
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这