重入锁
重入锁ReentrantLock,顾名思义,就是支持重进入的锁,他表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁还支持获取锁时的公平和非公平选择。
ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。
这里提到一个锁获取公平性问题,如果在绝对时间上,先对锁进行获取的请求一定会先被满足,那么这个锁是公平的,反之则是不公平的,公平的获取锁,也就是等待的时间最长的线程优先获取锁,也可以说锁获取是有顺序的。ReentantLock提供了一个构造函数,能够控制锁是否公平。
事实上,公平的锁机制往往没有非公平的效率高,但是,并不是任何场景都是以TPS(查询率)作为唯一的指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。
- 实现重进入
重进入是值任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取
2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
ReentrantLock获取同步状态的核心代码如下
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取到当前的状态
int c = getState();
//等于0的时候获取锁
if (c == 0) {
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;
}
该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值,核心代码如下
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
如果说该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。
2.公平锁与非公平锁的区别
公平性是否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
对于非公平锁来说,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早的请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
测试观察公平与非公平锁的区别
公平锁代码
package reentrantlock;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author liusupeng
* @date 2021/7/19 22:45
*/
public class FaireAndUnfairTest {
private static final Lock FAIR_LOCK = new ReentrantLock2(true);
private static final Lock UNFAIR_LOCK = new ReentrantLock2(false);
public static void main(String[] args) {
fair();
}
/**
* 公平锁
*/
public static void fair() {
testLock(FAIR_LOCK);
}
private static void testLock(Lock lock) {
/**
* 创建5个线程来执行
*/
for (int i = 1; i <= 5; i++) {
Job job = new Job(lock);
job.start();
}
}
private static class Job extends Thread {
private final Lock lock;
public Job(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 1; i <= 2; i++) {
//先上锁
lock.lock();
try {
//获取等待队列
ArrayList<Thread> list = (ArrayList<Thread>) ((ReentrantLock2) lock).getQueuedThreads();
System.out.print("当前线程的名字:" + this.getName() + "其他线程的名字[");
list.forEach(e -> {
System.out.print(e.getName() + ",");
});
System.out.println("]");
} finally {
//释放锁
lock.unlock();
}
}
}
}
}
class ReentrantLock2 extends ReentrantLock {
public ReentrantLock2(boolean fair) {
super(fair);
}
@Override
public Collection<Thread> getQueuedThreads() {
//获取父类的线程等待队列里的线程数据,并且转换下顺序
List<Thread> arrayList = new ArrayList<Thread>(super.getQueuedThreads());
Collections.reverse(arrayList);
return arrayList;
}
}
输出内容
当前线程的名字:Thread-0其他线程的名字[Thread-1,]
当前线程的名字:Thread-1其他线程的名字[Thread-2,Thread-3,Thread-4,Thread-0,]
当前线程的名字:Thread-2其他线程的名字[Thread-3,Thread-4,Thread-0,Thread-1,]
当前线程的名字:Thread-3其他线程的名字[Thread-4,Thread-0,Thread-1,Thread-2,]
当前线程的名字:Thread-4其他线程的名字[Thread-0,Thread-1,Thread-2,Thread-3,]
当前线程的名字:Thread-0其他线程的名字[Thread-1,Thread-2,Thread-3,Thread-4,]
当前线程的名字:Thread-1其他线程的名字[Thread-2,Thread-3,Thread-4,]
当前线程的名字:Thread-2其他线程的名字[Thread-3,Thread-4,]
当前线程的名字:Thread-3其他线程的名字[Thread-4,]
当前线程的名字:Thread-4其他线程的名字[]
Process finished with exit code 0
非公平锁
package reentrantlock;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author liusupeng
* @date 2021/7/19 22:45
*/
public class FaireAndUnfairTest {
private static final Lock FAIR_LOCK = new ReentrantLock2(true);
private static final Lock UNFAIR_LOCK = new ReentrantLock2(false);
public static void main(String[] args) {
unFair();
}
/**
* 非公平锁
*/
public static void unFair() {
testLock(UNFAIR_LOCK);
}
private static void testLock(Lock lock) {
/**
* 创建5个线程来执行
*/
for (int i = 1; i <= 5; i++) {
Job job = new Job(lock);
job.start();
}
}
private static class Job extends Thread {
private final Lock lock;
public Job(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 1; i <= 2; i++) {
//先上锁
lock.lock();
try {
//获取等待队列
ArrayList<Thread> list = (ArrayList<Thread>) ((ReentrantLock2) lock).getQueuedThreads();
System.out.print("当前线程的名字:" + this.getName() + "其他线程的名字[");
list.forEach(e -> {
System.out.print(e.getName() + ",");
});
System.out.println("]");
} finally {
//释放锁
lock.unlock();
}
}
}
}
}
class ReentrantLock2 extends ReentrantLock {
public ReentrantLock2(boolean fair) {
super(fair);
}
@Override
public Collection<Thread> getQueuedThreads() {
//获取父类的线程等待队列里的线程数据,并且转换下顺序
List<Thread> arrayList = new ArrayList<Thread>(super.getQueuedThreads());
Collections.reverse(arrayList);
return arrayList;
}
}
输出内容
当前线程的名字:Thread-0其他线程的名字[]
当前线程的名字:Thread-0其他线程的名字[Thread-1,Thread-2,Thread-3,Thread-4,]
当前线程的名字:Thread-1其他线程的名字[Thread-2,Thread-3,Thread-4,]
当前线程的名字:Thread-1其他线程的名字[Thread-2,Thread-3,Thread-4,]
当前线程的名字:Thread-2其他线程的名字[Thread-3,Thread-4,]
当前线程的名字:Thread-2其他线程的名字[Thread-3,Thread-4,]
当前线程的名字:Thread-3其他线程的名字[Thread-4,]
当前线程的名字:Thread-3其他线程的名字[Thread-4,]
当前线程的名字:Thread-4其他线程的名字[]
当前线程的名字:Thread-4其他线程的名字[]
观察以上代码可以发现,公平性锁每次都是从同步状态队列中的第一个节点获取到锁,而非公平锁则出现了一个线程连续获取锁的情况
为什么会出现线程连续获取锁的情况呢?
当一个线程请求锁的时候,只要获取了同步状态即成功获取锁,在这个前提下,刚释放的锁的线程再次获取同步状态的几率会非常的大,使得其他线程只能在同步队列中等待。
为什么默认是非公平锁
在进行公平锁和非公平锁的测试时,总耗时是其94.3倍,总切换次数是其133倍。可以看出,公平锁是明显保证了锁的获取FIFO(数据缓冲器)原则,而代价就是进行大量的线程切换。非公平锁虽然可能造成线程饥饿,但是极少的线程切换,保证了其更大的QBS;