ReentrantLock的概念
ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。相比synchronized,ReentrantLock具备以下特点:
可中断
可以设置超时时间
可以设置为公平锁
支持多个条件变量
与 synchronized 一样,都支持可重入
ReentrantLock与synchronized的区别
synchronized和ReentrantLock的区别:
synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的;
在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally块中显示释放锁;
ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;
使用ReentrantLock
同步执行
public class ReentrantLockDemo {
private static int sum = 0;
private static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(()->{
//加锁
lock.lock();
try {
// 临界区代码
// TODO 业务逻辑:读写操作不能保证线程安全
for (int j = 0; j < 10000; j++) {
sum++;
}
} finally {
// 解锁--一定要在finally中解锁,防止业务代码异常,无法释放锁
lock.unlock();
}
});
thread.start();
}
Thread.sleep(2000);
System.out.println(sum);
}
}
必须要在finally种解锁,防止业务抛出异常,导致锁永远放不掉。
可重入锁
可重入锁就是 A(加锁)–>调用—>B(加锁)–>调用–>C(加锁),从A到C即使B/C都有加锁,也可以进入。
public class ReentrantLockDemo2 {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
}
锁中断
可以使用lockInterruptibly来进行锁中断
lockInterruptibly()方法能够中断等待获取锁的线程。当两个线程同时通过lock.lockInterruptibly()获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
public class ReentrantLockDemo3 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("t1启动...");
try {
lock.lockInterruptibly();
try {
log.debug("t1获得了锁");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("t1等锁的过程中被中断");
}
}, "t1");
lock.lock();
try {
log.debug("main线程获得了锁");
t1.start();
//先让线程t1执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
log.debug("线程t1执行中断");
} finally {
lock.unlock();
}
}
}
锁等待超时
可以让线程等待指定的时间,如果还未获取锁则进行失败处理。
如下代码,首先让主线程获得锁,然后让子线程启动尝试获取锁,但是由于主线程获取锁之后,让线程等待了2秒,而子线程获得锁的超时时间只有1秒,如果未获得锁,则进行return失败处理
Thread t1 = new Thread(() -> {
log.debug("t1启动...");
//超时
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("等待 1s 后获取锁失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
try {
log.debug("t1获得了锁");
} finally {
lock.unlock();
}
}, "t1");
上面的代码线程1修改如下逻辑,主线程sleep(2000)
公平锁
公平锁和非公平锁:
非公平锁是ReentrantLock的默认实现,当AQS阻塞队列中有node等待时,前面释放了锁,这时又来了一个新线程要求获取锁。这时候新线程可以直接跟等锁的node竞争,并可能获取到锁,使node再次加锁失败。
公平锁就是有新线程要求锁时,先判断等待队列中是否有节点,有的话要排到队尾等待。
ReentrantLock默认不公平。
分两个循环各创建500个线程占用锁,公平锁情况下,上面500个都执行完了,下面才能一起去获取锁。
public class ReentrantLockDemo5 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock(true); //公平锁
for (int i = 0; i < 500; i++) {
new Thread(() -> {
lock.lock();
try {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
for (int i = 0; i < 500; i++) {
new Thread(() -> {
lock.lock();
try {
log.debug(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "强行插入" + i).start();
}
}
}
那ReentrantLock为什么默认使用非公平锁呢?实际上就是为了提高性能,如果使用公平锁,当前锁对象释放之后,还需要去队列中获取第一个排队的线程,然后进行加锁处理。而非公平锁,可能再当前对象释放锁之后,正好有新的线程在获取锁,这样就可以直接进行加锁操作,不必再去队列中读取。
reentrantLock代码分析
ReentrantLock基于AQS开发,核心成员变量有:sync、state。sync即锁的同步式控制。此外就是AQS继承来的volatile变量state,它是锁控制的关键
private final Sync sync;
private volatile int state; // 继承自AQS
其中包含两个子类:NonfairSync和FairSync。
sync继承自AbstractQueuedSynchronizer,自然也继承了exclusiveOwnerThread持锁对象。
非公平锁NonfairSync
lock - 加锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
非公平锁的加锁方法,可见上来是先检查状态为0,配置为1。如果配置成功,则加锁成功,然后将当前线程信息配置给exclusiveOwnerThread持锁对象。如果配置不成功,则意味着没竞争到锁,则尝试基于AQS的逻辑重新获取锁。
nonfairTryAcquire
NonfairSync对tryAcquire的实现委托给nonfairTryAcquire方法实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
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;
}
如果当前state=0,则可以正常加锁,修改state为1并且配置持锁线程。
如果state不为0,判断当前线程是否为持锁线程,如果是,则state自增。这里是可重入锁的逻辑,是否的时候也得一层一层释放。
如果state不为0,持锁线程也不是当前线程,则证明加不上锁,返回false
这里需要关注的是state是volatile变量,具备同步特性,因此就等于锁被本线程独占了。
tryRelease
非公平锁的tryRelease方法如下:
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;
}
因为是可重入锁,这里是要state进行一层一层释放,直到c为0,才真正说明锁已经释放了,把持锁线程set为null。
可以看到,释放锁最后写state值
公平锁FairSync
公平锁的tryAcquire没有委托,与NonfairSync调用的nonFairTryAcquire相比,区别在于加锁的时候做了额外判断:
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
比nonFair多判断了一个!hasQueuedPredecessors,即加入了同步队列中当前节点是否有前驱节点的判断
评论区