参考答案
关于重入锁的“重入”:
1. 重进入
1.1 定义
重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。
1.2 需要解决两个问题
- 线程再次获取锁:锁需要识别获取锁的现场是否为当前占据锁的线程,如果是,则再次成功获取;
- 锁的最终释放:线程重复n次获取锁,随后在第n次释放该锁后,其他线程能够获取该锁。要求对锁对于获取进行次数的自增,计数器对当前锁被重复获取的次数进行统计,当锁被释放的时候,计数器自减,当计数器值为0时,表示锁成功释放。
1.3 重入锁实现重入性
每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。
2. ReentrantLock是在非公平类中,通过组合自定义同步器来实现锁的获取与释放。
/** * Sync中的nonfairTryAcquire()方法实现 * 这个跟公平类中的实现主要区别在于不会判断当前线程是否是等待时间最长的线程 **/ final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 跟FairSync中的主要区别,不会判断hasQueuedPredecessors() if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
nonfairTryAcquire()方法中,增加了再次获取同步状态的处理逻辑,通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。
成功获取锁的现场再次获取锁,只是增加了同步状态值,要求ReentrantLock在释放同步状态时减少同步状态值。
/** * Sync中tryRelease() **/ protected final boolean tryRelease(int releases) { // 修改当前锁的状态 // 如果一个线程递归获取了该锁(也就是state != 1), 那么c可能不等0 // 如果没有线程递归获取该锁,则c == 0 int c = getState() - releases; // 如果锁的占有线程不等于当前正在执行释放操作的线程,则抛出异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // c == 0,表示当前线程释放锁成功,同时表示递归获取了该锁的线程已经执行完毕 // 则设置当前锁状态为free,同时设置锁的当前线程为null,可以让其他线程来获取 // 同时也说明,如果c != 0,则表示线程递归占用了锁资源, // 所以锁的当前占用线程依然是当前释放锁的线程(实际没有释放) if (c == 0) { free = true; setExclusiveOwnerThread(null); } // 重新设置锁的占有数 setState(c); return free; }
如果该锁被获取n次,则前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才返回true,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。
对于公平锁来说:
/** * FairSync中tryAcquire()的实现 * 返回 * true: 获取锁成功 * false: 获取锁不成功 **/ protected final boolean tryAcquire(int acquires) { // 获取当前线程 final Thread current = Thread.currentThread(); // 获取锁资源的状态 // 0: 说明当前锁可立即获取,在此种状态下(又是公平锁) // >0并且当前线程与持有锁资源的线程是同一个线程则state + 1并返回true // >0并且占有锁资源的不是当前线程,则返回false表示获取不成功 int c = getState(); if (c == 0) { // 在锁可以立即获取的情况下 // 首先判断线程是否是刚刚释放锁资源的头节点的下一个节点(线程的等待先后顺序) // 如果是等待时间最长的才会马上获取到锁资源,否则不会(这也是公平与不公平的主要区别所在) if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { //线程可以递归获取锁 int nextc = c + acquires; // 超过int上限值抛出错误 if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
与非公平唯一的区别是判断条件中多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回了true,则表示有线程比当前线程更早地请求获取锁,所以需要等待前驱线程获取并释放锁后才能继续获取该锁。
但是,非公平锁是默认实现:非公平性锁可能使线程“饥饿”,但是极少的线程切换,可以保证其更大的吞吐量。而公平性锁,保证了锁的获取按照FIFO原则,代价是进行大量的线程切换。
3. synchronized可重入性
同一线程在调用自己类中的其他synchronized方法/块,或调用父类的synchronized方法/块,都不会阻碍该线程的执行。
即同一线程对同一个对象锁是可重入的,且同一个线程可以获取同一把锁多次,也就是可以多次重入。
以上,是Java面试题【关于重入锁,你是怎么理解“重入”的】的参考答案。
输出,是最好的学习方法。
欢迎在评论区留下你的问题、笔记或知识点补充~
—end—