ReentrantLock和synchronized一样都是实现线程同步,但是像比synchronized它更加灵活、强大、增加了轮询、超时、中断等高级功能,可以更加精细化的控制线程同步,它是基于AQS实现的锁,他支持公平锁和非公平锁,同时他也是可重入锁和自旋锁。

本章将基于源码来探索一下ReentrantLock的加锁机制,文中如果存在理解不到位的地方,还请提出宝贵意见共同探讨,不吝赐教。

公平锁和非公平锁的加锁机制流程图:

一、ReentrantLock的公平锁

使用ReentrantLock的公平锁,调用lock进行加锁,lock方法的源码如下:

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

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

可以看到,FairLock首先调用了tryAcquire,tryAcquire源码如下:

/**
 * 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) {
        //如果队列中不存在等待的线程或者当前线程在队列头部,则基于CAS进行加锁
        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;
}

从源码中可以看到,当state为0,即没有线程获取到锁时,FairLock首先会调用hasQueuedPredecessors()方法检查队列中是否有等待的线程或者自己是否在队列头部,如果队列中不存在等待的线程或者自己在队列头部则调用compareAndSetState()方法基于CAS操作进行加锁,如果CAS操作成功,则调用setExclusiveOwnerThread设置加锁线程为当前线程。

当state不为0,即有线程占用锁的时候会判断占有锁的线程是否是当前线程,如果是的话则可以直接获取到锁,这就是ReentrantLock是可重入锁的体现。

如果通过调用tryAcquire没有获取到锁,从源码中我们可以看到,FairLock会调用addWaiter()方法将当前线程加入CLH队列中,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;
        //基于CAS将当前线程节点加入队列尾部
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果CAS操作失败,则调用enq自旋加入队列
    enq(node);
    return 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;
                }
            }
        }
  }

在addWaiter方法中,会CAS操作将当前线程节点加入队列尾部,如果第一次CAS失败,则会调用enq方法通过自旋的方式,多次尝试进行CAS操作将当前线程加入队列。

将当前线程加入队列之后,会调用acquireQueued方法实现当前线程的自旋加锁,acquireQueued源码如下:

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

在acquireQueued方法中每次自旋首先会调用predecessor()方法获取,当前线程节点的前节点,如果发现前节点是head节点,则说明当前线程节点处于对头(head是傀儡节点),那么则调用tryAcquire尽心加锁。

如果当前线程节点不在队列头部,那么则会调用shouldParkAfterFailedAcquire方法判断当前线程节点是否可以挂起知道前节点释放锁时唤醒自己,如果可以挂起,则调用parkAndCheckInterrupt实现挂起操作。

shouldParkAfterFailedAcquire源码如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (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 {
            /*
             * 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;
    }

shouldParkAfterFailedAcquire源码中,如果当前线程节点的前节点的waitStatus状态为SIGNAL(-1)时,表明前节点已经设置了释放锁时唤醒(unpark)它的后节点,那么当前线程节点可以安心阻塞(park),等待它的前节点在unlock时唤醒自己继续尝试加锁。

如果前节点的waitStatus状态>0,即为CANCELLED (1),表明前节点已经放弃了获取锁,那么则会继续往前找,找到一个能够在unlock时唤醒自己的线程节点为止。如果前节点waitStatus状态是CONDITION (-2),即处于等待条件的状态,则会基于CAS尝试设置前节点状态为SIGNAL(主动干预前节点达到唤醒自己的目的)。

parkAndCheckInterrupt源码:

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

二、ReentrantLock的非公平锁

和公平锁加锁机制不同的是,非公平锁一上来不管队列中是否还存在线程,就直接使用CAS操作进行尝试加锁(这就是它的非公平的体现),源码如下:

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

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

如果CAS操作失败(一上来就吃了个闭门羹),则调用acquire方法进行后续的尝试和等待。从源码中可以看到,首先回调用tryAcquire方法进行再次尝试加锁或者锁重入,NoFairLockd的tryAcquire方法源码如下:

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

可以看到NoFairLock的tryAcquire方法和FairLock的tryAcquire方法唯一不同之处是NoFairLock中尝试加锁前不需要调用hasQueuedPredecessors方法判断队列中是否存在其他线程,而是直接进行CAS操作加锁。

那么如果再次尝试加锁或者锁重入失败,则会进行后续的和公平锁完全一样的操作流程(不再赘述),即:加入队列(addWaiter)–>自旋加锁(acquireQueued)。另外,关注Java知音公众号,回复“后端面试”,送你一份面试题宝典!

三、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. 公平锁、非公平锁加锁过程
  2. 自旋锁的实现以及自旋过程中的阻塞唤醒
  3. 可重入锁的实现
  4. CLH队列

转载:blog.csdn.net/qq_40400960/article/details/114242448

深入了解ReentrantLock中的公平锁和非公平锁的加锁机制的更多相关文章

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

    公平锁与非公平锁 ReentrantLock有一个很大的特点,就是可以指定锁是公平锁还是非公平锁,公平锁表示线程获取锁的顺序是按照线程排队的顺序来分配的,而非公平锁就是一种获取锁的抢占机制,是随机获得 ...

  2. ReentrantLock中的公平锁与非公平锁

    简介 ReentrantLock是一种可重入锁,可以等同于synchronized的使用,但是比synchronized更加的强大.灵活. 一个可重入的排他锁,它具有与使用 synchronized ...

  3. Java中的公平锁和非公平锁实现详解

    前言 Java语言中有许多原生线程安全的数据结构,比如ArrayBlockingQueue.CopyOnWriteArrayList.LinkedBlockingQueue,它们线程安全的实现方式并非 ...

  4. 深入分析ReentrantLock公平锁和非公平锁的区别

    在ReentrantLock中包含了公平锁和非公平锁两种锁,通过查看源码可以看到这两种锁都是继承自Sync,而Sync又继承自AbstractQueuedSynchronizer,而AbstractQ ...

  5. Java之ReentrantLock公平锁和非公平锁

    在Java的ReentrantLock构造函数中提供了两种锁:创建公平锁和非公平锁(默认).代码如下: public ReentrantLock() { sync = new NonfairSync( ...

  6. 第五章 ReentrantLock源码解析1--获得非公平锁与公平锁lock()

    最常用的方式: int a = 12; //注意:通常情况下,这个会设置成一个类变量,比如说Segement中的段锁与copyOnWriteArrayList中的全局锁 final Reentrant ...

  7. 深入分析ReentrantLock公平锁和非公平锁的区别 (转)

    在ReentrantLock中包含了公平锁和非公平锁两种锁,通过查看源码可以看到这两种锁都是继承自Sync,而Sync又继承自AbstractQueuedSynchronizer,而AbstractQ ...

  8. 理解ReentrantLock的公平锁和非公平锁

    学习AQS的时候,了解到AQS依赖于内部的FIFO同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个Node对象并将其加入到同步队列,同时会阻塞当 ...

  9. 死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁

    问题 (1)重入锁是什么? (2)ReentrantLock如何实现重入锁? (3)ReentrantLock为什么默认是非公平模式? (4)ReentrantLock除了可重入还有哪些特性? 简介 ...

随机推荐

  1. 红色小圆点+数字的badge自定义小方法 by Nicky.Tsui

    效果如图. 实现方法比较简单,在view上增加一个label label设置: 1 badgeLabel = [[UILabel alloc]initWithFrame:CGRectMake(CGRe ...

  2. 彻底搞清楚class常量池、运行时常量池、字符串常量池

    彻底搞清楚class常量池.运行时常量池.字符串常量池 常量池-静态常量池 也叫 class文件常量池,主要存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference ...

  3. 实现redis哨兵,模拟master故障场景

    由于主从架构无法实现master和slave角色的自动切换,所以在发送master节点宕机时,redis主从复制无法实现自动的故障转移,即将slave 自动提升为新的master.因此,需要配置哨兵来 ...

  4. 磁盘管理+三剑客之awk

    目录 磁盘管理+三剑客之awk 一.磁盘管理 二.格式化命令awk 1.awk的语法 2.参数 3.awk的生命周期 4.awk中的预定义变量 5.awk处理规则的执行流程 6.awk中的函数 7.a ...

  5. 搭建 NFS 服务 & 实时同步

    今日内容 NFS简介 实现 NFS 文件同步功能 NFS 配置详解 统一用户 搭建 web 服务 NFS 实现文件共享 内容详细 1.NFS 简介 1.1 介绍 实现多台 web 服务器可以共享数据资 ...

  6. HashSet源码学习

    一.介绍 1.HashSet 2.HashSet和HashMap的区别 1.相同点 HashSet 内部使用 HashMap 存储元素,对应的键值对的键为 Set 的存储元素,值为一个默认的 Obje ...

  7. netty系列之:Bootstrap,ServerBootstrap和netty中的实现

    目录 简介 Bootstrap和ServerBootstrap的联系 AbstractBootstrap Bootstrap和ServerBootstrap 总结 简介 虽然netty很强大,但是使用 ...

  8. ReentrantLock与synchronized比较分析

    ReentrantLock:完成了Lock接口,是一个可重入锁,并且支持线程公正竞赛和非公正竞赛两种形式,默认情况下对错公正形式.ReentrantLock算是synchronized的补充和替代计划 ...

  9. sql server通过T-SQL导出Excel到磁盘

    ALTER PROCEDURE [dbo].[pro_ImportExcelByTime] AS BEGIN --第一步,开启高级功能 EXEC sp_configure 'show advanced ...

  10. CobaltStrike逆向学习系列(11):自实现 Beacon 检测工具

    这是[信安成长计划]的第 11 篇文章 关注微信公众号[信安成长计划] 0x00 目录 0x01 检测原理 0x02 检测方案 0x03 存在的问题 0x04 解决方案 0x05 示例代码 0x06 ...