ReentrantLock又名可重入锁,读音为[rɪ'entrənt],是entrant的同根词,拆分开来为“重复的-进入的-锁”。可重入指的是任意线程获取到某个锁,并且在该锁还未释放的情况下可再次获取到该锁。重入锁的实现主要需要解决两个问题:
- 线程再次获取锁。当前线程获取锁的时候需要判断其是否为当前已经占据锁的线程,如果是则让其可再次获取到锁,否则会被阻塞;
- 锁的最终释放。已经获取n次锁的线程需要进行n此release操作其他线程才能使用该锁。
如下示例展示了ReentrantLock的可重入特性:
public class Service { private static final int TIMES = 2; private Lock lock = new ReentrantLock(); public void reenter() { for (int i = 0; i < TIMES; i++) { lock.lock(); System.out.println(Thread.currentThread().getName() + " enter lock, times = " + i); } for (int i = 0; i < TIMES; i++) { lock.unlock(); System.out.println(Thread.currentThread().getName() + " release lock, times = " + i); } }}
以下是主程序代码:
public class App { public static void main(String[] args) { Service service = new Service(); Runnable runnable = () -> service.reenter(); Thread threadA = new Thread(runnable, "thread A"); Thread threadB = new Thread(runnable, "thread B"); threadA.start(); threadB.start(); }}
运行结果如下:
thread A enter lock, times = 0thread A enter lock, times = 1thread A release lock, times = 0thread A release lock, times = 1thread B enter lock, times = 0thread B enter lock, times = 1thread B release lock, times = 0thread B release lock, times = 1
可以看到线程A首先获取到锁,此时线程B被阻塞,而A在获取到锁之后再次获取了锁,这说明了ReentrantLock的可重入性,后面A两次释放锁之后B才能获取锁并执行相关代码。
ReentrantLock除了是可重入的以外,在其构造方法中还可以传入一个boolean类型的参数,以表示当前的锁是公平锁还是非公平锁(默认是非公平锁)。在上述示例代码中,当某个线程调用lock.lock()方法获取锁之后,其余的线程将会被阻塞在该方法处,所谓的阻塞其实是指这些线程会按顺序进入一个阻塞队列,当获取锁的线程执行完毕释放锁之后(其可能再次尝试获取锁),阻塞队列中的锁会按照一定的规则从队列中弹出而获取锁。这里获取锁的规则则是分为公平和非公平两种方式,具体的定义如下:
- 公平锁:所有尝试获取锁的线程都会被插入阻塞队列尾部,在拥有锁的线程执行完毕之后按照先进先出的规则从队列中弹出线程执行锁定代码;
- 非公平锁:初次调用lock.lock()方法的线程以及在阻塞队列头的线程都有权争夺锁的控制权,如果获取到了则执行后续代码,否则其会被加入到阻塞队列尾部。
如下示例展示了ReentrantLock的公平性与非公平性:
public class Service { private static final int TIMES = 3; private final MyReentrantLock lock1 = new MyReentrantLock(true); private final MyReentrantLock lock2 = new MyReentrantLock(false); public void fair() { testlock(lock1); } public void unfair() { testlock(lock2); } private void testlock(MyReentrantLock lock) { for (int i = 0; i < TIMES; i++) { lock.lock(); try { System.out.println("running: " + Thread.currentThread().getName() + "\twaiting: " + lock.printQueuedThreads()); } finally { lock.unlock(); } } } private static final class MyReentrantLock extends ReentrantLock { private static final String EMPTY = ""; public MyReentrantLock(boolean fair) { super(fair); } public String printQueuedThreads() { Listthreads = new ArrayList<>(super.getQueuedThreads()); Collections.reverse(threads); return printThreads(threads); } private String printThreads(List threads) { StringBuilder result = new StringBuilder("["); threads.forEach(thread -> result.append(thread.getName()).append(", ")); if (result.length() == 1) { return EMPTY; } return result.delete(result.lastIndexOf(","), result.length()).append("]").toString(); } }}
以下是主程序代码:
public class App { public static void main(String[] args) { Service service = new Service(); Runnable fairTask = () -> service.fair(); Runnable unfairTask = () -> service.unfair(); execTask(fairTask, service, "fair");// execTask(unfairTask, service, "unfair"); } private static void execTask(Runnable task, Service service, String prefix) { Thread thread1 = new Thread(task, prefix + " 1"); Thread thread2 = new Thread(task, prefix + " 2"); Thread thread3 = new Thread(task, prefix + " 3"); Thread thread4 = new Thread(task, prefix + " 4"); Thread thread5 = new Thread(task, prefix + " 5"); thread1.start(); thread2.start(); thread3.start(); thread4.start(); thread5.start(); }}
执行主程序代码,可得到类似以下运行结果:
running: fair 1 waiting: running: fair 2 waiting: [fair 3, fair 4, fair 5, fair 1]running: fair 3 waiting: [fair 4, fair 5, fair 1, fair 2]running: fair 4 waiting: [fair 5, fair 1, fair 2, fair 3]running: fair 5 waiting: [fair 1, fair 2, fair 3, fair 4]running: fair 1 waiting: [fair 2, fair 3, fair 4, fair 5]running: fair 2 waiting: [fair 3, fair 4, fair 5, fair 1]running: fair 3 waiting: [fair 4, fair 5, fair 1, fair 2]running: fair 4 waiting: [fair 5, fair 1, fair 2, fair 3]running: fair 5 waiting: [fair 1, fair 2, fair 3, fair 4]running: fair 1 waiting: [fair 2, fair 3, fair 4, fair 5]running: fair 2 waiting: [fair 3, fair 4, fair 5]running: fair 3 waiting: [fair 4, fair 5]running: fair 4 waiting: [fair 5]running: fair 5 waiting:
可以看到,对于公平锁,每次执行的线程都和阻塞队列中阻塞线程的顺序是一致的。将主程序中执行公平锁的代码替换为非公平锁的代码,可得到以下结果:
running: unfair 1 waiting: running: unfair 4 waiting: [unfair 2, unfair 3]running: unfair 4 waiting: [unfair 2, unfair 3]running: unfair 4 waiting: [unfair 2, unfair 3]running: unfair 1 waiting: [unfair 2, unfair 3]running: unfair 1 waiting: [unfair 2, unfair 3]running: unfair 2 waiting: [unfair 3, unfair 5]running: unfair 2 waiting: [unfair 3, unfair 5]running: unfair 2 waiting: [unfair 3, unfair 5]running: unfair 3 waiting: [unfair 5]running: unfair 3 waiting: [unfair 5]running: unfair 3 waiting: [unfair 5]running: unfair 5 waiting: running: unfair 5 waiting: running: unfair 5 waiting:
从结果中可以看出,获取锁的线程的顺序和在队列中的线程的顺序并不完全一致。公平锁的优点主要有两点:①每个线程都按照先后顺序执行锁定代码,不会出现某个线程等待时间过长的现象;②线程间锁竞争次数较少,降低了死锁的出现概率。但是公平锁的缺点也很明显,即由于每个线程是按照先后顺序执行任务,因而线程间切换的次数较高,这将消耗大量资源。相比较而言,非公平锁虽然出现死锁的概率较大,但这可以理解为代码问题,而不是锁的问题,另外,对于等待过长的线程,这个问题可以通过Condition的await()和signal()方法控制来减少阻塞队列中等待线程的数量,以此降低线程被长久阻塞的问题。相比较而言,非公平锁的优点则非常明显,通过上述实例代码可以看出,同样的代码,公平锁需要进行14次线程上下文环境的切换,而非公平锁只需要进行5次切换,这将极大的提高线程执行效率。
相对于synchronized,Java的concurrent包中的相关组件为我们使用锁提供了非常大的灵活性,但是其也有一些固定的使用模式,而ReentrantLock作为Lock框架的一员,也需要遵循相关的使用方式,如下是使用Lock框架时的一个固定模式:
public class FixedConstruct { private final Lock lock = new ReentrantLock(); public void method() { lock.lock(); try { // doSomething } finally { lock.unlock(); } }}
在调用lock.lock()方法时,当前锁只允许一个线程获取锁并执行后续代码,而其余的线程将会被阻塞,如果获取锁的线程抛出异常而退出了,那么此时锁是没有释放的,其他的线程也就会被一直阻塞。因而需要按照上述代码所示,在调用lock.lock()方法之后使用try...finally保证当前获取锁的线程肯定会释放锁。