系统梳理一下锁

简介: 有人对Java主流锁做了下面全面的梳理。梳理的确实挺好的。但是我看到这张图,第一个感觉是:记不住。

背景


有人对Java主流锁做了下面全面的梳理。梳理的确实挺好的。但是我看到这张图,第一个感觉是:记不住。


1112728-20200603224841457-951653485.png


因为分了太多类,彼此之间没有什么联系。做PPT可以。如果聊天或者面试,不用纸笔的情况下,就不太好描述了。也不利于对原理和应用的理解。


基于上述的考虑,我就自己系统的梳理一下锁,希望可以有助于大家理解和记忆,以至于最后在工作中得到很好的应用。


先说线程锁再说分布式锁。

 

线程锁


概述


这里说的线程锁是Java线程锁,从原理上各个语言应该都比较相似。有很多维度的划分方式,我比较建议的是从大面上分为乐观锁和悲观锁。


乐观锁主要是自旋+CAS的方式,比如JUC(java.util.concurrent包)的原子类。


悲观锁主要用synchronized关键字的隐式锁和基于AQS的显示锁。


上面三段总结如下:


1112728-20200604094229616-1238452027.png


悲观锁的实现原理


1>synchronized关键字


随着java版本升级,synchronized关键字虽然是用C++写的,但是原理和JCU包的ReentrantLock很相似。synchronized关键字有4种锁状态:无锁、偏向锁、轻量级锁、重量级锁。无锁类似于ReentrantLock的交替执行,没有并发,就不涉及锁;偏向锁类似于ReentrantLock的可重入的概念,使得已经获取到锁的线程可以多次获取锁;轻量级锁解决的问题是尽量避免线程切换,使用的方法也和ReentrantLock相似,是自旋+CAS的方式;重量级锁依赖于管程monitor来实现,和ReentrantLock一样都涉及用户态和内核态切换。


根据这个我们再来补充一下Java线程锁的思维导图:


1112728-20200604094038265-1316241092.png


2>基于基于AQS的显示锁


基于AQS的显示锁我之前看过一些源码。这里面比较经典的是ReentrantLock。这是可重入锁,就是同一个线程可以反复进入加锁的线程。如果想实现不可重入锁也很简单。把可重入锁对当前线程做特殊处理的部分去掉就好了。


其他JCU下locks包里的锁比如读写锁就是将锁细化成了读锁和写锁。读锁是共享锁的实现,写锁是排他锁的实现。


ReentrantLock可以使用公平锁和非公平锁两种方式,公平锁和非公平锁各自继承了AQS。区别只是非公平锁在需要加锁时先直接尝试是否可以获取锁成功,而公平锁是先看自己是否需要排队。


下面以ReentrantLock的公平锁为例来简单聊一下AQS的源码。AQS核心是实现了CLH队列。


AQS有head、tail、持有锁的线程、状态4个主要的成员变量。


利用head!=tail就是说AQS是否未被初始化来判断是否交替执行,交替执行则不用加锁;如果需要加锁则判断是否就是当前拥有锁的线程,是的话,将进入次数+1;如果不是则判断是否需要初始化AQS,需要的话先初始化一个dummy header,再将自己加入队尾,如果是队列里dummy header的指针指向的节点,则它为先自旋判断是否可以获取锁;如果不是dummy header指针指向的节点,则使用park让出cpu。当dummy header的指针指向的节点获取到锁之后,会将head指向自己,同时将自己这个Node节点的当前线程设置为空,将自己设置为dummy header,同时将原来dummy header的指针都设置为null,使得原dummy header成为一个没有引用的节点,便于垃圾回收。


根据这个我们再来补充一下Java线程锁的思维导图:


1112728-20200604093857354-1138537204.png


分布式锁


不管是线程锁还是分布式锁,都实现了tryLock、lock、unlock三个方法。


tryLock的语义是非阻塞锁,尝试获取锁,成功返回true,不成功返回false;主流lock语义是阻塞锁。实现一般基于tryLock来做自旋,不成功的时候也会有像ReentrantLock一样的阻塞操作。


常见的分布式锁实现以及数据库锁的实现详见之前写的文章:《MySQL常见6个考题在实际工作中的运用》这里就不再赘述了。

 

总结


本篇文章在介绍知识点是次要的,主要是展示了总结思考的思路,希望能对读者朋友们的思考问题方法上有所帮助,仅做参考。


相关文章
|
12月前
关于死锁的原因及解决方案
关于死锁的原因及解决方案
139 0
|
3月前
|
算法 Java
Java多线程基础-13:一文阐明死锁的成因及解决方案
死锁是指多个线程相互等待对方释放资源而造成的一种僵局,导致程序无法正常结束。发生死锁需满足四个条件:互斥、请求与保持、不可抢占和循环等待。避免死锁的方法包括设定加锁顺序、使用银行家算法、设置超时机制、检测与恢复死锁以及减少共享资源。面试中可能会问及死锁的概念、避免策略以及实际经验。
47 1
|
3月前
|
监控 算法 安全
Java并发编程案例分析:死锁的检测与解决
【4月更文挑战第6天】Java并发编程中的死锁导致线程僵持,资源无法释放,影响程序性能。死锁涉及互斥、请求与保持、不剥夺和循环等待四个条件。案例分析展示了银行转账场景下的死锁可能。检测死锁可通过日志、代码审查和使用工具。解决策略包括避免死锁(如设定锁顺序、超时机制)和处理死锁(如终止线程、资源抢占)。理解死锁原理并采取预防措施对构建稳定、高效的多线程应用至关重要。
76 1
|
3月前
|
Java
【专栏】Java多线程中,锁用于控制共享资源访问,确保数据一致性和正确性,锁是什么意思,有哪些分类?
【4月更文挑战第28天】Java多线程中,锁用于控制共享资源访问,确保数据一致性和正确性。本文探讨锁的概念、作用及分类:乐观锁与悲观锁、自旋锁与适应性自旋锁、公平锁与非公平锁、可重入锁和读写锁。使用锁需注意避免死锁、合理选择锁粒度及性能优化。理解锁有助于提升多线程编程的效率和稳定性。
65 0
|
3月前
|
算法 Java
【面试问题】锁如何优化?
【1月更文挑战第27天】【面试问题】锁如何优化?
|
Java 编译器 调度
锁的优化过程
锁的优化过程
|
缓存 监控 算法
内容服务锁优化实践
内容服务锁优化实践
79 0
|
缓存 Linux 容器
深入了解锁细节
深入了解锁细节
59 0
|
并行计算 安全 算法
Oh!老伙计,提高自己的并发技能,先从锁优化开始吧
锁是最常用的同步方法之一。在高并发的环境下,激烈的锁竞争会导致程序的性能下降。 对于单任务或者单线程的应用而言,其主要资源消耗都花在任务本身,它既不需要维护并行数据结构间的一致性状态,也不需要为线程的切换和调度花费时间。对于多线程应用来说,系统除了处理功能需求外,还需要额外维护多线程环境的特有信息,如线程本身的元数据、线程的调度、线程上下文的切换等。并行计算之所以能提高系统的性能,并不是因为它"少干活"了,而是因为并行计算可以更合理地进行任务调度,充分利用各个CPU资源。
|
存储 安全 Java
看完你就明白的锁系列之锁的状态
前面两篇文章我介绍了一下 看完你就应该能明白的悲观锁和乐观锁 看完你就明白的锁系列之自旋锁 看完你就会知道,线程如果锁住了某个资源,致使其他线程无法访问的这种锁被称为悲观锁,相反,线程不锁住资源的锁被称为乐观锁,而自旋锁是基于 CAS 机制实现的,CAS又是乐观锁的一种实现,那么对于锁来说,多个线程同步访问某个资源的流程细节是否一样呢?换句话说,在多线程同步访问某个资源时,锁的状态会如何变化呢?本篇文章来探讨一下。
88 0
看完你就明白的锁系列之锁的状态