我们来介绍一下与锁相关的几个概念。
- 可重入锁(Reentrant Lock)
- 公平锁(fair locking)和非公平锁(unfair locking)
- 可中断锁(Interruptable Lock)
- 可限时锁(Timed Lock)
- 读写锁(Read-Write Lock)
- 自旋锁(Spin Lock)
- 适应性自旋锁(Adaptive Spinning Lock)
- 独享锁(Exclusive Lock) VS 共享锁(Shared Lock)
- 偏向锁(Biased Lock)
可重入锁(Reentrant Lock)
如果锁具备可重入性(Reentracy),则称作为可重入锁(Reentrant Lock)。**可重入锁(Reentrant Lock)**允许一个线程可以反复得到相同的一把锁,
synchronized、 ReentrantLock 和 ReentrantReadWriteLock 都是可重入锁。
本质上,可重入锁有一个与锁相关的获取计数器,如果拥有锁的某个线程尝试再次得到锁,那么获取计数器就加1。最终,当锁被释放两次后,该锁才会真正被释放。
可重入锁对于锁基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个 synchronized 方法时,比如说 method1,而在 method1 中会调用另外一个 synchronized 方法 method2,此时线程不必重新去申请锁,而是可以直接执行 method2 方法。
以代码说明:
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method1() {
...
}
}
上述代码中的两个方法 method1 和 method2 都是 synchronized 方法。
假如某一时刻,线程A执行到了 method1 ,此时线程A获取了这个对象的锁,而由于 method2 也是synchronized方法。假如 synchronized 不具备可重入性,此时当线程A 进入 method2 方法的调用时,需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
公平锁(Fair Lock)和非公平锁(Unfair Lock)
Concept
何谓公平性(fairness),是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合线程请求锁时的绝对时间顺序,满足 FIFO。换句话说,等待时间最久的线程(最先请求的线程)会先获得该锁。
满足公平性的锁,就称为公平锁(fair locking),反之,称为非公平锁(unfair locking)。
在 Java 中,synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于 ReentrantLock 和 ReentrantReadWriteLock,它在默认情况下是非公平锁,但是可以设置为公平锁。
公平锁 VS 非公平锁
公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象(starvation)。
公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock 默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
所以,我们经常会将一个mutex设计为非公平锁,而又尽可能地不让它发生starvation,并引入一个机制来处理当starvation发生时的情况,以尽可能的减少starvation的时长。
可中断锁(Interruptable Lock)
可中断锁 (Interruptible Lock),顾名思义,就是可以被中断持有的锁。
比如,某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,由于等待时间过长,线程B急着想处理一个优先级相对更高的事务,我们可以让线程A中断自己正在持有的锁,或者线程B中断线程A正在持有的锁,这就是可中断锁(Interruptable Lock)。
在 Java 中,synchronized 就不是可中断锁,而 ReentrantLock 和 ReentrantReadWriteLock 都是可中断锁。
可中断锁解决死锁
可中断锁可以帮助解决死锁(deadlock)问题,比如:
import java.util.concurrent.locks.ReentrantLock;
public class IntLock implements Runnable{
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
/**
* 控制加锁顺序,产生死锁
*/
public IntLock(int lock) {
this.lock = lock;
}
public void run() {
try {
if (lock == 1) {
lock1.lockInterruptibly(); // 如果当前线程未被中断,则获取锁。
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+",执行完毕!");
} else {
lock2.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+",执行完毕!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 查询当前线程是否保持此锁。
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
System.out.println(Thread.currentThread().getName() + ",退出。");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock intLock1 = new IntLock(1);
IntLock intLock2 = new IntLock(2);
Thread thread1 = new Thread(intLock1, "线程1");
Thread thread2 = new Thread(intLock2, "线程2");
thread1.start();
thread2.start();
Thread.sleep(1000);
thread2.interrupt(); // 中断线程2,且释放线程2持有的所有锁,同时放弃所有锁申请
}
}
上述例子中,线程 thread1 和 thread2 启动后,thread1 立刻先占用 lock1,等待 500ms 后,再占用 lock2;thread2 反之,立刻先占 lock2,等待 500ms 后,后占 lock1。这便形成 thread1 和 thread2 之间的死锁。
当代码运行到 Thread.sleep(1000);
时,main 线程处于休眠(sleep)状态,两线程此时处于死锁的状态,thread2.interrupt();
导致 thread2 被中断(interrupt),且释放 thread2 持有的所有锁,故 thread2 会放弃对 lock1 的申请。这个操作导致 thread1 最终顺利获得 lock2,从而最终解开了死锁。
可限时锁(Timed Lock)
除了可中断锁外,**可限时锁(Timed Lock)**也可以在有些情况下避免死锁的发生。
ReentrantLock
是一种可限时锁(Timed Lock),它提供了 tryLock() 方法,并传入一个超时值作为参数。当在到达超时时间后,仍然没有获得锁,则放弃对锁的持有申请。
在 Java 中,synchronized就不是可限时锁,而 ReentrantLock 和 ReentrantReadWriteLock 都是可限时锁。
读写锁(Read-Write Lock)
读写锁是一种 design pattern,refer to https://swsmile.info/post/design-pattern-concurrency-read-write-pattern/.
读写锁(Read-Write Lock)将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
- 多个读锁之间是不需要互斥的(因为读操作不会改变数据,如果上了锁,反而会影响效率);
- 而写锁和写锁之间需要互斥。也就是说,如果只是读数据,就可以多个线程同时读,但是如果要写数据,就必须互斥,使得同一时刻只有一个线程在操作。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock 就是一个读写锁,它是一个接口,而 ReentrantReadWriteLock 实现了这个接口。
可以通过 readLock()
获取读锁,通过 writeLock()
获取写锁。
自旋锁(Spinlock)
自旋锁也是一种实现互斥(mutual exclusion)的方式,相比经典的互斥锁(Mutex)会在因无法进入临界区而进入阻塞状态(因此互斥锁是一个阻塞锁),因而放弃CPU。
而自旋锁(Spin Lock)是指当一个线程在申请获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断地判断锁是否能够被成功获取,直到获取到锁后,才会退出循环。
在这种情况中,尝试获取锁的线程一直处于 Running 状态,但是并没有执行任何有效的任务,即紧密循环(tight loop),这个过程也称为忙等待(busy waiting)。
Refer to https://swsmile.info/post/Spinlock/
独享锁(Exclusive Lock) VS 共享锁(Shared Lock)
独享锁(Exclusive Lock)也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
共享锁(Shared Lock)是指该锁可被多个线程所持有。典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。
Refer to https://swsmile.info/post/lock-exclusive-lock-vs-shared-lock/
Reference
-
《深入理解Java虚拟机》
-
Java多线程编程那些事:Java虚拟机对内部锁的优化 - https://zhuanlan.zhihu.com/p/30003980
-
【深入理解多线程】Java虚拟机的锁优化技术(五) - https://blog.csdn.net/w372426096/article/details/80079605
-
Java 并发:深入分析 synchronized 的实现原理 - https://juejin.im/entry/58a702b9128fe1006cb91707
-
JDK 5.0 中更灵活、更具可伸缩性的锁定机制 - https://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html
-
Java进阶(二)当我们说线程安全时,到底在说什么 - http://www.jasongj.com/java/thread_safe/
-
Java并发编程:Lock - https://www.cnblogs.com/dolphin0520/p/3923167.html
-
面试必备之深入理解自旋锁 - https://cloud.tencent.com/developer/article/1169074
-
读写自旋锁详解,第 1 部分 - https://www.ibm.com/developerworks/cn/linux/l-cn-rwspinlock1/index.html