重新学习Java线程原语

news/2025/2/5 17:38:23/

Synchronized曾经是一个革命性的技术,在当前仍然有重要的用途。但是,现在是时候转向更新的Java线程原语,同时重新考虑我们的核心逻辑。

自从Java第一个测试版以来,我就一直在使用它。从那时起,线程就是我最喜欢的特性之一。Java是第一种在编程语言本身中引入线程支持的语言。那是一个具有争议的决定。在过去的十年中,每种编程语言都竞相引入async/await,甚至Java也有一些第三方支持…但是Java选择了引入更优越的虚拟线程(Loom项目)。本文并不讨论这个问题。

我觉得这很好,证明了Java的核心实力。Java不仅仅是一种语言,还是一种文化。这种文化注重深思熟虑的变革,而不是盲目跟随时尚潮流。

在本文中,我想重新探讨Java中的线程编程旧方法。我习惯使用synchronized、wait、notify等技术。但是, “然而,这些方法已经不再是Java中线程处理的最佳方式。 我也是问题的一部分。我还是习惯于使用这些技术,发现很难适应自Java 5以来就存在的一些API。这是一种习惯的力量。 虽然可以讨论许多处理线程的出色API,但我想在这里专注讨论锁,因为它们是基础但极为重要的。

Synchronized 与 ReentrantLock

我犹豫放弃使用 synchronized 的原因是,并没有更好的替代方案。现在弃用 synchronized 的主要原因是,它可能会在 Loom 中触发线程固定,这并不理想。JDK 21 可能会修复这个问题(当 Loom 正式发布时),但还有一些理由弃用它。

synchronized 的直接替代品是 ReentrantLock。不幸的是,ReentrantLock 相比 synchronized 很少有优势,因此迁移的好处最多是存疑的。事实上,它有一个主要的缺点。为了了解这一点,让我们看一个例子。下面是我们如何使用 synchronized:

synchronized(LOCK) {// safe code
}LOCK.lock();
try {// safe code
} finally {LOCK.unlock();
}

ReentrantLock 的第一个缺点是冗长。我们需要try块,因为如果在块内部发生异常,锁将保持。而 synchronized 则会自动处理异常。

有些人会使用 AutoClosable 对锁进行封装,大概是这样的:

public class ClosableLock implements AutoCloseable {private final ReentrantLock lock;public ClosableLock() {this.lock = new ReentrantLock();}public ClosableLock(boolean fair) {this.lock = new ReentrantLock(fair);}@Overridepublic void close() throws Exception {lock.unlock();}public ClosableLock lock() {lock.lock();return this;}public ClosableLock lockInterruptibly() throws InterruptedException {lock.lock();return this;}public void unlock() {lock.unlock();}
}

注意,我没有实现 Lock 接口,这本来是最理想的。这是因为 lock 方法返回了可自动关闭的实现,而不是 void。

一旦我们这样做了,我们就可以编写更简洁的代码,比如这样:

try(LOCK.lock()) {// safe code
}

我喜欢代码更简洁的写法,但是这个方法存在一些问题,因为 try-with-resource 语句是用于清理资源的,而我们正在重复使用锁对象。虽然调用了 close 方法,但是我们会再次在同一个对象上调用它。我认为,将 try-with-resource 语法扩展到支持锁接口可能是个好主意。但在此之前,这个技巧可能不值得采用。

ReentrantLock 的优势

使用ReentrantLock的最大原因是Loom支持。其他的优点也不错,但没有一个是“杀手级功能”。

我们可以在方法之间使用它,而不是在一个连续的代码块中使用。但是这可能不是一个好主意,因为你希望尽量减少锁定区域,并且失败可能会成为一个问题。我不认为这个特性是一个优点。

ReentrantLock提供了公平锁(fairness)的选项。这意味着它会先服务于最先停在锁上的线程。我试图想到一个现实而简单的使用案例,但却无从下手。如果您正在编写一个复杂的调度程序,并且有许多线程不断地排队等待资源,您可能会发现一个线程由于其他线程不断到来而被“饥饿”。但是,这种情况可能更适合使用并发包中的其他选项。也许我漏掉了什么……

lockInterruptibly() 方法允许我们在线程等待锁时中断它。这是一个有趣的特性,但是很难找到一个真正实际应用场景。如果你编写的代码需要非常快速响应中断,你需要使用 lockInterruptibly() API 来获得这种能力。但是,你通常在 lock()方法内部花费多长时间呢?

这种情况可能只在极端情况下才会有影响,大多数人在编写高级多线程代码时可能不会遇到这种情况。

ReadWriteReentrantLock

更好的方法是使用ReadWriteReentrantLock。大多数资源都遵循频繁读取、少量写入的原则。由于读取变量是线程安全的,除非正在写入变量,否则没有必要加锁。这意味着我们可以将读取操作进行极致优化,同时稍微降低写操作的速度。

假设这是你的使用情况,你可以创建更快的代码。使用读写锁时,我们有两个锁,一个读锁,如下图所示。它允许多个线程通过,实际上是“自由竞争”的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AKAo4ONZ-1682060926379)(https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/3e5386d23fcc40138935e3504916f5e5~noop.image?_iz=58558&from=article.pc_detail&x-expires=1682581512&x-signature=tmh93LdLxcC4dd8GTCbUKKW8I%2Bw%3D)]

一旦我们需要写入变量,我们需要获得写锁,如下图所示。我们尝试请求写锁,但仍有线程从变量中读取,因此我们必须等待。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FACiYuTK-1682060926380)(https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/ff9d82fc40124dfa9abb4925b4a39eaf~noop.image?_iz=58558&from=article.pc_detail&x-expires=1682581512&x-signature=POSiQPUQVVW3MJYLfUcrETY%2BVA4%3D)]

一旦所有线程完成读取,所有读取操作都会阻塞,写入操作只能由一个线程执行,如下图所示。一旦释放写锁,我们将回到第一张图中的“自由竞争”状态。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VdanDMEI-1682060926380)(https://p3-sign.toutiaoimg.com/tos-cn-i-qvj2lq49k0/a02ff423e0324375859b15e2bc0657d1~noop.image?_iz=58558&from=article.pc_detail&x-expires=1682581512&x-signature=kHPZOo0FHii%2FeJ8FTdEsu92P1X0%3D)]

这是一种强大的模式,我们可以利用它使集合变得更快。一个典型的同步列表非常慢。它同步所有的操作,包括读和写。我们有一个CopyOnWriteArrayList,它对于读取操作非常快,但是任何写入操作都很慢。

如果可以避免从方法中返回迭代器,你可以封装列表操作并使用这个API。例如,在以下代码中,我们将名字列表暴露为只读,但是当需要添加名字时,我们使用写锁。这可以轻松超过synchronized列表的性能:

private final ReadWriteLock LOCK = new ReentrantReadWriteLock();
private Collection<String> listOfNames = new ArrayList<>();
public void addName(String name) {LOCK.writeLock().lock();try {listOfNames.add(name);} finally {LOCK.writeLock().unlock();}
}
public boolean isInList(String name) {LOCK.readLock().lock();try {return listOfNames.contains(name);} finally {LOCK.readLock().unlock();}
}

这个方案可行,因为synchronized是可重入的。我们已经持有锁,所以从methodA()进入methodB()不会阻塞。这在使用ReentrantLock时也同样适用,只要我们使用相同的锁或相同的synchronized对象。

StampedLock返回一个戳记(stamp),我们用它来释放锁。因此,它有一些限制,但它仍然非常快和强大。它也包括一个读写戳记,我们可以用它来保护共享资源。但ReadWriteReentrantLock不同的是,它允许我们升级锁。为什么需要这样做呢?

看一下之前的addName()方法…如果我用"Shai"两次调用它会怎样?

是的,我可以使用Set…但是为了这个练习的目的,让我们假设我们需要一个列表…我可以使用ReadWriteReentrantLock编写那个逻辑:

public void addName(String name) {LOCK.writeLock().lock();try {if(!listOfNames.contains(name)) {listOfNames.add(name);}} finally {LOCK.writeLock().unlock();}
}

这很糟糕。我“付出”写锁只是为了在某些情况下检查contains()(假设有很多重复项)。我们可以在获取写锁之前调用isInList(name)。然后我们会:

  • 获取读锁
  • 释放读锁
  • 获取写锁
  • 释放写锁

在两种情况下,我们可能会排队, 这样可能会增加额外的麻烦,不一定值得。

有了StampedLock,我们可以将读锁更新为写锁,并在需要的情况下立即进行更改,例如:

public void addName(String name) {long stamp = LOCK.readLock();try {if(!listOfNames.contains(name)) {long writeLock = LOCK.tryConvertToWriteLock(stamp);if(writeLock == 0) {throw new IllegalStateException();}listOfNames.add(name);}} finally {LOCK.unlock(stamp);}
}

这是针对这些情况的一个强大的优化。

终论

我经常不假思索地使用 synchronized 集合,这有时可能是合理的,但对于大多数情况来说,这可能是次优的。通过花费一点时间研究与线程相关的原语,我们可以显著提高性能。特别是在处理 Loom 时,其中底层争用更为敏感。想象一下在 100 万并发线程上扩展读取操作的情况…在这些情况下,减少锁争用的重要性要大得多。

你可能会想,为什么 synchronized 集合不能使用 ReadWriteReentrantLock 或者是 StampedLock 呢?

这是一个问题,因为API的可见接口范围非常大,很难针对通用用例进行优化。这就是控制低级原语的地方,可以使高吞吐量和阻塞代码之间的差异。


【注】本文译自: Relearning Java Thread Primitives - DZone


http://www.ppmy.cn/news/50742.html

相关文章

力扣刷题之路-01

作为一个修仙者&#xff0c;刚刚踏入练气期一层&#xff0c;无论从思维逻辑&#xff0c;还是技术层面&#xff0c;自己发现自己的能力不足。临渊羡鱼&#xff0c;不如退而结网&#xff0c;此后开始每天都继续学习&#xff0c;争取早日筑基 ----2023.4.22 说实话&#xff0c;第一…

【技术选型】Java 定时任务

文章目录 背景一、基础1.1 Cron表达式1.2 定时任务的三大组成部分 二、Java做定时任务的技术方案比较2.1、JDK seelp实现定时任务2.2、JDK Timer & TimerTask 实现定时任务2.3、JDK ScheduledExecutorService2.4、Quartz框架2.5、Spring Task 中的 schedule2.6、Elastic-Jo…

程序员员为什么总是要加班呢?不加班会被开除吗?

最近和圈子外的一个好朋友去聊天 他问我一个很奇怪的问题 就是你们程序员为什么会经常加班 并且都是11点12点才回家 为什么会这样 是因为有很多事情做不完吗其实 我半开玩笑的告诉他 其实事情没那么多 那为什么还要加班呢 首先加班的原因有很多 但是概述起来 就有如下几点 第一…

无人机遥感影像应用

目录 一、无人机遥感技术 二、无人机遥感影像数据生产 三、无人机遥感影像应用 一、无人机遥感技术 1.无人机遥感系统组成 1.1无人机遥感系统组成—无人机平台 1.2无人机遥感系统组成—传感器 2.无人机遥感技术的特点 高时效性&#xff1a;准确并快速获取地表数据 高分辨率…

正则表达式 - 简单模式匹配

目录 一、测试数据 二、简单模式匹配 1. 匹配字面值 2. 匹配数字和非数字字符 3. 匹配单词与非单词字符 4. 匹配空白字符 5. 匹配任意字符 6. 匹配单词边界 7. 匹配零个或多个字符 8. 单行模式与多行模式 一、测试数据 这里所用文本是《学习正则表达式》这本书带的&a…

2023年3月 青少年软件编程(C语言) 等级考试试卷(五级)

1. 拼点游戏 C和S两位同学一起玩拼点游戏。有一堆白色卡牌和一堆蓝色卡牌&#xff0c;每张卡牌上写了一个整数点数。C随机抽取n张白色卡牌&#xff0c;S随机抽取n张蓝色卡牌&#xff0c;他们进行n回合拼点&#xff0c;每次两人各出一张卡牌&#xff0c;点数大者获得三颗巧克力&…

缓存一致问题、幂等、jvm调优

缓存笔记 来自 程序员囧辉 黑马博学谷 幂等问题 1 缓存一致问题 1.1.同步删除 核心流程&#xff1a; 更新数据库数据删除缓存数据 问题&#xff1a; . 并发场景下存在脏数据 (并发有脏数据问题). 难以收拢所有更新数据库入口 (可能通过命令行、工具等删除db&#xff0c;…

MySQL:事务、索引、用户管理、备份、数据库设计(三大范式)

文章目录 Day 03&#xff1a;一、事务1. 原则2. 测试实现 二、索引1. 分类2. 创建索引3. 分析 sql 执行的状况4. 测试索引5. 索引原则 三、数据库用户管理四、备份五、规范数据库设计1. 三大范式 注意&#xff1a; Day 03&#xff1a; 一、事务 事务(transaction)&#xff1a…