ReentrantLock,翻译过来叫做重入锁,是实现线程安全的一个方式,和synchronized的作用类似,但是他的实现原理是什么呢,在查看了很多的博客之后,我决定自己写一篇,形成自己的理解。从ReentrantLock的方法一个一个的来吧。
补充:博客中会不停的提到一个叫做标记的概念,是我自己给起的名字,就是AbstractQueuedSynchronizer的private volatile int state;对象,在ReentrantLock中,如果当前对象为0则表示锁没有被任何线程占有,否则表示被占有了,只有当前占有锁的线程可以继续获得锁(通过调用lock或者trylock方法),继续占有锁会继续增加这个属性的值(没调用一次lock或者trylock就会加一)
1、new ReentrantLock()
构造方法,在这个方法里面会形成一个NonfairSync,也就是不公平的同步器,即并不是公平的,先来的线程不一定先获得锁(当然还有一个含有参数的构造方法,可以形成一个公平的同步器,先不看这个。在这篇博客中都是以不公平的锁来做说明的。)。NonfairSync是ReentrantLock.Sync的子类,而ReentrantLock.Sync是AbstractQueuedSynchronizer(也就是常说的aqs)的子类,用来做同步器。
2、lock()
关键方法,尝试获得锁的操作。在javadoc中已经说明了,如果当前线程没有获得锁且当前的锁没有被其他线程捕获,就会获得锁,并将标记置为1;如果已经获得锁了会继续获得锁,并且将标记加一;如果当前的锁被其他线程获得,则当前线程被挂起。他的内部实现是使用之前形成的sync对象,调用的是sync的lock方法,
final void lock() { if (compareAndSetState(0, 1))//如果当前的标记为是0(即没有线程占有当前的锁),那么原子性的将标记为设置为1。 setExclusiveOwnerThread(Thread.currentThread());//如果上面返回true,则设置持有锁的线程为当前的线程。这个方法比较简单,不做过多的介绍 else acquire(1);//如果当前的锁被占有(可能是当前的线程,也可能不是当前的线程) }
下面挨个方法的分析:
2.1 comapreAndSetState(int expect,int value):这个方法在AbstractQueuedSynchronizer中,是CAS的操作,CAS是一个机器操作命令,使用非java语言实现,可以黑盒的理解为他是原子性的,意思是如果标记的值是expect的值,那么就原子性的将标记置为vlaue的值,如果标记的不是则gai
2.2 acquire(1),方法很重要,存在于aqs中
public final void acquire(int arg) {//这里的arg是1, if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
这个方法会先调用tryAcquire,即尝试获得锁,如果获得成功,则返回true,否则返回false,如果返回true的话就结束改方法,此时已经获得了锁;如果返回false,则调用acquireQueued。我们先看看tryAcquire方法的实现:
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState();//获得标记 if (c == 0) {//如果标记为0,表示当前的锁没有被占用, if (compareAndSetState(0, acquires)) {//使用cas将标记置为1,这个和上面的lock方法的if分支是一个道理。 setExclusiveOwnerThread(current); return true;//成功获得锁,返回true } }else if (current == getExclusiveOwnerThread()) {//如果当前的锁被占用了且占用锁的线程是当前的线程, int nextc = c + acquires;//增加标记的值,增大1,即上面说的,在已经占用了锁的情况下没调用一次lock或者trylock都会增大标记,增大1. if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc);//这个方法并不是cas的,为什么呢?不害怕并发的问题吗? 原因很简单,因为当前的锁已经被当前的线程持有,也就是只有一个线程在运行,所以不会出现并发问题。 return true;//成功获得锁,返回true。 } return false;//没有获得锁,返回false。 }
看完了tryAcquire方法,继续看一下addWaiter方法,这个方法用于将调用tryAcquire方法并返回false,也就是没有获得锁的线程加入到等待队列中,Node只是封装了线程
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); Node pred = tail;//队列的尾巴 if (pred != null) {//当不是第一次进入的时候,pred不是null,则进入if, node.prev = pred;//将新的node的prev指向尾巴 if (compareAndSetTail(pred, node)) {//原子性的更新tail,如果成功进入if,这里有点绕,如果我进入if后,其他线程又更新成功了呢?会不会出错啊?不会的,因为这里仅仅是更新tail,即使更新了100个tail,前面进入if的线程已经获得了更新tail之前的tail(也就是pred),仍然能将链表串联起来。 pred.next = node;//将原先的tail的尾巴指向最新的tail return node; } } enq(node);//当第一次进入的时候tail是null,进入这个方法。这个方法很简单,就是原子性的设置tail和head的Node,这个node的状态为0,不再贴代码了。 return node; }
再看一下acquireQueued方法:这个方法用于将没有获得锁的线程挂起。
final boolean acquireQueued(final Node node, int arg) {//node表示封装了没有获得锁的线程的对象,arg为1. boolean failed = true; try { boolean interrupted = false; for (;;) {//这个是死循环的原因是因为现在没有获得锁的node的状态时exclusive,在下面的shouldParkAfterFailedAcquire方法中会检查这个状态,然后更新为signal,这样线程就能被挂起了 final Node p = node.predecessor();//node的上一个节点 if (p == head && tryAcquire(arg)) {//如果上一个节点是head,即当前的线程是要马上获得锁的线程,并且尝试获得了锁,则进入if,此时当前的线程不被挂起。 setHead(node);//从这个地方可以得出这样的结论:head有两种情况,一个是无意义的,也就是第一次有阻塞的线程的时候,第二种是之前被阻塞的线程获得锁之后就会被设置为head。 p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())//在这个方法中会更新node的状态,将head的状态变为signal(-1),默认在创建head的时候是0, 然后将当前的线程挂起。这个方法很重要,当被挂起的线程又运行后,还是从这里运行,进入下一轮for循环,会进入到上面的if,这样锁就可以被下一个线程捕获了。 interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
没有获得锁的线程在acquireQueued之后,有两种可能:一是被挂起,也就是进入了上面的if(shouldPa....分支,二是当前的线程获得锁。 至此,获得锁的过程就完了用几句话总结下就是:如果当前的标记是0,则当前的线程获得锁,如果当前的标记不为0 ,则看当前的线程是否是已经获得锁的线程,如果是则将标记加一,如果不是,则将当前的线程加入到一个队列中,并将当前的线程挂起。从这里可以得出,如果调用了m次lock,而调用了m次的unlock,就会造成其他线程获取不到锁。
3、lockInterruptibly():这个和Lock很像,不同之处在于,如果当前的线程如果在获取锁之前或者获取不到在加入到队列之后再重新获取到锁之后被interrupt,就会抛一个InterruptedException。代码我就不贴了,基本类似上面的代码。
4、tryLock():这个方法更简单,尝试获取锁,如果获取不到返回false,不会加入到队列中等待,如果获取得到,则返回true,如果当前线程已经获取了锁,也返回true。
5、getHoldCount():去的标记的大小,也就是当前线程加锁的次数。如果是0表示当前的线程没有获得锁。
6、isHeldByCurrentThread():判断当前的锁是不是由当前的线程持有。
7、isLocked:判断当前的锁是否加锁了
8:、hasQueuedThreads:判断当前的锁是否是多个线程在获取,他的内部实现是判断队列的head和tail是否相等,也就是判断是否有其他的线程加入了争夺锁的竞争中。
9、unlock:释放所,如果当前的线程只 加了一次锁,则会释放,否则仍然会持有锁,但是标记一定减1.最终调用的是NonFairSync的release方法,参数是1:
public final boolean release(int arg) { if (tryRelease(arg)) {//当标记为0的时候返回true,也就是锁当前没有被任何线程获得 Node h = head; if (h != null && h.waitStatus != 0)//这里的head可能不存在,即在当前的线程获得了锁之后没有其他的线程加入到队列中。当head不是null的话,唤醒head后面阻塞的线程(head也可能是封装当前的线程的node) unparkSuccessor(h);//unpark方法会唤醒head的下一个节点,下一个节点在acquireQueued中阻塞了,唤醒后继续做for循环,可以参考acquireQueued方法。 return true; } return false; }
这样就算是看完了ReentrantLock的所有的关键点了,不过还没有看公平的锁,下一个博客中介绍这个。
相关推荐
Java中的ReentrantLock是Java并发包(java.util.concurrent.locks)中的一个高级锁,它是可重入的,意味着一个线程可以多次获取同一锁。在深入ReentrantLock之前,我们首先需要了解Java并发编程的基础,特别是Java...
本项目"tuling-juc-final.zip"显然聚焦于Java并发编程的实践,通过一系列代码示例来演示和解释Java内存模型(JMM)、`synchronized`关键字以及`volatile`关键字的使用。下面我们将深入探讨这些核心概念。 Java内存...
Java-JUC-多线程进阶 Java-JUC-多线程进阶resources是 Java 并发编程的高级课程,涵盖了 Java 中的并发编程概念、线程安全、锁机制、集合类、线程池、函数式接口、Stream流式计算等多个方面。 什么是JUC JUC...
Java并发编程中的ReentrantLock是Java并发包(java.util.concurrent,简称JUC)中的一个重要的锁机制,它是Lock接口的一个具体实现,提供了比synchronized更强大的锁定功能和更细粒度的控制。ReentrantLock的主要...
JUC是Java 5及后续版本引入的一个重要特性,极大地提升了Java在多处理器和高并发环境下的性能表现。 在Java中,JUC包(java.util.concurrent)包含了一系列的类和接口,这些类和接口主要用于解决并发问题,如线程池...
Java并发编程是Java开发中的重要领域,而JUC(Java Util Concurrency)是Java平台提供的一套高级并发工具包,它极大地简化了多线程和并发控制的复杂性。本笔记主要围绕尚硅谷周阳老师的JUC课程展开,旨在帮助个人...
Java 提供了一个名为 JUC(Java Utilities for Concurrency)的框架,用于帮助开发者编写高效、可靠的多线程程序。本文将对 JUC 框架中的类进行分类和总结,并提供学习指南。 JUC 框架包含多个部分,包括 Lock 框架...
### JUC并发编程 #### JUC多线程及高并发 Java并发编程包(java.util.concurrent,简称JUC)封装了大量用于高并发编程的工具类和接口,其中涉及了线程池、阻塞队列、同步器、原子操作类等。在并发环境下,可以有效...
学习狂神说的juc编程的笔记
Java Util Concurrency (JUC) 是 Java SDK 中的一个核心包,位于 `java.util.concurrent` 下,它提供了丰富的线程同步和并发工具类,旨在简化多线程编程,提高程序的并发性能。JUC 包含了线程池、并发容器、同步器、...
juc-jenkins-2018 JUC Jenkins 2018演示源代码 先决条件 为了运行此演示,必须有一个有效的JDK,git命令以及curl。 克隆存储库 将此存储库克隆到您家中的某个位置: git clone ...
NIO(New Input/Output),是Java提供的一种非阻塞I/O模型,相较于传统的BIO(Blocking I/O),NIO具有更好的性能和更高的灵活性。在NIO中,Channel(通道)和Buffer(缓冲区)是两个核心概念。Channel类似于流,...
本项目"juc-learn"专注于JUC相关源码的分析和使用介绍,旨在帮助开发者深入理解并熟练运用这些并发工具。 1. **并发基础** 在Java中,多线程是并发编程的基础。通过创建Thread对象或实现Runnable接口,我们可以...
微程序控制器实验1. 连接好实验线路,检查无误后接通电源。2. 将编程开关(MJ20)置为PROM(编程)状态。3. 将STATE UNIT中的STEP置为“STEP”状态,STOP置为“RUN...7. 重复上述3至6步骤,将每一条微指令写入E2PROM2816。
《JUC:Java并发编程的艺术》 在Java世界中,JUC(Java Util Concurrency)是并发编程的核心库,它提供了丰富的...通过深入学习JUC-master项目,我们可以更深入地理解Java并发编程的原理和实践,提升我们的编程技能。
juc-demo JUC包下常用工具练习Demo 内容: 1、Semaphore 2、CountDownLatch 3、CyclicBarrier 4、ReentrantLock + Condition实现阻塞队列 Created by @minghui.y.
1、Java并发体系-第一阶段-多线程基础知识 2、Java并发体系-第二阶段-锁与同步-[1] 3、Java并发体系-第二阶段-锁与同步-[2] 4、Java并发体系-第二阶段-锁与同步-[3] ...7、Java并发体系-第四阶段-AQS源码解读-[1]
JUC线程高级,
JUC是Java平台提供的一组高级并发工具包,它极大地简化了多线程编程,并提供了更高效、安全的并发解决方案。在本课程中,你将学习到如何利用这些工具来提升应用程序的并发性能,同时理解其底层实现原理。 课程内容...
教程视频:在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类, 用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架;还提供了设计用于多线程上下文...