广告
返回顶部
首页 > 资讯 > 后端开发 > Python >java锁机制ReentrantLock源码实例分析
  • 605
分享到

java锁机制ReentrantLock源码实例分析

2024-04-02 19:04:59 605人浏览 泡泡鱼

Python 官方文档:入门教程 => 点击学习

摘要

目录一:简述二:ReentrantLock类图三:流程简图四:源码分析lock()源码分析:非公平实现:公平锁实现:tryAcquire()方法公平锁实现:非公平锁实现:addWai

一:简述

ReentrantLock是java.util.concurrent包中提供的一种锁机制,它是一种可重入,互斥的锁,ReentrantLock还同时支持公平和非公平两种实现。本篇文章基于java8,对并发工具ReentrantLock的实现原理进行分析。

二:ReentrantLock类图

三:流程简图

四:源码分析

我们以lock()方法和unlock()方法为入口对ReentrantLock的源码进行分析。

lock()源码分析:

ReentrantLock在构造构造方法创建对象的时候会根据构造函数传递的fair参数 创建不同的Sync对象(默认是非公平锁的实现),reentrantLock.lock()会调用sync.lock()。在Sync类中的lock()方法是一个抽象方法,具体实现分别在FairSync(公平)和NonfairSync(非公平)中。

    public ReentrantLock() {
        //默认是非公平锁
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    public void lock() {
        sync.lock();
    }

非公平实现:

       final void lock() {
            // state表示锁重入次数 0代表无锁状态
            //尝试抢占锁一次  利用cas替换state标志 替换成功代表抢占锁成功
            if (compareAndSetState(0, 1))
		//抢占成功将抢占到锁的线程设置为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

公平锁实现:

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

可以看出非公平锁在lock()方法开始会尝试cas抢占一次锁,也就是在这里会插队一次。然后都会调用acquire()方法。acquire()方法中调用了三个方法,tryAcquire(),addWaiter(),acquireQueued()三个方法,而这三个方法正是ReentrantLock加锁的核心方法,接下来我们会对这三个方法进行重点的分析。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(node.EXCLUSIVE), arg))		
	//这里acquireQueued()是一步步将线程是否中断的标志传回来的 如果为true 那么代表线程被中断过 需要设置中断标志 交给客户端处理
	// 因为LockSupport.park()之后不会对中断进行响应 所以需要一步一步将中断标记传回来
            selfInterrupt();
        }

tryAcquire()方法

流程图:

tryAcquire()方法是抢占锁的方法,返回ture表示线程抢占到锁了,如果抢占到锁就什么都不做,直接执行同步代码,如果没有抢占到锁就需要将线程的信息保存起来,并且阻塞线程,也就是调用addWaiter()方法和acquireQueued()方法。

tryAcquire()也有公平和非公平锁两种实现。

公平锁实现:

	protected final boolean tryAcquire(int acquires) {
	    //获取当前线程
            final Thread current = Thread.currentThread();
	    //获取state的值
            int c = getState();
            if (c == 0) {
	    //如果state为0 代表现在是无锁状态 那么可以抢占锁
	    //公平锁这里多了一个判断 只有在AQS链表中不存在元素 才去尝试抢占锁 否则就去链表中排队
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
		//cas替换成功 那么就代表抢占锁成功了 将获取锁的线程设置为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
	//判断获取锁的线程是否是当前线程 如果是的话增加重入次数 (即增加state的值)
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

非公平锁实现:

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) {
		//如果state为0 代表现在是无锁状态 非公平锁这里就直接尝试抢占锁
		// 公平锁这里多了一个判断 先去看AQS链表中有没有已经在等待的线程 有的话就不会尝试去抢占锁了,非公平锁这里没有这个判断,也就是这里允许插队(不公平)
                if (compareAndSetState(0, acquires)) {	
		//cas替换成功 那么就代表抢占锁成功了 将获取锁的线程设置为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
	   //判断获取锁的线程是否是当前线程 如果是的话增加重入次数 (即增加state的值)
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //重新设置state的值    
                setState(nextc);
                return true;
            }
            return false;
        }

接下来分析addWaiter()方法

addWaiter()

addWaiter()方法的作用就是将没有获取到锁的线程封装成一个Node对象,然后存储在AbstractQueuedSynchronizer这个队列同步器中(为了偷懒简称AQS)。我们先看一下Node对象的结构。

    static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITioN = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
        final boolean isshared() {
            return nextWaiter == SHARED;
        }
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
        Node() {    // Used to establish initial head or SHARED marker
        }
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

通过Node对象的结构我们可以看出这是一个双向链表的结构,保存了prev和next引用,thread成员变量用来保存阻塞的线程引用,并且node是有状态的,分别是CANCELLED,SIGNAL,CONDITION,PROPAGATE,其中ReentrantLock涉及到的状态就是SIGNAL(等待被唤醒),CANCELLED(取消)(由于相关性不强,其他的状态在后续文章用到的时候在讲吧)。而AQS又保存了链表的头结点head和尾结点tail,所以实际上AQS存储阻塞线程的数据结构是一 个Node双向链表。addWaiter()方法的作用就是将阻塞线程封装成Node并且将其保存在AQS的链表结构中。

流程图:

源码分析:

private Node addWaiter(Node mode) {
	//将没有获得锁的线程封装成一个node 
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
	//如果AQS尾结点不为null 代表AQS链表已经初始化 尝试将构建好的节点添加到链表的尾部
        if (pred != null) {
            node.prev = pred;
	    //cas替换AQS的尾结点 
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
	//没有初始化调用enq()方法
        enq(node);
        return node;
    }
private Node enq(final Node node) {
	    //自旋
        for (;;) {
            Node t = tail;
	    //尾结点为空 说明AQS链表还没有初始化 那么进行初始化
            if (t == null) { // Must initialize
	    //cas 将AQS的head节点 初始化 成功初始化head之后,将尾结点也初始化
            //注意 这里我们可以看到head节点是不存储线程信息的 也就是说head节点相当于是一个虚拟节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
		//尾结点不为空 那么直接添加到链表的尾部即可
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

接下来分析acquireQueued()

acquireQueued()

acquireQueued()方法的作用就是利用Locksupport.park()方法将AQS链表中存储的线程阻塞起来。

流程图:

源码分析:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
	    //进入自旋
            for (;;) {
		//获取当前节点的前一个节点
                final Node p = node.predecessor();
		// 如果前一个节点是head 而且再次尝试获取锁成功,将节点从AQS队列中去除 并替换head 同时返回中断标志
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    //注意 只有抢占到锁才会跳出这个for循环
                    return interrupted;
                }
                //去除状态为CANCELLED的节点 并且阻塞线程 线程被阻塞在这
                //注意 线程被唤醒之后继续执行这个for循环 尝试抢占锁 没有抢占到的话又会阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                //如果失败 将node状态修改为CANCELLED
                cancelAcquire(node);
        }
    }

在acquireQueued()方法中有两个方法比较重要shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法。

shouldParkAfterFailedAcquire()

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
	    //如果节点是SIGNAL状态 不需要处理 直接返回
            return true;
        if (ws > 0) {
            
	   //如果节点状态>0 说明节点是取消状态 这种状态的节点需要被清除 用do while循环顺便清除一下前面的连续的、状态为取消的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            
	    //正常的情况下 利用cas将前一个节点的状态替换为 SIGNAL状态 也就是-1
	    //注意 这样队列中节点的状态 除了最后一个都是-1 包括head节点
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
  //挂起当前线程 并且返回中断标志  LockSupport.park(thread) 会调用UNSAFE.park()方法将线程阻塞起来(是一个native方法)
        LockSupport.park(this);
        return Thread.interrupted();
    }

到这里lock()方法也就分析完了 接下来我们分析unlock()方法

unlock()方法源码分析:

流程图:

reentrantLock的unlock()方法会调用sync的release()方法。

public void unlock() {
	    //每次调用unlock 将state减1
        sync.release(1);
}

release()方法有两个方法比较重要,tryRelease()方法和unparkSuccessor(),tryRelease()方法计算state的值,看线程是否已经彻底释放锁(这是因为ReentrantLock是支持重入的),如果已经彻底释放锁那么需要调用unparkSuccessor()方法来唤醒线程,否则不需要唤醒线程。

public final boolean release(int arg) {
	//只有tryRelease返回true 说明已经释放锁 需要将阻塞的线程唤醒 否则不需要唤醒别的线程
        if (tryRelease(arg)) {
            Node h = head;
	//如果头结点不为空 而且状态不为0 代表同步队列已经初始化 且存在需要唤醒的node
	//注意 同步队列的头结点相当于是一个虚拟节点  这一点我们可以在构建节点的代码中很清楚的知道
	//并且在shouldParkAfterFailedAcquire方法中 会把head节点的状态修改为-1
	//如果head的状态为0 那么代表队列中没有需要被唤醒的元素 直接返回true
            if (h != null && h.waitStatus != 0)
		//唤醒头结点的下一个节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease()

protected final boolean tryRelease(int releases) {
	     //减少重入次数
            int c = getState() - releases;
	    //如果获取锁的线程不是当前线程 抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
	    //如果state为0 说明已经彻底释放了锁 返回true
            if (c == 0) {
                free = true;
		//将获取锁的线程设置为null
                setExclusiveOwnerThread(null);
            }
	    //重置state的值
            setState(c);
	    //如果释放了锁 就返回true 否则返回false
            return free;
        }

unparkSuccessor()

private void unparkSuccessor(Node node) {
        //获取头结点的状态 将头结点状态设置为0 代表现在正在有线程被唤醒 如果head状态为0 就不会进入这个方法了
        int ws = node.waitStatus;
        if (ws < 0)
            //将头结点状态设置为0
            compareAndSetWaitStatus(node, ws, 0);
        
	//唤醒头结点的下一个状态不是cancelled的节点 (因为头结点是不存储阻塞线程的)
        Node s = node.next;
	//当前节点是null 或者是cancelled状态
        if (s == null || s.waitStatus > 0) {
            s = null;
	 //从aqs链表的尾部开始遍历 找到离头结点最近的 不为空的 状态不是cancelled的节点 赋值给s 这里为什么从尾结点开始遍历而是头结点 应该是因为添加结点的时候是先初始化结点的prev的, 从尾结点开始遍历 不会出现prve没有赋值的情况 
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
	    //调用LockSupport.unpark()唤醒指定的线程
            LockSupport.unpark(s.thread);
    }	

五:总结

最后总结一下,有以下几点需要注意:

1.ReentrantLock有公平锁和非公平锁两种实现,其实这两种实现的差别只有两点,第一是在lock()方法开始的时候非公平锁会尝试cas抢占锁插一次队, 第二是在tryAcquire()方法发现state为0的时候,非公平锁会抢占一次锁,而公平锁会判断AQS链表中是否存在等待的线程,没有等待的线程才会去抢占锁。

2.AQS存储阻塞线程的数据结构是一个双向链表的结构,而且它是遵循先进先出的,因为它是从头结点的下一个节点开始唤醒,而添加新的节点的时候是添加到链表的尾部,所以AQS同时也是一个队列的数据结构。

3.线程被唤醒之后会继续执行acquireQueued()方法,因为它是阻塞在acquireQueued()方法的for循环中的,唤醒后尝试去获取锁,获取成功就会将节点从AQS中删除并跳出for循环,否则又会继续阻塞。(获取锁失败的原因就是因为有人插队啊。。也就是非公平锁导致的)。

以上就是java锁机制ReentrantLock源码实例分析的详细内容,更多关于java锁机制ReentrantLock的资料请关注编程网其它相关文章!

--结束END--

本文标题: java锁机制ReentrantLock源码实例分析

本文链接: https://www.lsjlt.com/news/169023.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • java锁机制ReentrantLock源码实例分析
    目录一:简述二:ReentrantLock类图三:流程简图四:源码分析lock()源码分析:非公平实现:公平锁实现:tryAcquire()方法公平锁实现:非公平锁实现:addWai...
    99+
    2022-11-13
  • java底层AQS实现类ReentrantLock锁的构成及源码解析
    目录引导语1、类注释2、类结构3、构造器 4、Sync 同步器4.1、nonfairTryAcquire 4.2、tryRelease5、FairSync 公平锁...
    99+
    2022-11-13
  • java中锁机制的示例分析
    这篇文章主要介绍java中锁机制的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!何为同步?JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使...
    99+
    2023-06-19
  • webpack源码之loader机制的示例分析
    这篇文章主要介绍webpack源码之loader机制的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!loader概念loader是用来加载处理各种形式的资源,本质上是一个函数...
    99+
    2022-10-19
  • MySQL锁机制的示例分析
    这篇文章主要介绍了MySQL锁机制的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。锁在MySQL中是非常重要的一部分,锁对MySQL...
    99+
    2022-10-18
  • Java反射机制实例分析
    这篇“Java反射机制实例分析”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Java反射机制实例分析”文章吧。Java反射机...
    99+
    2023-06-29
  • MySQL中表锁和行锁机制浅析(源码篇)
    目录前言行锁mysql 事务属性事务常见问题事务的隔离级别间隙锁排他锁共享锁分析行锁定行锁优化表锁共享读锁独占写锁查看加锁情况分析表锁定什么场景下用表锁页锁补充:行级锁与死锁总结前言 众所周知,MySQL的存储引擎有My...
    99+
    2022-11-04
  • 如何用源码分析Java HashMap实例
    本篇文章为大家展示了如何用源码分析Java HashMap实例,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。引言HashMap在键值对存储中被经常使用,那么它到底是如何实现键值存储的呢?一 Entr...
    99+
    2023-06-17
  • Mysql锁内部实现机制之C源码解析
    目录概述行锁结构表锁结构事务中锁的描述概述 虽然现在关系型数据库越来越相似,但其背后的实现机制可能大相径庭。实际使用方面,因为SQL语法规范的存在使得我们熟悉多种关系型数据库并非难事,但是有多少种数据库可能就有多少种锁的...
    99+
    2022-08-22
  • Java并发编程之ReentrantLock实现原理及源码剖析
    目录一、ReentrantLock简介二、ReentrantLock使用三、ReentrantLock源码分析1、非公平锁源码分析2、公平锁源码分析前面《Java并发编程之JUC并发...
    99+
    2022-11-12
  • Java源码ConcurrentHashMap的示例分析
    小编给大家分享一下Java源码ConcurrentHashMap的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!一、记录形式打算直接把过程写在源码中,会按序进行注释,查阅的时候可以按序号只看注释部分二、Concur...
    99+
    2023-06-15
  • Java反射机制的实例分析
    这篇文章将为大家详细讲解有关Java反射机制的实例分析,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。在 Java 运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法对于任意一个对象...
    99+
    2023-06-17
  • RocketMQ producer容错机制源码分析
    这篇文章主要介绍“RocketMQ producer容错机制源码分析”,在日常操作中,相信很多人在RocketMQ producer容错机制源码分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家...
    99+
    2023-07-05
  • Vue3响应式机制源码分析
    本篇内容介绍了“Vue3响应式机制源码分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!什么是响应式响应式一直都是 Vue 的特色功能之一;...
    99+
    2023-07-06
  • 源码分析Android的消息机制
    一、引言 ​Android消息机制主要指的是Handler的运行机制,是一块很有意思,也很有研究意义的内容。本文计划在较短的篇幅内,通过一定的源...
    99+
    2022-06-06
    消息机制 源码 Android
  • SpringCloud @RefreshScope刷新机制源码分析
    今天小编给大家分享一下SpringCloud @RefreshScope刷新机制源码分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我...
    99+
    2023-07-05
  • java中CopyOnWriteArrayList源码的示例分析
    这篇文章将为大家详细讲解有关java中CopyOnWriteArrayList源码的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。简介CopyOnWriteArrayList是ArrayList的...
    99+
    2023-06-29
  • Java中Handler源码的示例分析
    这篇文章主要介绍了Java中Handler源码的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。从很早开始就认识到 Handler 了,只不过那时修为尚浅,了解的不够深...
    99+
    2023-06-02
  • Java源码解析之ConcurrentHashMap的示例分析
    小编给大家分享一下Java源码解析之ConcurrentHashMap的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!早期 ConcurrentHashMap,其实现是基于:分离锁,也就是将内部进行分段(Segme...
    99+
    2023-06-15
  • Java反射机制原理实例分析
    今天小编给大家分享一下Java反射机制原理实例分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。什么是反射?反射机制是在运行...
    99+
    2023-06-29
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作