深入理解 ReentrantLock
前言
我们都知道JDK中已经有了synchronized 锁,为什么还要提供 ReentrantLock锁
与 相比 ReentrantLock锁有什么优势?为什么需要提供这个锁?
ReentrantLock 锁 和 synchronized 锁 该怎么选择?
synchronized 锁
- 隐式锁
- 可冲入锁
- 自动释放锁
- 不能人为控制
synchronized 锁 ,自动释放锁,好处是不需要我们担心释放锁,但也带来一个问题,如果下面代码锁总执行时间很长,也就意味着长时间没释放锁,其他线程等待时间就过长。
synchronized (lock){
//时间很长
}
业务中,我们一般都会有执行时间限制,但对于synchronized 来说,我们不能去打断锁,除非我们在里面写一写特定的打断锁代码,比如抛个异常。。
这样虽然也能干,但显得我们代码有点臃肿,远远没有 ReentrantLock 那么灵活。
ReentrantLock用法详解
ReentrantLock 相对于 synchronized 而言 reentrantlock 是显示锁
提供了 Lock(),unLock()加锁释放锁,一般配合try…catch…使用
public static ReentrantLock reentrantLock = new ReentrantLock();
new Thread(()->{
reentrantLock.lock();
try {
System.out.println( Thread.currentThread().getName() + " do some thing");
}finally {
reentrantLock.unlock();
}
}).start();
也提供了尝试获取锁方法
public boolean tryLock()
public boolean tryLock(long timeout, TimeUnit unit)
使用如下
public static void testtryLock() {
boolean b = reentrantLock.tryLock();
if (b) {
try {
TimeUnit.MICROSECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " get lock ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
} else {
try {
TimeUnit.MICROSECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " un lock ");
}
}
其中还提供了可以被打断方法,相比 synchronized 很灵活
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
使用如下
public static void lockInterruptibly() throws InterruptedException {
//可以被打断的锁
reentrantLock.lockInterruptibly();
try {
while (!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName() + " get lock ");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
System.out.println(Thread.currentThread().getName() + " unlock ");
}
}
test
Thread thread = new Thread(() -> {
lockInterruptibly();
try {
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt(); // 锁 被打断
可以看到,打断后释放锁
锁的公平性与非公平性
Sync是一个抽象类,它有两个子类FairSync与NonfairSync,
分别对应公平锁和非公平锁。从下面的ReentrantLock构造函数可以看出,会传入一个布尔类型的变量fair指定锁是公平的还是非公平的,默认为非公平的。
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平就是排队一个一个来
非公平就是你可以插队先来
阻塞队列与唤醒机制
对于 synchronized 来说,我们经常和wait 和 notify 配合使用,可以用来阻塞和唤醒
同样 ReentrantLock 也提供了一个接口配合使用 Condition。
Condition本身也是一个接口,其功能和wait/notify类似,功能比 wait 和 notify 更加精细。
synchronized 搭配 wait 和 notify 来阻塞和唤醒,比如在生产者和消费者模式中,唤醒线程,唤醒的是全部的线程,可能唤醒生产者线程了,此时队列是满的,还得继续阻塞等待,继续唤醒消费者线程。所以这样就存在一个问题。唤醒不够精细,此时需要唤醒消费者线程,而otifyall唤醒全部线程,可能生产者抢到,也可能消费者抢到。
对于synchronized来说,会将所有阻塞的都放在一个阻塞队列中,等待通知时从里面通知,不知道哪个线程会出来抢占锁。
ReentrantLock 支持多个,不同的Condition唤醒和通知不同的线程
对此 Condition 就比较精细化了,分别用不同的Condition对生产者和消费者进行阻塞和通知
private final static Lock lock = new ReentrantLock();
private final static Condition produceCond = lock.newCondition();
private final static Condition comsumerCond = lock.newCondition();
private final static LinkedList<Long> TIME_POOL = new LinkedList<>();
private final static int MAX = 100;
public static void main(String[] args) {
IntStream.rangeClosed(0,5).forEach(x->{
new Thread(() -> {
for (; ; ) {
buildData();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},x+":P").start();
});
IntStream.rangeClosed(0,10).forEach(x->{
new Thread(() -> {
for (; ; ) {
consumeData();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},x+":C ").start();
});
}
static void buildData() {
lock.lock();
try {
while (TIME_POOL.size() >= MAX) {
try {
produceCond.await(); // 相当于 wait()
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Long value = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + " p -> " + value);
TIME_POOL.addLast(value);
produceCond.signalAll(); // 相当于 notifyAll();
} finally {
lock.unlock();
}
}
static void consumeData() {
lock.lock();
try {
while (TIME_POOL.isEmpty()) {
try {
comsumerCond.await(); // 相当于 wait()
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Long value = TIME_POOL.removeFirst();
System.out.println(Thread.currentThread().getName() + " c -> " + value);
comsumerCond.signalAll(); // 相当于 notifyAll();
} finally {
lock.unlock();
}
}
Condition必须和Lock一起使用,所以Condition的实现也是Lock的一部分。首先查看互斥锁和
读写锁中Condition的构造方法
public class ReentrantLock implements Lock, java.io.Serializable {
// ...
public Condition newCondition() {
return sync.newCondition();
}
}
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
//...
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
// ...
public static class ReadLock implements Lock, java.io.Serializable {
// 读锁不支持Condition
public Condition newCondition() {
// 抛异常
throw new UnsupportedOperationException();
}
}
public static class WriteLock implements Lock, java.io.Serializable {
// ...
public Condition newCondition() {
return sync.newCondition();
}// ...
//
}
// ...
}
读写锁中的 ReadLock 是不支持 Condition 的,读写锁的写锁和互斥锁都支持Condition。虽
然它们各自调用的是自己的内部类Sync,但内部类Sync都继承自AQS。因此,上面的代码
sync.newCondition最终都调用了AQS中的newCondition:
每一个Condition对象上面,都阻塞了多个线程。因此,在ConditionObject内部有一个单向链表
组成的队列
public final void await() throws InterruptedException {
// 刚要执行await()操作,收到中断信号,抛异常
if (Thread.interrupted()) throw new InterruptedException();
// 加入Condition的等待队列
Node node = addConditionWaiter();
// 阻塞在Condition之前必须先释放锁,否则会死锁
int savedState = fullyRelease(node);
int interruptMode = 0; while (!isOnSyncQueue(node)) {
// 阻塞当前线程
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break;
}
// 重新获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
// clean up
if cancelled unlinkCancelledWaiters();
if (interruptMode != 0)
// 被中断唤醒,抛中断异常
reportInterruptAfterWait(interruptMode);
}
signal
public final void signal() {
// 只有持有锁的线程,才有资格调用signal()方法
if (!isHeldExclusively()) throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
// 发起通知
doSignal(first);
}
// 唤醒队列中的第1个线程
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
}
while (!transferForSignal(first) && (first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false;
// 先把Node放入互斥锁的同步队列中,再调用unpark方法
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
当一个线程在调用了await方法以后,直到线程等待的某个条件为真的时候才会被唤醒。这种方式为线程提供了更加简单的等待/通知模式。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。
条件队列转同步队列可以参考 CyclicBarrie里的debug流程
Lock接口的主要方法如下
void lock():
给对象加锁,如果锁未被其他线程使用,则当前线 程将获取该锁;如果锁正在被其他线程持有,则将禁用当前线程,直到当
前线程获取锁。
boolean tryLock():
试图给对象加锁,如果锁未被其他线程使 用,则将获取该锁并返回true,否则返回false。tryLock()和lock()的区
别在于tryLock()只是“试图”获取锁,如果没有可用锁,就会立即返回。
lock()
在锁不可用时会一直等待,直到获取到可用锁。
tryLock(long timeout TimeUnit unit):
创建定时锁,如果在给定的等待时间内有可用锁,则获取该锁。
void unlock():
释放当前线程所持有的锁。锁只能由持有者释放,如果当前线程并不持有该锁却执行该方法,则抛出异常。
Condition newCondition():
创建条件对象,获取等待通知组件。该组件和当前锁绑定,当前线程只有获取了锁才能调用该组件的await(),
在调用后当前线程将释放锁。
getHoldCount():
查询当前线程保持此锁的次数,也就是此线程执行lock方法的次数。
getQueueLength():
返回等待获取此锁的线程估计数,比如启动 5个线程,1 个线程获得锁,此时返回4。getWaitQueueLength(Condition condition):返回在Condition条件下等待该锁的线程数量。比如有 5 个线程用同一个condition对象,并
且这 5 个线程都执行了condition对象的await方法,那么执行此方法将返
回5。
hasWaiters(Condition condition):
查询是否有线程正在等待与 给定条件有关的锁,即对于指定的contidion对象,有多少线程执行了
condition.await方法。
hasQueuedThread(Thread thread):
查询给定的线程是否等待获取该锁。
hasQueuedThreads():
查询是否有线程等待该锁。
isFair():
查询该锁是否为公平锁。
isHeldByCurrentThread():
查询当前线程是否持有该锁,线程执行lock方法的前后状态分别是false和true。
isLock():
判断此锁是否被线程占用。
lockInterruptibly():
如果当前线程未被中断,则获取该锁。
synchronized和ReentrantLock的比较
synchronized和ReentrantLock的共同点如下。
- 都用于控制多线程对共享对象的访问。
- 都是可重入锁。
- 都保证了可见性和互斥性。
synchronized和ReentrantLock的不同点如下。
- ReentrantLock显式获取和释放锁;synchronized隐式获取和释放
锁。为了避免程序出现异常而无法正常释放锁,在使用ReentrantLock时必
须在finally控制块中进行解锁操作。 - ReentrantLock可响应中断、可轮回,为处理锁提供了更多的灵活
性。 - ReentrantLock是API级别的,synchronized是JVM级别的。
- ReentrantLock可以定义公平锁。
- ReentrantLock通过Condition可以绑定多个条件。
- 二者的底层实现不一样:synchronized是同步阻塞,采用的是悲观
并发策略;Lock是同步非阻塞,采用的是乐观并发策略。 - Lock 是 一 个 接 口 , 而 synchronized 是 Java 中 的 关 键 字 ,
synchronized是由内置的语言实现的。 - 我们通过Lock可以知道有没有成功获取锁,通过synchronized却无
法做到。 - Lock可以通过分别定义读写锁提高多个线程读操作的效率。
其他知识点
Java 多线程基础
深入理解aqs
ReentrantLock用法详解
深入理解信号量Semaphore
深入理解并发三大特性
并发编程之深入理解CAS
深入理解CountDownLatch
Java 线程池