文章目录
- 2.一篇文章读懂Java常用的锁机制
- 2.1锁介绍
- 2.1.1定义
- 2.1.2相关概念
- 2.2锁的种类
- 2.2.1按功能层面分
- (1)共享锁/排他锁/读写锁
- 2.2.2按性能和线程安全分
- (1)乐观锁/悲观锁
- (2)偏向锁/轻量级锁(自旋锁)/重量级锁(排他锁)
- (3)公平锁/非公平锁
- 2.2.3按锁的特性分
- (1)重入锁(ReentrantLock)
- (2)分布式锁
- 2.2.4按锁的状态分
- (1)死锁
- (2)活锁
- 2.3锁消除/锁膨胀
- 2.4CAS思想
- 2.5AQS机制
- (1)概念
- (2)AQS与Synchronized的区别
- (3)常见的实现锁类
- (4)基本工作机制
2.一篇文章读懂Java常用的锁机制
2.1锁介绍
2.1.1定义
在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。
简单来说,锁要解决的问题就是 线程安全问题
所谓线程安全,主要体现在三方面:原子性、可见性和有序性。
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作。(synchronized、lock)
- 可见性:一个线程对主内存的修改可以及时被其他线程看到。(volatile)
- 有序性:一个线程观察其他线程的指令执行顺序,由于在JMM中允许编译器和处理器对指令重排序,因此该观察结果一般杂乱无序。(volatile)
2.1.2相关概念
需要了解关于锁的三个概念:
1、锁开销 lock overhead :锁占用内存空间、 cpu初始化和销毁锁、获取和释放锁的时间。程序使用的锁越多,相应的锁开销越大
2、锁竞争 lock contention: 一个进程或线程试图获取另一个进程或线程持有的锁,就会发生锁竞争。锁粒度越小,发生锁竞争的可能性就越小
3、死锁 deadlock 至少两个任务中的每一个都等待另一个任务持有的锁的情况锁粒度是衡量锁保护的数据量大小,通常选择粗粒度的锁(锁的数量少,每个锁保护大量的数据),在当单进程访问受保护的数据时锁开销小,但是当多个进程同时访问时性能很差。因为增大了锁的竞争。相反,使用细粒度的锁(锁数量多,每个锁保护少量的数据)增加了锁的开销但是减少了锁竞争。例如数据库中,锁的粒度有表锁、页锁、行锁、字段锁、字段的一部分锁
相关术语 Critical Section(临界区)、 Mutex/mutual exclusion(互斥体)、 Semaphore/binary semaphore(信号量)
2.2锁的种类
以下将从功能层面、性能线程安全、锁的特性、以及锁的状态四个角度来分,锁一共可分为以下几种类型
-
功能层面:
- 共享锁/排他锁/读写锁
-
性能线程安全:
- 乐观锁/悲观锁
- 偏向锁/轻量级锁(自旋锁)/重量级锁(排他锁)
- 公平锁/非公平锁
-
锁的特性:
- 重入锁/分布式锁
-
锁的状态:
- 死锁/活锁
2.2.1按功能层面分
(1)共享锁/排他锁/读写锁
若从功能层面角度来看,锁可以分为三类:共享锁、排他锁、读写锁
- 共享锁也叫读锁((ReadWriteLock),读锁的特点是在同一时刻允许多个线程抢占到锁。
- 排它锁也叫写锁(ReentrantLock、 Synchronized),写锁的特点是在同一时刻只允许一个线程抢占到锁。
锁升级:读锁到写锁 (不支持)
锁降级:写锁到读锁 (支持)
《共享锁(读锁)/排他锁(写锁)》详情笔记链接:暂无
- 读写锁 ReentrantReadWriteLock
低16位代表写锁,高16位代表读锁
- 该读写锁 ReentrantReadWriteLock提供了一个读锁,支持多个线程共享同一把锁。
- 它也提供了一把写锁,是排他锁,和其他读锁或者写锁互斥,表明只有一个线程能持有锁资源。
通过两把锁的协同工作,能够最大化的提高读写的性能,特别是读多写少的场景,而往往大部分的场景都是读多写少的。
《读写锁 ReentrantReadWriteLock》详情笔记链接:暂无
2.2.2按性能和线程安全分
(1)乐观锁/悲观锁
-
乐观锁
- 每次去读取数据时,都认为其他人不会修改该数据,因此不会加锁
- 修改数据,提交更新时候会判断在此期间是否有他人去更新此数据。
- 使用版本号机制和CAS算法实现
-
悲观锁
- 总是假设最坏的情况,每次去 拿(读取/修改) 数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁
- Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现
《乐观锁/悲观锁》详情笔记链接:暂无
(2)偏向锁/轻量级锁(自旋锁)/重量级锁(排他锁)
-
偏向锁:
- 定义:可以让同一线程一直拥有同一个锁,直到出现竞争,才去释放锁!
- 场景:应用于同步代码块在大多数情况下只有同一个线程访问
- 举例:所谓偏向锁就是当线程1进入锁的时候如果当前不存在竞争,那么它就会把这个锁偏向线程1,线程1下次再进入的时候,就不再需要竞争锁。
-
轻量级锁(自旋锁)
- 定义:轻量级锁即通过自旋方式不断尝试获取锁,而不是阻塞。当偏向锁被其他线程访问后,就会升级为轻量级锁。常见的轻量级锁即自旋锁
- 场景:前提是线程在临界区的操作非常快,所以它会非常快速地释放锁
-
重量级锁(排他锁):
- 定义:有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态
- 场景:有大量的线程参与锁的竞争,冲突性很高
《偏向锁/轻量级锁(自旋锁)/重量级锁(排他锁)》详情笔记链接:暂无
(3)公平锁/非公平锁
- 公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的;
- 非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来后到的规则,所有线程会竞争获取锁。
默认情况下锁都是非公平的,比如Synchronized(只能为非公平锁)、Reentrantlock(在创建Reentrantlock时可以手动指定成公平锁)
因为非公平锁的性能要比公平锁的性能更好,非公平锁意味着减少了锁的等待,减少了线程的阻塞和唤醒。
《公平锁/非公平锁》详情笔记链接:暂无
2.2.3按锁的特性分
第三维度从锁的特性来说,又会有重入锁和分布式锁。
(1)重入锁(ReentrantLock)
锁主要用来控制多线程访问问题,对于同一线程,如果连续两次对同一把锁进行加锁,那么这个线程就会被卡死
在实际开发中,方法之间的调用错综复杂,一不小心就可能在多个不同的方法中反复调用lock(),造成死锁
重入锁:同一线程可以对同一把锁在不释放的前提下,反复加锁不会导致线程卡死,唯一的一点就是需要保证lock()和unlock0)的次数相同
(2)分布式锁
分布式锁是解决分布式架构下粒度的问题,解决的是进程维度的问题,而Synchronized是解决Java并发里面的线程维度。关于分布式锁更多知识点后面我们单独来讨论。
《重入锁/分布式锁》详情笔记链接:暂无
2.2.4按锁的状态分
(1)死锁
-
定义:指两个或两个以上的线程在执行过程中,因争夺资源造成的一种互相等待的现象。当一个线程永久地持有一把锁后,其他线程将永久等待下去
-
四个条件:
- 互斥性:即线程占用的锁是互斥锁,不能被其他为占用的线程访问
- 不剥夺:即线程已经获得锁,在未主动释放之前,不会被其他线程剥夺
- 请求和保持:即有锁S1,S2,线程一持有了S1,又发起了对S2的持有请求。而同时有线程二持有了S2,又发起了对S1的持有请求。
- 环路等待:即死锁发生时,必然有一个环形链。如{p0,p1,p2,…pn}。p0等待p1释放资源,p1等待p2释放资源,p2等待p3释放资源,… pn等待p0释放资源
-
解决方式:
- jstack定位死锁:https://www.cnblogs.com/chenpi/p/5377445.html
- 线上环境死锁,查看堆栈信息:使用 jps和jstack 命令分别查看JVM中运行的进程状态信息、以及java进程内线程的堆栈信息
-
死锁避免:
- 避免相反的获取锁的顺序
- 设置超时时间(lock类的 tryLock)
- 多使用并发类而不是自己设计锁
(2)活锁
- 定义:活锁即线程并没有阻塞,也始终在运行,但是程序却得不到进展,因为线程始终重复做同样的事。本质原因是重试机制一样,始终互相谦让。
- 案例:例如消息队列,若消息队列第一个一直消费失败,则会不断进行重试。而非第一个消息则会一直等待第一个消息被消费,造成了整个队列的罢工
- 解决方案:
- 增加随机因素
- 增加重试机制
2.3锁消除/锁膨胀
在jdk中还引入了锁消除和锁膨胀,这是编译器层面的优化,主要优化加锁的性能。
- 锁消除:代码本身可能就没有线程安全问题,但是你又加了锁,然后ivm编译的时候发现这个地方加了锁,导致无效竞争,那么它就会把这个锁消除掉。
- 锁膨胀:因为控制的锁粒度太小,导致频繁加锁和释放锁,所以它就会把锁的范围扩大。
2.4CAS思想
-
定义:CAS即Compare And Swap,比较后再交换,体现了乐观锁思想,在无锁情况下保证线程操作共享数据的原子性
-
数据交换流程:
-
底层:
2.5AQS机制
(1)概念
AQS即AbstractQueuedSynchronizer,即抽象队列同步器,为构建锁或者其他同步组件的基础框架
同步器功能:屏蔽了一些锁内部实现的细节例如:线程的等待排列,锁的实现,同步状态的管理,等待和唤醒等,让锁的使用者能够简单的使用锁
(2)AQS与Synchronized的区别
(3)常见的实现锁类
- ReentrantLock:可重入锁
- ReentrantReadWriteLock:读写锁
- Semaphore:信号量
- CountDownLatch:倒计时锁
(4)基本工作机制
- AQS内部维护一个先进先出的双向队列,存储排队获取锁的线程
- AQS内部有一个属性state,相当于是一个资源, 默认为0;若队列中有一个线程将state修改了为 1,则意味着当前线程获取了资源
- 原子性:对state修改时,使用cas操作,保证了 多个线程修改的原子性
《深入理解AQS原理与Reentrantlock锁》笔记:https://blog.csdn.net/weixin_61440595/article/details/136442636