什么是“重入”
Re-Entrant-Lock 翻译成重入锁也是非常贴切的。之所以这么叫,那是因为这种锁是可以反复进入的。当然,这里的反复仅仅局限于一个线程,观察下面的代码, f1 锁住 lock 之后, f2 依然能继续获取到 lock 并执行,因为它们都属于主线程。
public class Main {
static class Test {
private final ReentrantLock lock = new ReentrantLock();
public void f1() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName());
f2();
} finally {
lock.unlock();
}
}
public void f2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName());
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
Test test = new Test();
test.f1();
}
}
重入锁 VS synchronized
重入锁可以完全替代 synchronized 关键字。在 JDK 5.0 的早期版本中,重入锁的性能远远好于 synchronized ,但从 JDK 6.0 开始,JDK 在 synchronized 上做了大量的优化,使得两者的性能差距并不大。
用两种方法分别实现 num++:
重入锁
public class Main {
static int num = 0;
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[32];
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
lock.lock();
try {
num++;
} finally {
lock.unlock();
}
}
};
for (int i = 0; i < 32; i++) {
ts[i] = new Thread(runnable);
ts[i].start();
}
for (int i = 0; i < 32; i++) {
ts[i].join();
}
System.out.println(num);
}
}
synchronized
public class Main {
static int num = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[32];
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
synchronized (obj) {
num++;
}
}
};
for (int i = 0; i < 32; i++) {
ts[i] = new Thread(runnable);
ts[i].start();
}
for (int i = 0; i < 32; i++) {
ts[i].join();
}
System.out.println(num);
}
}
响应中断
对于 synchronized 来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。而使用重入锁,则提供另外一种可能,那就是线程可以被中断。
模拟一个死锁的场景:两个线程都需要获取 lock1, lock2 两把锁,线程 t1 先获取了 lock1 ,线程 t2 先获取了 lock2 ,然后它们就开始无限等待对方让出锁。但是这个时候,如果中断了线程 t2 ,并且释放 t2 占有的锁, t1 就能正常运行:
public class Main {
static class IntLock implements Runnable {
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
private int lock;
public IntLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
try {
if (lock == 1) {
lock1.lockInterruptibly();
try {
Thread.sleep(500);
lock2.lockInterruptibly();
try {
System.out.println("t1成功获取两把锁");
} finally {
lock2.unlock();
}
} catch (InterruptedException e) {
System.out.println("t1被中断");
Thread.currentThread().interrupt();
} finally {
lock1.unlock();
}
} else {
lock2.lockInterruptibly();
try {
Thread.sleep(500);
lock1.lockInterruptibly();
try {
System.out.println("t2成功获取两把锁");
} finally {
lock1.unlock();
}
} catch (InterruptedException e) {
System.out.println("t2被中断");
Thread.currentThread().interrupt();
} finally {
lock2.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) throws InterruptedException {
IntLock r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(1000);
t2.interrupt();
}
}
锁申请等待限时
tryLock() 方法接收两个参数,一个表示等待时长,另外一个表示计时单位。也可以不带参数直接运行。在这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回 true 。如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回 false。
public class Main {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Runnable target = () -> {
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " get lock success");
Thread.sleep(4000);
} else {
System.out.println(Thread.currentThread().getName() + " get lock failed");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
};
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
t1.start();
t2.start();
}
}
公平锁
在大多数情况下,锁的申请都是非公平的。也就是说,线程 1 首先请求了锁 A,接着线程 2 也请求了锁 A。那么当锁 A 可用时,是线程 1 可以获得锁还是线程 2 可以获得锁呢?这是不一定的。系统只是会从这个锁的等待队列中随机挑选一个。因此不能保证其公平性。这就好比买票不排队,大家都乱哄哄得围在售票窗口前,售票员忙得焦头烂额,也顾不及谁先谁后,随便找个人出票就完事了。而公平的锁,则不是这样,它会按照时间的先后顺序,保证先到者先得,后到者后得。公平锁的一大特点是:它不会产生饥饿现象。只要你排队,最终还是可以等到资源的。如果我们使用 synchronized 关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们对其公平性进行设置。它有一个如下的构造函数:
public ReentrantLock(boolean fair)
当参数 fair 为 true 时,表示锁是公平的。公平锁看起来很优美,但是要实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能相对也非常低下,因此,默认情况下,锁是非公平的。如果没有特别的需求,也不需要使用公平锁。公平锁和非公平锁在线程调度表现上也是非常不一样的。下面的代码可以很好地突出公平锁的特点:
public class Main {
private static ReentrantLock fairLock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
Runnable target = () -> {
while (!Thread.currentThread().isInterrupted()) {
fairLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获得锁");
} finally {
fairLock.unlock();
}
}
};
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
t1.start();
t2.start();
Thread.sleep(1000);
t1.interrupt();
t2.interrupt();
}
}
从运行结果可以看出,大部分情况下,两个线程是交替运行的。
...
Thread-0 获得锁
Thread-1 获得锁
Thread-0 获得锁
Thread-1 获得锁
Thread-0 获得锁
Thread-1 获得锁
Thread-0 获得锁
Thread-1 获得锁
Thread-0 获得锁
...
Thread-0 获得锁
Thread-1 获得锁
Thread-0 获得锁
Thread-1 获得锁
Thread-0 获得锁
...
但是换成非公平锁之后,会发现连续多次都是同一个线程获取锁。
...
Thread-0 获得锁
Thread-0 获得锁
Thread-0 获得锁
Thread-0 获得锁
Thread-0 获得锁
Thread-0 获得锁
...
Thread-1 获得锁
Thread-1 获得锁
Thread-1 获得锁
Thread-1 获得锁
Thread-1 获得锁
Thread-1 获得锁
Thread-1 获得锁
...