0. 分类

0.0 可重入锁 不可重入锁

重入:当前线程获取到 A 锁,在获取之后尝试再次获取 A 锁是可以直接拿到的。

不可重入:当前线程获取到 A 锁,在获取之后尝试再次获取 A 锁,是无法获取到的,因为 A 锁被当前线程占用着,需要等待自己释放锁再获取锁。容易形成死锁。

0.1 乐观锁 悲观锁

Java 中提供的 synchronized, ReentrantLock, ReentrantReadWriteLock 都是悲观锁。悲观锁获取不到锁资源时会将当前线程挂起(进入 BLOCKED, WAITING)。

Q: 阻塞也是等,非阻塞的自己自旋也是等,都是干不了别的事,为啥说阻塞就性能低呢? A: 悲观锁获取不到锁资源时会将当前线程挂起(进入 BLOCKED, WAITING),线程挂起会涉及用户态和内核态的切换,而这种切换是比较消耗资源的。而乐观锁的线程始终都是 RUNNING 状态,哪怕竞争失败。

0.2 公平锁 非公平锁

公平锁是指线程竞争锁资源时,如果已经有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队。

而非公平锁,就是不管是否有线程在排队等待锁,它都会尝试去竞争一次锁。

0.3 互斥锁 共享锁

Java 中提供的 synchronized、Reentrantlock 是互斥锁。 Java 中提供的 ReentrantReadWritelock,有互斥锁也有共享锁。

排它锁,多用于写操作,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源。

共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源。

Synchronized

理解锁,最好带着 Java 的 Synchronized 一起比较理解。

Lock 比 Synchronized 的灵活性更高,Lock 可以自主决定什么时候加锁,什么时候释放锁,只需要调用 lock() 和 unlock() 这两个方法就行,同时 Lock 还提供了非阻塞的竞争锁方法 tryLock() 方法,这个方法通过返回 true/false 来 告诉当前线程是否已经有其他线程正在使用锁。

Lock 提供了公平锁和非公平锁的机制,Synchronized 只提供了一种非公平锁的实现。

从性能方面来看,Synchronized 和 Lock 在性能方面相差不大,在实现上会有一 些区别,Synchronized 后来引入了偏向锁、轻量级锁、重量级锁以及锁升级的方式来优化加锁的性能,而 Lock 中则用到了自旋锁的方式来优化性能。

ReentrantLock

首先,ReentrantLock 是一种可重入的排它锁,主要用来解决多线程对共享资源竞争的问题。

核心特征:

  1. 可重入。

  2. 它支持公平和非公平特性。公平,指的是竞争锁资源的线程,严格按照请求顺序来分配锁。 非公平,表示竞争锁资源的线程,允许插队来抢占锁资源。ReentrantLock 默认采用了非公平锁的策略来实现锁的竞争逻辑。

  3. 它提供了阻塞竞争锁和非阻塞竞争锁的两种方法,分别是 lock() 和 tryLock()

(ps. 其实,Sychronized 也是一种可重入锁。)

//单例
public class ReTest {
	private static volatile ReTest reTest;
	
	private ReTest() {}

	public static ReTest getInstance() {
		if(reTest == null) {
			sychronized (ReTest.class) {
				if (reTest == null) {
					sychronized (ReTest.class) {
						reTest = ReTest();
					}
				}
			}
		}
	}
}

可重入的意义是什么呢?如上,9 和 11 行都在竞争同一个资源 ReTest.class。如果不可重入,则容易死锁。

要理解 ReentrantLock 底层原理,就不得不提 AQS 队列。

AQS 队列

AQS (AbstractQueuedSychronized) 是多线程同步器,它是 J.U.C 包中多个组件的底层实现,如 Lock、 CountDownLatch、Semaphore 等都用到了 AQS.

AQS 提供了两种锁机制,分别是排它锁,和共享锁。

排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源。比如 Lock 中的 ReentrantLock 可重入锁实现就是用到了 AQS 中的排它锁功能。

共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如 CountDownLatch 和 Semaphore 都是用到了 AQS 中共享锁。

AQS 有一个成员变量 state 和一个队列,数据结构是用双向链表结构维护的 FIFO 线程等待队列。

具体工作原理是,多个线程通过对这个 state 共享变量进行修改来实现竞态条件, 竞争失败的线程加入到 FIFO 队列并且阻塞 Blocked, 抢占到竞态资源的线程释放之后,后续的线程按照 FIFO 顺序实现有序唤醒 wake。

回到 ReentrantLock

公平锁的实现方式就是,线程在竞争锁资源的时候判断 AQS 同步队列里面有没有等待的线程。 如果有,就加入到队列的尾部等待。 而非公平锁的实现方式,就是不管队列里面有没有线程等待,它都会先去尝试抢占锁资源,如果抢不到,再加入到 AQS 同步队列等待。

死锁

争夺同一个共享资源造成的相互等待资源的现象。 如果没有外部干预,线程会一直阻塞无法往下执行。

导致死锁之后,只能通过人工干预来解决,比如重启服务,或者杀掉某个线程。 所以,只能在写代码的时候,去规避可能出现的死锁。

产生的原因和应该怎样避免?常见原因如进程推进顺序非法。

  • 互斥条件。不可改变

  • 请求和保持条件。一次性申请所有资源

  • 不可抢占条件。让某线程主动释放

  • 循环等待。让资源有线性顺序

死锁问题不仅仅局限在多线程领域,但凡涉及到互斥锁的地方都有可能出现, 比如 Mysql 数据库的行锁、表锁,以及分布式锁。

死锁监测

死锁定理(死锁状态的充分条件):

  • 当且仅当此状态下资源分配图是_不可完全简化_的

  • 简化过程类似于“拓扑排序”算法

死锁监测-0.png
死锁监测-1.png
死锁监测-2.png

死锁的解除

资源剥夺。挂起 suspend 死锁进程,释放 cpu 执行权,强制其放弃所有资源,剥夺其资源。 撤销进程。 进程回退。需要记录历史信息,设置还原点。

Last updated