From da218463342e5b984fea4cf5673fbadfb6cafa59 Mon Sep 17 00:00:00 2001 From: itwanger Date: Thu, 31 Aug 2023 18:55:11 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=BB=E5=86=99=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/home.md | 2 +- docs/thread/lock.md | 169 +++++++++++++++++++++++------------ docs/thread/reentrantLock.md | 117 ++++++++++++++---------- 4 files changed, 185 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 2e9c7b692..49c419a1e 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ - [深入浅出偏向锁](docs/thread/pianxiangsuo.md) - [CAS详解](docs/thread/cas.md) - [AQS详解](docs/thread/aqs.md) -- [锁分类及 JUC 包下的那些锁](docs/thread/lock.md) +- [锁分类和 JUC](docs/thread/lock.md) - [重入锁ReentrantLock](docs/thread/reentrantLock.md) - [读写锁ReentrantReadWriteLock](docs/thread/ReentrantReadWriteLock.md) - [协作类Condition](docs/thread/condition.md) diff --git a/docs/home.md b/docs/home.md index 14551fcd4..d97f9b8ec 100644 --- a/docs/home.md +++ b/docs/home.md @@ -266,7 +266,7 @@ head: - [深入浅出偏向锁](thread/pianxiangsuo.md) - [CAS详解](thread/cas.md) - [AQS详解](thread/aqs.md) -- [锁分类及 JUC 包下的那些锁](thread/lock.md) +- [锁分类和 JUC](thread/lock.md) - [重入锁ReentrantLock](thread/reentrantLock.md) - [读写锁ReentrantReadWriteLock](thread/ReentrantReadWriteLock.md) - [协作类Condition](thread/condition.md) diff --git a/docs/thread/lock.md b/docs/thread/lock.md index 405b5971e..f7a7908ae 100644 --- a/docs/thread/lock.md +++ b/docs/thread/lock.md @@ -1,6 +1,6 @@ --- title: 锁分类以及 JUC 包下的锁介绍,一网打尽 -shortTitle: 锁分类及 JUC 包下的那些锁 +shortTitle: 锁分类和 JUC description: Java的并发包(java.util.concurrent,简称JUC)提供了许多并发工具类,包括一些用于并发编程的锁。 category: - Java核心 @@ -12,19 +12,19 @@ head: content: Java,并发编程,多线程,Thread,锁,JUC,ReentrantLock,StampedLock,ReadWriteLock,Condition,锁分类 --- -# 第十四节:锁分类及 JUC 包下的那些锁 +# 第十四节:锁分类和 JUC -前面我们介绍了 Java 原生的锁——基于对象的锁,它一般是配合 [synchronized 关键字](https://javabetter.cn/thread/synchronized-1.html)来使用的。实际上,Java 在`java.util.concurrent.locks`包下,还为我们提供了几个关于锁的类和接口。它们有更强大的功能或更高的性能。 +前面我们介绍了基于对象的原生锁——[synchronized](https://javabetter.cn/thread/synchronized-1.html),实际上,Java 在`java.util.concurrent`(JUC)包下,还为我们提供了更多的锁类和锁接口(尤其是子包 locks 下),它们有更强大的功能或更牛逼的性能。 -## synchronized 的不足之处 - -我们先来看看`synchronized`有什么不足之处。 +来看看`synchronized`的不足之处吧。 - 如果临界区是只读操作,其实可以多线程一起执行,但使用 synchronized 的话,**同一时间只能有一个线程执行**。 -- synchronized 无法知道线程有没有成功获取到锁 +- synchronized 无法知道线程有没有成功获取到锁。 - 使用 synchronized,如果临界区因为 IO 或者 sleep 方法等原因阻塞了,而当前线程又没有释放锁,就会导致**所有线程等待**。 -而这些都是 locks 包下的锁可以解决的。 +>临界区(Critical Section)是多线程中一个 非常重要的概念,指的是在代码中访问共享资源的那部分,且同一时刻只能有一个线程能访问的代码。多个线程同时访问临界区的资源如果没有任何同步(加锁)操作,会导致资源的状态不可预测和不一致,从而产生所谓的“竞态条件”(Race Condition)。在许多并发控制策略中,例如互斥锁 synchronized,目标就是确保任何时候只有一个线程进入临界区。 + +不过,synchronized 的这些不足之处都可以通过 JUC 包下的其他锁来弥补,下面先来看一下锁的分类吧。 ## 锁的几种分类 @@ -34,13 +34,13 @@ Java 提供了种类丰富的锁,每种锁因其特性的不同,在适当的 ### 乐观锁 VS 悲观锁 -乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在 Java 和数据库中都有此概念对应的实际应用。 +乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。 -先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java 中,[synchronized 关键字](https://javabetter.cn/thread/synchronized-1.html)和[Lock 的实现类](https://javabetter.cn/thread/lock.html)都是悲观锁。 +先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java 中,[synchronized 关键字](https://javabetter.cn/thread/synchronized-1.html) 是最典型的悲观锁。 而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会加锁,只是在更新数据的时候会去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。 -乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是[CAS 算法](https://javabetter.cn/thread/cas.html),[Java 原子类](https://javabetter.cn/thread/atomic.html)中的递增操作就通过 CAS 自旋实现的。 +乐观锁在 Java 中是通过无锁编程来实现的,最常采用的是[CAS 算法](https://javabetter.cn/thread/cas.html),[Java 原子类](https://javabetter.cn/thread/atomic.html)的递增操作就通过 CAS 自旋实现的。 ![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/other-bukfsdjavassmtjstd-840de182-83e2-4639-868a-bd5cc984575f.png) @@ -52,27 +52,29 @@ Java 提供了种类丰富的锁,每种锁因其特性的不同,在适当的 光说概念有些抽象,我们来看下乐观锁和悲观锁的调用方式: ```Java -// ------------------------- 悲观锁的调用方式 ------------------------- +// --------- 悲观锁的调用方式 ------------------------- // synchronized public synchronized void testMethod() { // 操作同步资源 } // ReentrantLock -private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁 +private ReentrantLock lock = new ReentrantLock(); +// 需要保证多个线程使用的是同一个锁 public void modifyPublicResources() { lock.lock(); // 操作同步资源 lock.unlock(); } -// ------------------------- 乐观锁的调用方式 ------------------------- -private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保证多个线程使用的是同一个AtomicInteger +// --------- 乐观锁的调用方式 ------------------------- +private AtomicInteger atomicInteger = new AtomicInteger(); +// 需要保证多个线程使用的是同一个AtomicInteger atomicInteger.incrementAndGet(); //执行自增1 ``` -通过调用方式的举例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们这里再次来温习一下 [“CAS” 的技术原理](https://javabetter.cn/thread/cas.html),之前也讲过,就当是复习了。 +通过调用方式的举例,我们发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们这里再次来温习一下 [“CAS” 的技术原理](https://javabetter.cn/thread/cas.html),之前也讲过,就当是复习了。 -CAS 全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。`java.util.concurrent`包中的原子类就是通过 CAS 实现的乐观锁。 +CAS 是一种无锁算法,可以在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。JUC 包中的[原子类](https://javabetter.cn/thread/atomic.html)(后面会细讲,戳链接直达)就是通过 CAS 实现的乐观锁。 CAS 算法涉及到三个操作数: @@ -82,7 +84,7 @@ CAS 算法涉及到三个操作数: 当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。 -之前提到`java.util.concurrent`包中的原子类,就是通过 CAS 实现的乐观锁,那么我们进入原子类 AtomicInteger 的源码,来看一下 AtomicInteger 的定义: +之前提到 JUC 包中的原子类,就是通过 CAS 实现的乐观锁,那么我们进入原子类 AtomicInteger 的源码(后面也会细讲,既然讲到了,这里就过一下吧),来看一下 AtomicInteger 的定义: ![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/other-bukfsdjavassmtjstd-86e17b45-2993-48df-b7cd-ee86bb15c922.png) @@ -92,7 +94,7 @@ CAS 算法涉及到三个操作数: - valueOffset: 存储 value 在 AtomicInteger 中的偏移量。 - value: 存储 AtomicInteger 的 int 值,该属性需要借助 volatile 关键字保证其在线程间是可见的。 -接下来,我们查看 AtomicInteger 的自增方法`incrementAndGet()`,发现自增方法底层调用的是`unsafe.getAndAddInt()`。但是由于 JDK 本身只有 Unsafe.class,通过 class 文件中的参数名,并不能很好地了解方法的作用,所以我们通过 OpenJDK 8 来查看 Unsafe 的源码: +接下来,我们查看 AtomicInteger 的自增方法`incrementAndGet()`,发现自增方法底层调用的是`unsafe.getAndAddInt()`。但是由于 JDK 本身只有 Unsafe.class,通过 class 文件中的参数名,并不能很好地了解方法的作用,所以我们通过 OpenJDK 8 来查看 [Unsafe](https://javabetter.cn/thread/Unsafe.html)(后面也会讲,戳链接直达) 的源码: ```Java // ------------------------- JDK 8 ------------------------- @@ -121,17 +123,17 @@ public final int getAndAddInt(Object o, long offset, int delta) { } ``` -根据 OpenJDK 8 的源码我们可以看出,`getAndAddInt()`循环获取给定对象 o 中的偏移量处的值 v,然后判断内存值是否等于 v。如果相等则将内存值设置为 v + delta,否则返回 false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。 +根据源码我们可以看出,`getAndAddInt()`循环获取给定对象 o 中偏移量处的值 v,然后判断内存值是否等于 v。如果相等则将内存值设置为 v + delta,否则返回 false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。 整个“比较+更新”操作都封装在`compareAndSwapInt()`中,在 JNI 里是借助于一个 CPU 指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。 -后续 JDK 通过 CPU 的 cmpxchg 指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过 Java 代码中的 while 循环再次调用 cmpxchg 指令进行重试,直到设置成功为止。 +>Java Native Interface(JNI)是Java与本地代码(如C、C++)之间的 桥梁。它允许Java代码与原生应用程序的接口(API)和本地库进行交互,并获得一些Java不能轻松完成任务的能力。 + +后续 JDK 通过 CPU 的 cmpxchg 指令去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过 Java 代码中的 while 循环再次调用 cmpxchg 指令进行重试,直到设置成功为止。 -CAS 虽然很高效,但是它也存在三大问题,[我们前面也讲过](https://javabetter.cn/thread/cas.html),不知道大家还记得不: +>CMPXCHG是“Compare and Exchange”的缩写,它是一种原子指令,用于在多核/多线程环境中安全地修改共享数据。CMPXCHG在很多现代微处理器体系结构中都有,例如Intel x86/x64体系。对于32位操作数,这个指令通常写作CMPXCHG,而在64位操作数中,它被称为CMPXCHG8B或CMPXCHG16B。 -1. **ABA 问题**。CAS 需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是 A,后来变成了 B,然后又变成了 A,那么 CAS 进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA 问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。JDK 从 1.5 开始提供了 AtomicStampedReference 类来解决 ABA 问题,具体操作封装在`compareAndSet()`中。`compareAndSet()`首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。 -2. **循环时间长开销大**。CAS 操作如果长时间不成功,会导致其一直自旋,给 CPU 带来非常大的开销。 -3. **只能保证一个共享变量的原子操作**。对一个共享变量执行操作时,CAS 能够保证原子操作,但是对多个共享变量操作时,CAS 是无法保证操作的原子性的。Java 从 1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行 CAS 操作。 +CAS 虽然高效,但也存在三大问题,[我们前面也讲过](https://javabetter.cn/thread/cas.html),不知道大家还记得不,如果不记得,可以戳链接去回顾一波(dogdogdog)。 ### 自旋锁 VS 适应性自旋锁 @@ -143,7 +145,7 @@ CAS 虽然很高效,但是它也存在三大问题,[我们前面也讲过](h ![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/other-bukfsdjavassmtjstd-be0964a8-856a-45c9-ab75-ce9505c2e237.png) -自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是 10 次,可以使用-XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。 +自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是 10 次,可以使用`-XX:PreBlockSpin` 来更改)没有成功获得锁,就应当挂起线程。 自旋锁的实现原理同样也是 CAS,AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。 @@ -153,13 +155,13 @@ CAS 虽然很高效,但是它也存在三大问题,[我们前面也讲过](h 自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功的,进而它将允许自旋等待更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。 -### 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁 +### 无锁偏向锁轻量级锁重量级锁 -这四种锁是指锁的状态,专门针对 synchronized 的。我们在[synchronized 锁的到底是什么](https://javabetter.cn/thread/synchronized.html)一文中已经详细地介绍过,这里就不再赘述了。 +这四种锁是专门针对 synchronized 的,我们在[synchronized 锁的到底是什么](https://javabetter.cn/thread/synchronized.html)一文中已经详细地介绍过,这里就不再赘述了。 ### 可重入锁和非可重入锁 -可重入锁又名递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提:锁的是同一个对象或者 class),不会因为之前已经获取过还没释放而阻塞。Java 中[ReentrantLock](https://javabetter.cn/thread/reentrantLock.html)和[synchronized](https://javabetter.cn/thread/synchronized-1.html)都是可重入锁,可重入锁的一个优点就是可以一定程度避免死锁。下面用示例代码来进行分析: +可重入锁又名递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提:锁的是同一个对象或者 class),不会因为之前已经获取过还没释放而阻塞。Java 中[ReentrantLock](https://javabetter.cn/thread/reentrantLock.html)(后面会细讲,戳链接直达)和[synchronized](https://javabetter.cn/thread/synchronized-1.html)都是可重入锁,可重入锁的一个优点就是可以一定程度避免死锁。下面用示例代码来进行分析: ```Java public class Widget { @@ -206,33 +208,33 @@ public class Widget { 一般情况下,**非公平锁能提升一定的效率。但是非公平锁可能会发生线程饥饿(有一些线程长时间得不到锁)的情况**。所以要根据实际的需求来选择非公平锁和公平锁。 -ReentrantLock 支持非公平锁和公平锁两种。 +[ReentrantLock](https://javabetter.cn/thread/reentrantLock.html) 支持非公平锁和公平锁两种。 ### 读写锁和排它锁 -我们前面讲到的 [synchronized](https://javabetter.cn/thread/synchronized.html) 用的锁和 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html),其实都是“排它锁”。也就是说,这些锁在同一时刻只允许一个线程进行访问。 +我们前面讲到的 [synchronized](https://javabetter.cn/thread/synchronized.html) 和后面要讲的 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html),其实都是“排它锁”。也就是说,这些锁在同一时刻只允许一个线程进行访问。 -而读写锁可以在同一时刻允许多个读线程访问。Java 提供了 [ReentrantReadWriteLock](https://javabetter.cn/thread/ReentrantReadWriteLock.html) 类作为读写锁的默认实现,内部维护了两个锁:一个读锁,一个写锁。通过分离读锁和写锁,使得在“读多写少”的环境下,大大地提高了性能。 +而读写锁可以在同一时刻允许多个读线程访问。Java 提供了 [ReentrantReadWriteLock](https://javabetter.cn/thread/ReentrantReadWriteLock.html)(后面会细讲,戳链接直达)类作为读写锁的默认实现,内部维护了两个锁:一个读锁,一个写锁。通过分离读锁和写锁,使得在“读多写少”的环境下,大大地提高了性能。 > 注意,即使用读写锁,在写线程访问时,所有的读线程和其它写线程均被阻塞。 -排它锁也叫独享锁,如果线程 T 对数据 A 加上排它锁后,则其他线程不能再对 A 加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。 +排它锁也叫独享锁,如果线程 T 对数据 A 加上排它锁后,则其他线程不能再对 A 加任何类型的锁。获得排它锁的线程既能读数据又能修改数据。 与之对应的,就是共享锁,指该锁可被多个线程所持有。如果线程 T 对数据 A 加上共享锁后,则其他线程只能对 A 再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。 独享锁与共享锁也是通过[AQS](https://javabetter.cn/thread/aqs.html)来实现的,通过实现不同的方法,来实现独享或者共享。 -下图为 ReentrantReadWriteLock 的部分源码(后面也会细讲): +下图为 ReentrantReadWriteLock 的部分源码: ![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/other-bukfsdjavassmtjstd-baa93e76-ac90-4955-8955-50dabc6efbdd.png) -我们看到[ReentrantReadWriteLock](https://javabetter.cn/thread/ReentrantReadWriteLock.html)有两把锁:ReadLock 和 WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现 ReadLock 和 WriteLock 是靠内部类 Sync 实现的锁。Sync 是 AQS 的一个子类,这种结构在[CountDownLatch](https://javabetter.cn/thread/CountDownLatch.html)、[ReentrantLock](https://javabetter.cn/thread/reentrantLock.html)、Semaphore 里面也都存在。 +我们看到[ReentrantReadWriteLock](https://javabetter.cn/thread/ReentrantReadWriteLock.html)有两把锁:ReadLock 和 WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现 ReadLock 和 WriteLock 是靠内部类 Sync 实现的锁。Sync 是 AQS 的一个子类,这种结构在[CountDownLatch、Semaphore](https://javabetter.cn/thread/CountDownLatch.html)(后面会细讲,戳链接直达)、[ReentrantLock](https://javabetter.cn/thread/reentrantLock.html)(接下来会讲,戳链接直达)里面也都存在。 在 ReentrantReadWriteLock 里面,读锁和写锁的锁主体都是 Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以 ReentrantReadWriteLock 的并发性相比一般的互斥锁有了很大提升。 那读锁和写锁的具体加锁方式有什么区别呢? -在了解源码之前我们需要回顾一下其他知识。 在最开始提及 AQS 的时候我们也提到了 state 字段(int 类型,32 位),该字段用来描述有多少线程持有锁。 +在了解源码之前我们需要回顾一下其他知识。 在最开始提及 [AQS](https://javabetter.cn/thread/aqs.html) 的时候我们也提到了 state 字段(int 类型,32 位),该字段用来描述有多少线程持有锁。 在独享锁中,这个值通常是 0 或者 1(如果是重入锁的话 state 值就是重入的次数),在共享锁中 state 就是持有锁的数量。但是在 ReentrantReadWriteLock 中有读、写两把锁,所以需要在一个整型变量 state 上分别描述读锁和写锁的数量(或者也可以叫状态)。 @@ -314,23 +316,23 @@ protected final int tryAcquireShared(int unused) { 我们发现在 ReentrantLock 虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。根据源码所示,当某一个线程调用 lock 方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用 CAS 更新 state 成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定 ReentrantLock 无论读操作还是写操作,添加的锁都是都是独享锁。 -**综上,只有 synchronized 是远远不能满足多样化的业务对锁的要求的**。接下来我们介绍一下 JDK 中有关锁的一些接口和类。 +**综上,只有 synchronized 是远远不能满足多样化业务对锁的要求的**。接下来我们介绍一下 JDK 中有关锁的一些接口和类。 -## JUC 包下的那些锁 +## JUC 包下的锁 -众所周知,JDK 中关于并发的类大多都在`java.util.concurrent`(以下简称 JUC)包下。 +众所周知,JDK 中关于并发的类大多都在 JUC 包下。 ![](https://cdn.tobebetterjavaer.com/stutymore/lock-20230806103310.png) -而 juc.locks 包看名字就知道,是提供了一些并发锁的工具类的。前面我们介绍的 [AQS(AbstractQueuedSynchronizer)](https://javabetter.cn/thread/aqs.html)就是在这个包下。 +看名字就知道,locks 包是提供一些并发锁的工具类的。前面我们介绍的 [AQS(AbstractQueuedSynchronizer)](https://javabetter.cn/thread/aqs.html)就是在这个包下。 ### 抽象类 AQS/AQLS/AOS 这三个抽象类有一定的关系,所以这里放到一起讲。 -首先我们来看**AQS**(AbstractQueuedSynchronizer),之前[专门介绍过这个类](https://javabetter.cn/thread/aqs.html),它是在 JDK 1.5 发布的,提供了一个“队列同步器”的基本功能实现。 +首先我们来看**AQS**(AbstractQueuedSynchronizer),它是在 JDK 1.5 发布的,提供了一个“队列同步器”的基本功能实现。 -AQS 里面的“资源”是用一个`int`类型的数据来表示的,有时候我们的业务需求资源的数量超出了`int`的范围,所以在 JDK 1.6 中,多了一个**AQLS**(AbstractQueuedLongSynchronizer)。它的代码跟 AQS 几乎一样,只是把资源的类型变成了`long`类型。 +AQS 里面的“资源”是用一个`int`类型的数据来表示的,有时候业务需求的资源数超出了`int`的范围,所以在 JDK 1.6 中,多了一个**AQLS**(AbstractQueuedLongSynchronizer)。它的代码跟 AQS 几乎一样,只是把资源的类型变成了`long`类型。 ![](https://cdn.tobebetterjavaer.com/stutymore/lock-20230805213746.png) @@ -357,7 +359,7 @@ protected final Thread getExclusiveOwnerThread() { ### 接口 Condition/Lock/ReadWriteLock -juc.locks 包下共有三个接口:`Condition`、`Lock`、`ReadWriteLock`。 +locks 包下共有三个接口:`Condition`、`Lock`、`ReadWriteLock`。 其中,Lock 和 ReadWriteLock 从名字就可以看得出来,分别是锁和读写锁的意思。Lock 接口里面有一些获取锁和释放锁的方法声明,而 ReadWriteLock 里面只有两个方法,分别返回“读锁”和“写锁”: @@ -368,15 +370,15 @@ public interface ReadWriteLock { } ``` -Lock 接口中有一个方法是可以获得一个[Condition](https://javabetter.cn/thread/condition.html): +Lock 接口中有一个方法可以获得一个[Condition](https://javabetter.cn/thread/condition.html)(后面会细讲,戳链接直达): ```java Condition newCondition(); ``` -之前我们提到过每个对象都可以用继承自`Object`的**wait/notify**方法来实现**等待/通知机制**。而 Condition 接口也提供了类似 Object 监视器的方法,通过与**Lock**配合来实现等待/通知模式。 +之前我们提到过每个对象都可以用`Object`的**wait/notify**方法来实现**等待/通知机制**。而 Condition 接口也提供了类似 Object 的方法,可以配合**Lock**来实现等待/通知模式。 -那为什么既然有 Object 的监视器方法了,还要用 Condition 呢?这里有一个二者简单的对比: +既然有 Object 的监视器方法了,为什么还要用 Condition 呢?这里有一个简单的对比: | 对比项 | Object 监视器 | Condition | | ---------------------------------------------- | -------------------------------- | ----------------------------------------------------------------- | @@ -401,9 +403,9 @@ Condition 和 Object 的 wait/notify 基本相似。其中,Condition 的 await | signal() | 唤醒一个等待在 Condition 上的线程,被唤醒的线程在方法返回前必须获得与 Condition 对象关联的锁 | | signalAll() | 唤醒所有等待在 Condition 上的线程,能够从 await()等方法返回的线程必须先获得与 Condition 对象关联的锁 | -### ReentrantLock +### 可重入锁ReentrantLock -[ReentrantLock](https://javabetter.cn/thread/reentrantLock.html) 是 Lock 接口的默认实现,实现了锁的基本功能。 +[ReentrantLock](https://javabetter.cn/thread/reentrantLock.html)(接下来细讲,戳链接直达)是 Lock 接口的默认实现,实现了锁的基本功能。 从名字上看,它是一个“可重入”锁,从源码上看,它内部有一个抽象类`Sync`,继承了 [AQS](https://javabetter.cn/thread/aqs.html),自己实现了一个同步器。 @@ -467,11 +469,11 @@ public class Counter { ![](https://cdn.tobebetterjavaer.com/stutymore/lock-20230806103823.png) -### ReentrantReadWriteLock +### 读写锁ReentrantReadWriteLock -ReentrantReadWriteLock 是 ReadWriteLock 接口的默认实现。它与 ReentrantLock 的功能类似,同样是可重入的,支持非公平锁和公平锁。不同的是,它还支持”读写锁“。 +[ReentrantReadWriteLock](https://javabetter.cn/thread/ReentrantReadWriteLock.html)(后面会细讲,戳链接直达) 是 ReadWriteLock 接口的默认实现。它与 ReentrantLock 的功能类似,同样是可重入的,支持非公平锁和公平锁。不同的是,它还支持”读写锁“。 -[ReentrantReadWriteLock](https://javabetter.cn/thread/ReentrantReadWriteLock.html) 内部的结构大概是这样: +ReentrantReadWriteLock 内部的结构大概是这样: ```java // 内部结构 @@ -606,7 +608,7 @@ public class SharedResource { ReentrantReadWriteLock 实现了读写锁,但它有一个小弊端,就是在“写”操作的时候,其它线程不能写也不能读。我们称这种现象为“写饥饿”,将在下文的 StampedLock 类继续讨论这个问题。 -### StampedLock +### 锁王StampedLock `StampedLock` 类是 Java 8 才发布的,也是 Doug Lea 大神所写,有人称它为锁的性能之王。 @@ -711,11 +713,9 @@ StampedLock 用这个 long 类型的变量的前 7 位(LG_READERS)来表示 乐观读锁就比较简单了,并没有真正改变 state 的值,而是在获取锁的时候记录 state 的写状态,在操作完成后去检查 state 的写状态部分是否发生变化,上文提到了,每次写锁都会留下痕迹,也是为了这里乐观锁检查变化提供方便。 -总的来说,StampedLock 的性能是非常优异的,基本上可以取代 ReentrantReadWriteLock 的作用。 +总的来说,StampedLock 的性能是非常优异的,基本上可以取代 ReentrantReadWriteLock。我们来一个 StampedLock 和 ReentrantReadWriteLock 的对比使用示例。 -我们来一个 StampedLock 和 ReentrantReadWriteLock 的对比使用示例。 - -使用 ReentrantReadWriteLock。 +ReentrantReadWriteLock: ```java public class SharedResourceWithReentrantReadWriteLock { @@ -763,6 +763,59 @@ public class SharedResourceWithReentrantReadWriteLock { } ``` +StampedLock: + +```java +public class SharedResourceWithStampedLock { + private final StampedLock sl = new StampedLock(); + private int data = 0; + + public void write(int value) { + long stamp = sl.writeLock(); + try { + data = value; + } finally { + sl.unlockWrite(stamp); + } + } + + public int read() { + long stamp = sl.tryOptimisticRead(); + int currentData = data; + if (!sl.validate(stamp)) { + stamp = sl.readLock(); + try { + currentData = data; + } finally { + sl.unlockRead(stamp); + } + } + return currentData; + } + + public static void main(String[] args) { + SharedResourceWithStampedLock sharedResource = new SharedResourceWithStampedLock(); + + Thread writer = new Thread(() -> { + for (int i = 0; i < 5; i++) { + sharedResource.write(i); + System.out.println("Write: " + i); + } + }); + + Thread reader = new Thread(() -> { + for (int i = 0; i < 5; i++) { + int value = sharedResource.read(); + System.out.println("Read: " + value); + } + }); + + writer.start(); + reader.start(); + } +} +``` + 来看一下输出结果的对比。 ![](https://cdn.tobebetterjavaer.com/stutymore/lock-20230806105654.png) @@ -777,9 +830,9 @@ public class SharedResourceWithReentrantReadWriteLock { 综上所述,StampedLock 提供了更高的性能和灵活性,但也带来了更复杂的使用方式。ReentrantReadWriteLock 则相对简单和直观,特别适用于没有高并发读的场景。 -## 其他工具类 +## JUC 包下的其他工具类 -locks 包下的锁接口和锁类介绍完了,我们这里再讲一些 JUC 包下的其他工具类,比如 Condition、Semaphore、CountDownLatch、CyclicBarrier 等。 +locks 包下的锁接口和锁类介绍完了,我们这里再讲一些 JUC 包下的其他工具类,比如 Semaphore、CountDownLatch、CyclicBarrier、Exchanger、Phaser 等(这些在[通信工具类](https://javabetter.cn/thread/CountDownLatch.html)中也会细讲)。 ### Semaphore diff --git a/docs/thread/reentrantLock.md b/docs/thread/reentrantLock.md index ac4f2903d..d4f30ba7b 100644 --- a/docs/thread/reentrantLock.md +++ b/docs/thread/reentrantLock.md @@ -119,7 +119,7 @@ protected final boolean tryAcquire(int acquires) { ## ReentrantLock 的使用 -ReentrantLock 的使用方式与 synchronized 关键字类似,都是通过加锁和释放锁来实现同步的。我们来看看 ReentrantLock 的使用方式,以非公平锁为例: +ReentrantLock 的使用方式与 [synchronized](https://javabetter.cn/thread/synchronized-1.html) 关键字类似,都是通过加锁和释放锁来实现同步的。我们来看看 ReentrantLock 的使用方式,以非公平锁为例: ```java public class ReentrantLockTest { @@ -181,70 +181,97 @@ private static final ReentrantLock lock = new ReentrantLock(true); 需要注意的是,使用 ReentrantLock 时,必须在 finally 块中手动释放锁。 -## Condition 接口 +## ReentrantLock 与 synchronized -[Condition 接口](https://javabetter.cn/thread/condition.html)是与 Lock 绑定的,可以理解为一个 Lock 对象可以绑定多个 Condition 对象,Condition 接口提供了类似于 Object 的 wait、notify、notifyAll 等方法,与 Lock 一起使用可以实现等待/通知模式,比如实现一个阻塞队列: +ReentrantLock 与 synchronized 关键字都是用来实现同步的,那么它们之间有什么区别呢?我们来看看它们的对比: + +- **ReentrantLock 是一个类,而 synchronized 是 Java 中的关键字**; +- **ReentrantLock 可以实现选择性通知(可以绑定多个 [Condition](https://javabetter.cn/thread/condition.html)(后面会细讲,戳链接直达)),而 synchronized 只能唤醒一个线程或者唤醒全部线程**; +- **ReentrantLock 是可重入锁,而 synchronized 不是**; +- ReentrantLock 必须手动释放锁。通常需要在 finally 块中调用 unlock 方法以确保锁被正确释放。synchronized 会自动释放锁,当同步块执行完毕时,由 JVM 自动释放,不需要手动操作。 +- ReentrantLock: 通常提供更好的性能,特别是在高竞争环境下。synchronized: 在某些情况下,性能可能稍差一些,但随着 JDK 版本的升级,性能差距已经不大了。 + +以下是一个简单的性能比较demo: ```java -public class BlockingQueue { - private final Lock lock = new ReentrantLock(); - private final Condition notFull = lock.newCondition(); - private final Condition notEmpty = lock.newCondition(); - private final Object[] items = new Object[100]; - private int putptr, takeptr, count; - - public void put(T t) throws InterruptedException { +import java.util.concurrent.locks.ReentrantLock; + +public class PerformanceTest { + private static final int NUM_THREADS = 10; + private static final int NUM_INCREMENTS = 1_000_000; + + private int count1 = 0; + private int count2 = 0; + + private final ReentrantLock lock = new ReentrantLock(); + private final Object syncLock = new Object(); + + public void increment1() { lock.lock(); try { - while (count == items.length) { - notFull.await(); - } - items[putptr] = t; - if (++putptr == items.length) { - putptr = 0; - } - ++count; - notEmpty.signal(); + count1++; } finally { lock.unlock(); } } - public T take() throws InterruptedException { - lock.lock(); - try { - while (count == 0) { - notEmpty.await(); - } - Object x = items[takeptr]; - if (++takeptr == items.length) { - takeptr = 0; - } - --count; - notFull.signal(); - return (T) x; - } finally { - lock.unlock(); + public void increment2() { + synchronized (syncLock) { + count2++; } } + + public static void main(String[] args) throws InterruptedException { + PerformanceTest test = new PerformanceTest(); + + // Test ReentrantLock + long startTime = System.nanoTime(); + Thread[] threads = new Thread[NUM_THREADS]; + for (int i = 0; i < NUM_THREADS; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < NUM_INCREMENTS; j++) { + test.increment1(); + } + }); + threads[i].start(); + } + for (Thread thread : threads) { + thread.join(); + } + long endTime = System.nanoTime(); + System.out.println("ReentrantLock time: " + (endTime - startTime) + " ns"); + + // Test synchronized + startTime = System.nanoTime(); + for (int i = 0; i < NUM_THREADS; i++) { + threads[i] = new Thread(() -> { + for (int j = 0; j < NUM_INCREMENTS; j++) { + test.increment2(); + } + }); + threads[i].start(); + } + for (Thread thread : threads) { + thread.join(); + } + endTime = System.nanoTime(); + System.out.println("synchronized time: " + (endTime - startTime) + " ns"); + } } ``` -代码很简单,就是一个阻塞队列的实现,put 方法用来向队列中添加元素,take 方法用来从队列中获取元素。我们来看看 put 方法的实现,首先获取锁,然后判断队列是否已满,如果已满则调用 `notFull.await()` 方法阻塞当前线程,直到队列不满,然后将元素添加到队列中,最后调用 `notEmpty.signal()` 方法唤醒一个等待的线程。take 方法的实现与 put 方法类似,不再赘述。 +来看输出结果: -## 与 synchronized 关键字的比较 - -ReentrantLock 与 synchronized 关键字都是用来实现同步的,那么它们之间有什么区别呢?我们来看看它们的对比: +``` +ReentrantLock time: 269913857 ns +synchronized time: 350595013 ns +``` -- **ReentrantLock 是一个类,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现**; -- **ReentrantLock 可以实现选择性通知(锁可以绑定多个 Condition),而 synchronized 只能唤醒一个线程或者唤醒全部线程**; -- **ReentrantLock 是可重入锁,而 synchronized 不是**; -- ReentrantLock 必须手动释放锁。通常需要在 finally 块中调用 unlock 方法以确保锁被正确释放。synchronized: 自动释放锁。当同步块执行完毕时,JVM 会自动释放锁,不需要手动操作。 -- ReentrantLock: 通常提供更好的性能,特别是在高竞争环境下。ynchronized: 在某些情况下,性能可能稍差一些,但在现代 JVM 实现中,性能差距通常不大。 +这个测试在两种锁机制下尝试执行多次增量操作,然后测量所需的时间。 ## 小结 -本篇主要介绍了 ReentrantLock 的实现原理,以及与 synchronized 关键字的比较。ReentrantLock 是一个类,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。ReentrantLock 可以实现选择性通知(锁可以绑定多个 Condition),而 synchronized 只能唤醒一个线程或者唤醒全部线程。ReentrantLock 是可重入锁,而 synchronized 不是。ReentrantLock 必须手动释放锁。通常需要在 finally 块中调用 unlock 方法以确保锁被正确释放。synchronized: 自动释放锁。当同步块执行完毕时,JVM 会自动释放锁,不需要手动操作。ReentrantLock: 通常提供更好的性能,特别是在高竞争环境下。ynchronized: 在某些情况下,性能可能稍差一些,但在现代 JVM 实现中,性能差距通常不大。 +本篇主要介绍了 ReentrantLock 的实现原理,以及与 synchronized 关键字的比较。 >编辑:沉默王二,编辑前的内容主要来自于CL0610的 GitHub 仓库[https://github.com/CL0610/Java-concurrency](https://github.com/CL0610/Java-concurrency/blob/master/10.彻底理解ReentrantLock/彻底理解ReentrantLock.md) -- GitLab