你用对锁了吗?浅谈 Java “锁” 事(下)

简介: 你用对锁了吗?浅谈 Java “锁” 事(下)

读写锁


读写锁就是我们上面提交的根据场景减小锁的粒度了,把一个锁拆成了读锁和写锁,特别适合在读多写少的情况下使用,例如自己实现的一个缓存。


ReentrantReadWriteLock

读写锁允许多个线程同时读共享变量,但是写操作是互斥的,即写写互斥、读写互斥。讲白了就是写的时候就只能一个线程写,其他线程也读不了也写不了。

我们来看个小例子,里面也有个小细节。这段代码就是模拟缓存的读取,先上读锁去缓存拿数据,如果缓存没数据则释放读锁,再上写锁去数据库取数据,然后塞入缓存中返回。


image.png


这里面的小细节就是再次判断 data = getFromCache() 是否有值,因为同一时刻可能会有多个线程调用getData(),然后缓存都为空因此都去竞争写锁,最终只有一个线程会先拿到写锁,然后将数据又塞入缓存中。

此时等待的线程最终一个个的都会拿到写锁,获取写锁的时候其实缓存里面已经有值了所以没必要再去数据库查询。

当然 Lock 的使用范式大家都知道,需要用 try- finally,来保证一定会解锁。而读写锁还有一个要点需要注意,也就是说锁不能升级。什么意思呢?我改一下上面的代码。


image.png

但是写锁内可以再用读锁,来实现锁的降级,有些人可能会问了这写锁都加了还要什么读锁。

还是有点用处的,比如某个线程抢到了写锁,在写的动作要完毕的时候加上读锁,接着释放了写锁,此时它还持有读锁可以保证能马上使用写锁操作完的数据,而别的线程也因为此时写锁已经没了也能读数据

其实就是当前已经不需要写锁这种比较霸道的锁!所以来降个级让大家都能读。

小结一下,读写锁适用于读多写少的情况,无法升级,但是可以降级。Lock 的锁需要配合 try- finally,来保证一定会解锁。

对了,我再稍稍提一下读写锁的实现,熟悉 AQS 的同学可能都知道里面的 state ,读写锁就是将这个 int 类型的 state 分成了两半,高 16 位与低 16 位分别记录读锁和写锁的状态。它和普通的互斥锁的区别就在于要维护这两个状态和在等待队列处区别处理这两种锁

所以在不适用于读写锁的场景还不如直接用互斥锁,因为读写锁还需要对state进行位移判断等等操作。


StampedLock

这玩意我也稍微提一下,是 1.8 提出来的出镜率似乎没有 ReentrantReadWriteLock 高。它支持写锁、悲观读锁和乐观读。写锁和悲观读锁其实和 ReentrantReadWriteLock 里面的读写锁是一致的,它就多了个乐观读。

从上面的分析我们知道读写锁在读的时候其实是无法写的,而 StampedLock 的乐观读则允许一个线程写。乐观读其实就是和我们知道的数据库乐观锁一样,数据库的乐观锁例如通过一个version字段来判断,例如下面这条 sql。

image.png

它与 ReentrantReadWriteLock 对比也就强在这里,其他的不行,比如 StampedLock 不支持重入,不支持条件变量。还有一点使用 StampedLock 一定不要调用中断操作,因为会导致CPU 100%,我跑了下并发编程网上面提供的例子,复现了。


image.png

具体的原因这里不再赘述,文末会贴上链接,上面说的很详细了。

所以出来一个看似好像很厉害的东西,你需要真正的去理解它,熟悉它才能做到有的放矢。


CopyOnWrite

写时复制的在很多地方也会用到,比如进程 fork() 操作。对于我们业务代码层面而言也是很有帮助的,在于它的读操作不会阻塞写,写操作也不会阻塞读。适用于读多写少的场景。

例如 Java 中的实现 CopyOnWriteArrayList ,有人可能一听,这玩意线程安全读的时候还不会阻塞写,好家伙就用它了!

你得先搞清楚,写时复制是会拷贝一份数据,你的任何一个修改动作在CopyOnWriteArrayList 中都会触发一次Arrays.copyOf,然后在副本上修改。假如修改的动作很多,并且拷贝的数据也很大,这将是灾难!


并发安全容器


最后再来谈一下并发安全容器的使用,我就拿相对而言大家比较熟悉的 ConcurrentHashMap 来作为例子。我看新来的同事好像认为只要是使用并发安全容器一定就是线程安全了。其实不尽然,还得看怎么用。

我们先来看下以下的代码,简单的说就是利用 ConcurrentHashMap 来记录每个人的工资,最多就记录 100 个。

image.png

最终的结果都会超标,即 map 里面不仅仅只记录了100个人。那怎么样结果才会是对的?很简单就是加个锁。

image.png

看到这有人说,你这都加锁了我还用啥 ConcurrentHashMap ,我 HashMap 加个锁也能完事!是的你说的没错!因为当前我们的使用场景是复合型操作,也就是我们先拿 map 的 size 做了判断,然后再执行了 put 方法,ConcurrentHashMap 无法保证复合型的操作是线程安全的!

而 ConcurrentHashMap 合适只是用其暴露出来的线程安全的方法,而不是复合操作的情况下。比如以下代码

image.png

当然,我这个例子不够恰当其实,因为 ConcurrentHashMap 性能比 HashMap + 锁高的原因在于分段锁,需要多个 key 操作才能体现出来,不过我想突出的重点是使用的时候不能大意,不能纯粹的认为用了就线程安全了。


总结一下


今天谈了谈并发 BUG 的源头,即三大问题:可见性问题、原子性问题和有序性问题。然后简单的说了下 synchronized 关键字的注意点,即修饰静态字段或者静态方法是类层面的锁,而修饰非静态字段和非静态方法是实例层面的类。

再说了下锁的粒度,在不同场景定义不同的锁不能粗暴的一把锁搞定,并且方法内部锁的粒度要细。例如在读多写少的场景可以使用读写锁、写时复制等。

最终要正确的使用并发安全容器,不能一味的认为使用并发安全容器就一定线程安全了,要注意复合操作的场景。

image.png

当然我今天只是浅浅的谈了一下,关于并发编程其实还有很多点,要写出线程安全的代码不是一件容易的事情,就像我之前分析的 Kafka 事件处理全流程一样,原先的版本就是各种锁控制并发安全,到后来bug根本修不动,多线程编程难,调试也难,修bug也难。

因此 Kafka 事件处理模块最终改成了单线程事件队列模式将涉及到共享数据竞争相关方面的访问抽象成事件,将事件塞入阻塞队列中,然后单线程处理

所以在用锁之前我们要先想想,有必要么?能简化么?不然之后维护起来有多痛苦到时候你就知道了。


最后


之后继续开始写消息队列相关的包括 RocketMQ 和 Kafka,有不少同学在后台留言想和我深入的交流一下,发生点关系,我把公众号菜单加了个联系我,有需求的小伙伴可以加我微信。

StampedLock bug 的那个链接:ifeve.com/stampedlock…



相关文章
|
14天前
|
Java
Java中ReentrantLock释放锁代码解析
Java中ReentrantLock释放锁代码解析
25 8
|
1月前
|
Java
Java并发编程中的锁机制
【2月更文挑战第22天】 在Java并发编程中,锁机制是一种重要的同步手段,用于保证多个线程在访问共享资源时的安全性。本文将介绍Java锁机制的基本概念、种类以及使用方法,帮助读者深入理解并发编程中的锁机制。
|
1月前
|
存储 Java 程序员
记一次synchronized锁字符串引发的坑兼再谈Java字符串
记一次synchronized锁字符串引发的坑兼再谈Java字符串
21 2
|
1月前
|
Java
深入了解Java中的锁机制
深入了解Java中的锁机制
|
14天前
|
Java 调度
Java中常见锁的分类及概念分析
Java中常见锁的分类及概念分析
15 0
|
7天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
8天前
|
Java
浅谈Java的synchronized 锁以及synchronized 的锁升级
浅谈Java的synchronized 锁以及synchronized 的锁升级
8 0
|
10天前
|
存储 缓存 Java
线程同步的艺术:探索 JAVA 主流锁的奥秘
本文介绍了 Java 中的锁机制,包括悲观锁与乐观锁的并发策略。悲观锁假设多线程环境下数据冲突频繁,访问前先加锁,如 `synchronized` 和 `ReentrantLock`。乐观锁则在访问资源前不加锁,通过版本号或 CAS 机制保证数据一致性,适用于冲突少的场景。锁的获取失败时,线程可以选择阻塞(如自旋锁、适应性自旋锁)或不阻塞(如无锁、偏向锁、轻量级锁、重量级锁)。此外,还讨论了公平锁与非公平锁,以及可重入锁与非可重入锁的特性。最后,提到了共享锁(读锁)和排他锁(写锁)的概念,适用于不同类型的并发访问需求。
40 2
|
11天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
12天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第13天】 在Java并发编程中,锁是一种常见的同步机制,用于保证多个线程之间的数据一致性。然而,不当的锁使用可能导致性能下降,甚至死锁。本文将探讨Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁降级等方法,以提高程序的执行效率。
13 4