首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01 | 可见性、原子性和有序性问题:并发编程Bug的源头
02 | Java内存模型:看Java如何解决可见性和有序性问题
03 | 互斥锁(上):解决原子性问题
04 | 互斥锁(下):如何用一把锁保护多个资源?
05 | 一不小心就死锁了,怎么办?
06 | 用“等待-通知”机制优化循环等待
07 | 安全性、活跃性以及性能问题
08 | 管程:并发编程的万能钥匙
09 | Java线程(上):Java线程的生命周期
10 | Java线程(中):创建多少线程才是合适的?
11 | Java线程(下):为什么局部变量是线程安全的?
12 | 如何用面向对象思想写好并发程序?
13 | 理论基础模块热点问题答疑
14 | Lock和Condition(上):隐藏在并发包中的管程
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
16 | Semaphore:如何快速实现一个限流器?
17 | ReadWriteLock:如何快速实现一个完备的缓存?
18 | StampedLock:有没有比读写锁更快的锁?
19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?
20 | 并发容器:都有哪些“坑”需要我们填?
21 | 原子类:无锁工具类的典范
22 | Executor与线程池:如何创建正确的线程池?
23 | Future:如何用多线程实现最优的“烧水泡茶”程序?
24 | CompletableFuture:异步编程没那么难
25 | CompletionService:如何批量执行异步任务?
26 | Fork/Join:单机版的MapReduce
27 | 并发工具类模块热点问题答疑
28 | Immutability模式:如何利用不变性解决并发问题?
29 | Copy-on-Write模式:不是延时策略的COW
30 | 线程本地存储模式:没有共享,就没有伤害
31 | Guarded Suspension模式:等待唤醒机制的规范实现
32 | Balking模式:再谈线程安全的单例模式
33 | Thread-Per-Message模式:最简单实用的分工方法
34 | Worker Thread模式:如何避免重复创建线程?
35 | 两阶段终止模式:如何优雅地终止线程?
36 | 生产者-消费者模式:用流水线思想提高效率
37 | 设计模式模块热点问题答疑
38 | 案例分析(一):高性能限流器Guava RateLimiter
39 | 案例分析(二):高性能网络应用框架Netty
40 | 案例分析(三):高性能队列Disruptor
41 | 案例分析(四):高性能数据库连接池HiKariCP
42 | Actor模型:面向对象原生的并发模型
43 | 软件事务内存:借鉴数据库的并发经验
44 | 协程:更轻量级的线程
45 | CSP模型:Golang的主力队员
当前位置:
首页>>
技术小册>>
Java并发编程实战
小册名称:Java并发编程实战
### 04 | 互斥锁(下):如何用一把锁保护多个资源? 在Java并发编程的广阔领域中,互斥锁(Mutex)是确保多线程环境下数据一致性和完整性的基石。上一章节我们初步探讨了互斥锁的基本概念、使用场景以及如何通过Java内置的`synchronized`关键字或`ReentrantLock`类来实现基本的互斥访问。然而,在实际应用中,我们常常面临需要一把锁来保护多个资源的情况,这要求我们在设计并发控制策略时更加细致和周全。本章将深入探讨如何在复杂场景下,使用一把锁来有效保护多个资源,避免死锁、活锁及性能瓶颈等问题。 #### 一、引言 在多线程程序中,如果多个线程需要同时访问并修改多个共享资源,而这些资源的状态变化之间存在依赖关系,那么仅仅为每个资源单独加锁可能不足以保证数据的一致性和完整性。例如,在一个银行转账系统中,如果从一个账户扣款和向另一个账户存款被视为两个独立的操作,并分别用两把锁保护,那么在极端情况下可能会出现一个账户扣款成功但另一个账户存款失败的情况,导致资金不一致。因此,我们需要一种机制来确保这些相关操作能够作为一个整体被同步执行,即使用一把锁来保护多个资源。 #### 二、一把锁保护多个资源的策略 ##### 2.1 单一锁策略 最简单直接的方法是为所有需要保护的资源使用同一把锁。这种策略易于实现,但在高并发场景下可能成为性能瓶颈,因为所有访问这些资源的线程都必须竞争同一把锁。此外,如果锁持有时间过长,还可能导致其他线程饥饿。 **示例代码**: ```java public class BankAccount { private final Object lock = new Object(); private double balance; public void transfer(BankAccount toAccount, double amount) { synchronized (lock) { if (this.balance >= amount) { this.balance -= amount; toAccount.transferIn(amount, lock); // 假设toAccount也使用相同的lock } } } private void transferIn(double amount, Object lock) { synchronized (lock) { this.balance += amount; } } } ``` 注意:在上面的示例中,虽然`transferIn`方法也接受了一个锁对象作为参数,但在实际应用中,如果两个账户实例都使用相同的锁对象,则可以直接在`transfer`方法中完成所有操作,无需额外传递锁。 ##### 2.2 锁分段技术 对于大量资源的并发访问,单一锁策略可能导致严重的性能问题。锁分段技术通过将资源分成多个段,每段使用独立的锁来保护,从而减小锁的竞争范围,提高并发性能。这种技术适用于资源可以逻辑上分为多个独立部分,且各部分之间操作互不干扰的场景。 **示例概念**: 假设有一个大型的哈希表,可以将其分成多个段(segment),每个段使用独立的锁。当线程需要访问哈希表中的某个元素时,首先确定该元素属于哪个段,然后仅对该段的锁进行加锁操作。 ##### 2.3 锁粒度调整 锁粒度是指锁保护的数据范围大小。粗粒度锁(如单一锁策略)保护的数据范围大,竞争激烈但管理简单;细粒度锁(如锁分段)保护的数据范围小,竞争减少但管理复杂。在实际应用中,应根据具体场景和需求灵活调整锁粒度,以达到性能和一致性的最佳平衡。 #### 三、避免死锁与活锁 在使用一把锁保护多个资源时,尤其需要注意避免死锁和活锁的发生。 ##### 3.1 死锁 死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,这些线程都将无法向前推进。避免死锁的一种有效方法是确保所有线程以相同的顺序获取锁,或者使用锁超时机制来中断长时间等待锁的线程。 ##### 3.2 活锁 活锁与死锁不同,它指的是线程之间不断地相互谦让,导致没有线程能够执行。这通常发生在多线程尝试以某种方式“合作”完成任务,但它们的执行顺序不断被对方的谦让行为打断。避免活锁的一种方法是引入随机等待时间或优先级策略,以减少线程间的无谓谦让。 #### 四、性能优化 在使用一把锁保护多个资源时,性能优化是不可或缺的一环。以下是一些常见的优化策略: - **减少锁的范围**:只在真正需要同步的代码块内加锁,避免将锁的范围扩大到不必要的操作。 - **锁降级与升级**:根据操作的需要,动态地调整锁的粒度,以减少锁的竞争。 - **读写分离锁**:对于读多写少的资源,可以使用读写锁(`ReentrantReadWriteLock`)来优化性能,允许多个读线程同时访问资源,而写线程则独占访问权。 - **使用无锁编程**:在某些场景下,如果资源状态的变化可以通过原子操作来保证,那么可以考虑使用无锁编程技术,如原子变量(`AtomicInteger`、`AtomicReference`等),以进一步提高性能。 #### 五、总结 在Java并发编程中,使用一把锁来保护多个资源是一种常见的需求,但也需要谨慎处理以避免死锁、活锁及性能瓶颈等问题。通过选择合适的锁策略、调整锁粒度、优化锁的使用方式以及采用无锁编程技术,我们可以在保证数据一致性和完整性的同时,最大限度地提升程序的并发性能。希望本章的内容能够为您在Java并发编程实践中提供有益的参考和启示。
上一篇:
03 | 互斥锁(上):解决原子性问题
下一篇:
05 | 一不小心就死锁了,怎么办?
该分类下的相关小册推荐:
Java语言基础11-Java中的泛型
Mybatis合辑5-注解、扩展、SQL构建
Java语言基础3-流程控制
Java必知必会-Maven高级
Java必知必会-JDBC
java源码学习笔记
SpringBoot合辑-高级篇
Mybatis合辑3-Mybatis动态SQL
Java语言基础5-面向对象初级
Java语言基础1-基础知识
Java语言基础12-网络编程
Java并发编程