一,ReentrantLock类简介
ReentrantLock 类,实现了 Lock 接口,是一种可重入的独占锁,在同一个时间点只能被一个线程锁持有
,可重入表示,ReentrantLock锁可以被同一个线程多次获取。
通过 lock
生成可一个与之绑定的 Condition
对象,用来替代传统的 Object 的 wait()、notify() 实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的 await()、signal()
这种方式实现线程间协作更加安全和高效。
它具有与使用 synchronized 相同的一些基本行为和语义,但功能更灵活更强大。ReentrantLock 内部通过内部类实现了AQS 框架(AbstractQueuedSynchronizer) 的 API 来实现独占锁的功能。
二,主要方法
lock()
①获取锁成功:线程直接返回;
②获取锁失败:加入等待队列,直到获取锁成功;
③等待过程中被中断:不响应,直到获取锁后再自我中断 selfInterrupt()。
tryLock()
线程尝试获取锁,如果获取成功,则返回 true,如果获取失败,则返回 false。
tryLock(long timeout,TimeUnit unit)
线程如果在指定等待时间内获得了锁,就返回true,否则返回 false。
lockInterruptibly()
对线程 interrupt 方法做出响应。
三,ReentrantLock类原理
通过内部类实现了 AQS 框架,Lock 接口的实现仅仅是对 AQS 的 api 的简单封装。
lock() 源码
public void lock() {
sync.lock();
}
Sync 继承 AbstractQueuedSynchronizer (AQS),对AQS 的方法进行封装。
非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 调用AQS 中的方法
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
可见,非公平锁中,首先通过 compareAndSetState 抢锁,抢锁失败,再调用的是 AQS 中的 acquire 获取独享锁方法 AQS源码解读-独占模式下获取锁和释放锁
公平锁
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;
}
当 state = 0 时,通过 hasQueuedPredecessors
先判断等待队列中是否有节点,有节点在等待则放弃争抢锁,体现了获取锁的公平性。
四,中断响应
对于 synchronized 来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待,synchronized在等待锁时不能被中断
。
而使用重入锁,则提供另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,通过 lockInterruptibly() 方法,程序可以根据需要取消对锁的请求。
五,锁申请等待限时
我们可以使用 tryLock()
方法尝试获得锁,超过限定时间仍未成功获得锁,将放弃对锁的获取。可以避免死锁。
六,公平锁
ReentrantLock 类的其中一个构造器提供了指定公平策略 / 非公平策略的功能,默认为非公平策略。
公平策略
:在多个线程争用锁的情况下,公平策略倾向于将访问权授予等待时间最长的线程。也就是说,相当于有一个线程等待队列,先进入等待队列的线程后续会先获得锁,这样按照“先来后到”的原则,对于每一个等待线程都是公平的。
非公平策略
:在多个线程争用锁的情况下,能够最终获得锁的线程是随机的(由底层OS调度)。
七,简单案例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockService implements Runnable {
private static ReentrantLock lock = new ReentrantLock();
private static int i = 0;
@Override
public void run() {
for (int j = 0; j < 100; j++) {
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockService service = new ReentrantLockService();
Thread t1 = new Thread(service);
Thread t2 = new Thread(service);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
八,重入锁好搭档:Condition条件
Condtion是与重入锁相关联的,通过Lock接口(重入锁就实现了这一接口) 的Condition newCondition() 方法可以生成一个与当前重入锁绑定的 Condition 实例。利用 Condition 对象,我们就可以让线程在合适的时间等待,或者在某一个特定的时刻得到通知,继续执行。
当线程使用 Condition await时,要求线程持有相关的重入锁,在 Condition await调用后,这个线程会释放这把锁。同理,在 Condition signal() 方法调用时,也要求线程先获得相关的锁
。在 signal()方法调用后,系统会从当前 Condition 对象的等待队列中,唤醒一个线程。一旦线程被唤醒,它重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。因此,在 signal()方法调用之后,一般需要释放相关的锁,谦让给被唤醒的线程,让它可以继续执行。
几个重要方法:
await()
:方法会使当前线程等待,同时释放当前锁,当其他线程中使用 signal() 或者 signal All() 方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和 Object. wait()方法很相似。
awaitUninterruptibly()
:方法与 await() 方法基本相同,但是它并不会在等待过程中响应中断。
singal()
:方法用于唤醒一个在等待中的线程。相对的 singal All() 方法会唤醒所有在等待中的线程。
九,ReentrankLock 结合 Condition 使用案例
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//用ReentrantLock绑定三个条件实现线程A打印一次1,线程B打印两次2,线程C打印三次3
class ReentrantLockService {
private int number = 1;//A:1 B:2 C:3
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
//1 判断
public void print1() {
lock.lock();
try {
//判断
while (number != 1) {
c1.await();
}
//2 do sth
for (int i = 0; i < 1; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + number);
}
//3 通知
number = 2;
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//1 判断
public void print2() {
lock.lock();
try {
//判断
while (number != 2) {
c2.await();
}
//2 do sth
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + number);
}
//3 通知
number = 3;
c3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//1 判断
public void print3() {
lock.lock();
try {
//判断
while (number != 3) {
c3.await();
}
//2 do sth
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + number);
}
//3 通知
number = 1;
c1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockService resource = new ReentrantLockService();
new Thread(()->{
for (int i = 1; i <= 2; i++) {
resource.print1();
}
},"A").start();
new Thread(()->{
for (int i = 1; i <= 2; i++) {
resource.print2();
}
},"B").start();
new Thread(()->{
for (int i = 1; i <= 2; i++) {
resource.print3();
}
},"C").start();
}
}
执行结果:
十,Reentranklock 和 synchronization对比
Synchronized
:它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
ReentrantLock
:它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
用途比较:
Reentranklock 等待可中断
,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况;
Reentranklock 锁申请等待限时
,避免产生死锁;
Reentranklock 可实现公平锁
,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好;
Reentranklock 锁可以绑定多个条件
,一个ReentrantLock对象可以同时绑定多个 Condition 对象,使用更灵活;
评论区