目录
0 概述
- Java中的锁主要用于保障多并发线程情况下数据的一致性;
- 在多线程编程中为了保障数据的一致性,通常需要在使用对象或方法前加锁,这时如果有另一个线程也需要使用该对象或方法,就会进入阻塞队列,等待锁释放后,该线程才有机会再次获取锁进行操作;
- 如此就保障了在同一时刻只有一个线程持有对象的锁并修改对象,从而保障了数据的安全;
1 乐观锁与悲观锁
- 乐观锁以乐观的思想处理数据,在读数据时认为别人不会修改该数据,所以不会上锁;在写数据时会判断在此期间有没有其他人更新该数据,如果没有更新,则加锁并执行写操作,否则不执行写操作,返回失败状态;
- Java中的乐观锁通过CAS(Compare And Swap,比较和交换)来实现,在对数据操作前比较当前值和传入的值,如果一样则更新,否则不执行更新操作,直接返回失败状态;
- 乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量;
- 悲观锁采用悲观思想处理数据,在读数据时认为别人会修改数据,所以每次读写数据时都会上锁;
- Java中的悲观锁大部分基于AQS(Abstract Queued Synchronized,抽象的队列同步器)实现;
2 公平锁与非公平锁
- 公平锁指不同的线程竞争锁的机制是公平的,即遵循先到先得原则;
- 非公平锁指不同的线程竞争锁的机制是不公平的,指JVM遵循随机、就近原则分配锁的机制;
- 公平锁需要在多核的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多。Java中的synchronized是非公平锁,ReentrantLock默认也是非公平锁;
3 共享锁和独占锁
- 独占锁:也叫互斥锁,每次只允许一个线程持有该锁,ReentrantLock为独占锁的实现;
- 共享锁:允许多个线程同时获取该锁,并发访问共享资源。ReentrantReadWriteLock中的读锁为共享锁的实现;
4 活锁与死锁
- 活锁:当一个线程响应另一个线程的行为而运行,并且另一个线程也响应这一个线程的行为而运行时,就可能发生活锁;活锁线程无法继续进行,但线程没有被阻塞,只是忙于互相响应;
让我们通过将其与现实世界联系起来来理解该概念。 考虑两辆车在狭窄桥梁的相对两侧。 一次只能乘一辆车通过桥。 这两辆车的驾驶员都很有礼貌,正在等待对方先通过桥。 他们互相鸣叫,让他们知道他们想让对方先通过。 但是,两者都没有越过桥梁并互相鸣喇叭。 这种情况类似于活锁。
- 死锁:在有多个线程同时被阻塞时,它们之间若互相等待对方释放锁资源,就会出现死锁。为了避免出现死锁,可以为锁操作添加超时时间,在线程持有锁超时后自动释放该锁;
5 重量级锁和轻量级锁、偏向锁
- 重量级锁:基于操作系统的互斥量而实现的锁,会导致进程在用户态和内核态之间切换,开销较大。synchronized属于重量级锁;
- 轻量级锁:相对于重量级锁而言的,核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能;在无竞争的情况下使用CAS原子操作去消除同步使用的互斥量;
- 偏向锁:用于在某个线程获取锁后,消除这个线程锁重入的开销,看起来好像这个线程得到了该锁的偏向;轻量级锁的获取及释放需要多次CAS原子操作,而偏向锁只需在切换ThreadID时执行一次CAS原子操作,因此可以提高锁的运行效率;
- 锁的状态有4种:无锁、偏向锁、轻量级锁和重量级锁。随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再升级到重量级锁,但在Java中锁只单向升级,不会降级;
6 可重入锁
可重入锁也叫递归锁,指在同一线程中,在外层函数获取到该锁后,内层的递归函数仍然可以继续获取该锁。Java中的synchronized和ReentrantLock都是可重入锁。
7 自旋锁
- 自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需等一等(即自旋),在持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在内核状态的切换上导致的锁时间消耗;
- 线程在自旋时会占用CPU,长时间自旋获取不到锁就会造成CPU的浪费,因此需要设定一个自旋等待的最大时间;
- 适合占用锁的时间非常短或锁竞争不激烈的代码块,对性能会有大幅度提升;
8 分段锁
- 分段锁并非实际的锁,而是一种思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步细粒度化,以提高并发效率;
- ConcurrentHashMap在内部就是使用分段锁实现的;
8 synchronized
- synchronized用于为Java对象、方法、代码块提供线程安全的操作;
- synchronized属于独占式的悲观锁:在使用synchronized修饰对象时,同一时刻只能有一个线程对该对象进行访问;在synchronized修饰方法、代码块时,同一时刻只能有一个线程执行该方法体或代码块;
9 ReentrantLock
- ReentrantLock是一个可重入的独占锁,通过AQS(Abstract Queued Synchronized,抽象的队列同步器)来实现锁的获取与释放;
- ReentrantLock支持公平锁和非公平锁的实现;
- ReentrantLock通过在构造函数ReentrantLock(boolean fair)中传递不同的参数来定义不同类型的锁,默认非公平锁;
10 读写锁:ReadWriteLock
- 读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,写锁与写锁互斥;
- 如果系统要求共享数据可以同时支持多个线程并发读,但不支持多个线程并发写,那么使用读锁能很大程度提高效率;如果系统要求共享数据在同一时刻只能有一个线程在写,且写但时候不能读取该共享数据,则需要使用写锁;
public class SafeCache {
private final Map<String, Object> cache = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock(); // 1 定义读锁
private final Lock writeLock = rwLock.writeLock(); // 2 定义写锁
// 3 读数据时加读锁
public Object get(String key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
// 4 写数据时加写锁
public Object put(String key, Object value) {
writeLock.lock();
try {
return cache.put(key, value);
} finally {
writeLock.unlock();
}
}
}