有经典,有干货,微信搜索【李子捌】关注这个每日更新的程序员。
简介
重入锁ReentrantLock指的是支持同一个线程对资源的重复加锁。ReentrantLock中有公平锁和非公平锁的两种实现。
synchronized
synchronized关键字支持隐式的重入;当一个线程获取到锁时,是支持这个线程多次获取这个锁的,不会出现自己阻塞自己的情况,并且我们开发过程中对于synchronized关键字也不需要关心锁的释放。举个递归的例子我们来看synchronized关键字对锁的重入。
代码示例
ReentrantLock
ReentrantLock基于AQS和Lock来实现的,那如果是我们ReentrantLock要实现可重入,需要解决和实现如下两个问题:
同一个线程多次获取锁,则需要判断当前来获取锁的线程和占有锁的线程是否为同一个线程
多次获取锁,则需要多次释放这个锁,可以通过一个计数器累加和自减来记录锁的重复获取与释放
ReentrantLock中有两种重入锁的实现
NonfairSync-非公平锁
FairSync-公平锁
公平锁和非公平锁的本质区别就在于,获取锁的顺序是否符合FIFO,对于公平锁来说先加入同步队列等待的线程,必将会先获取到同步状态(锁),对于非公平锁来说,获取到锁的顺序不确定。
简单使用
在进行源码分析之前,先来看看ReentrantLock是如何使用的,ReentrantLock的使用非常简单,示例代码如下:
Sync—ReentrantLock组合的自定义同步器抽象
/** * ReentrantLock内部类Sync,也是其内部组合实现的自定义同步器的抽象 */ abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; /** * 定义获取锁的抽象方法,由NonfairSync和FairSync去实现各自获取锁的方式 */ abstract void lock(); /** * NonfairSync中tryAcquire调用的方法 */ final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 如果当前共享状态未被其他线程占用 if (c == 0) { // 尝试通过CAS占有当前共享状态 if (compareAndSetState(0, acquires)) { // 设置共享状态持有线程为当前线程 setExclusiveOwnerThread(current); return true; } } // 如果共享状态已被占用,则判断当前占用共享状态的线程是否就是当前线程 else if (current == getExclusiveOwnerThread()) { // 如果是则自增获取次数,设值state int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } /** * 释放共享状态 */ protected final boolean tryRelease(int releases) { // 计算减少后的值state int c = getState() - releases; // 判断当前线程和持有共享状态的线程是否是同一个线程,不是则抛出异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 如果state递减后的值为0了,表示线程释放完共享状态,需要情况持有共享状态的线程变量 if (c == 0) { free = true; setExclusiveOwnerThread(null); } // 设置state setState(c); return free; } /** * 判断占用共享状态的线程是否是当前线程 */ protected final boolean isHeldExclusively() { return getExclusiveOwnerThread() == Thread.currentThread(); } /** * 如果state不为0,则获取共享状态的持有线程,否则返回null */ final Thread getOwner() { return getState() == 0 ? null : getExclusiveOwnerThread(); } /** * 如果当前线程持有共享状态,则返回state,否则返回0 */ final int getHoldCount() { return isHeldExclusively() ? getState() : 0; } /** * 判断当前共享状态是否被持有 */ final boolean isLocked() { return getState() != 0; } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } final ConditionObject newCondition() { return new ConditionObject(); } }
FairSync源码分析
/** * 公平锁的代码实现 */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; /** * 调用AQS中的acquire(int arg)尝试获取同步状态,失败则加入等待队列,自旋获取共享状态 */ final void lock() { acquire(1); } /** * 公平锁和非公平锁的主要区别在于此方法 */ protected final boolean tryAcquire(int acquires) { // 获取到当前线程 final Thread current = Thread.currentThread(); // 获取当前同步状态 int c = getState(); // 如果同步状态为0,则说明当前同步状态已完全释放 if (c == 0) { // 1、hasQueuedPredecessors判断当前节点是否存在前驱节点 // 2、如果不存在则CAS设置state的值 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; } }
FairSync能够顺序的获取共享状态,也就是保证加入同步队列的顺序和获取到同步状态的顺序一致,依靠的是hasQueuedPredecessors()这个判断当前节点是否存在前驱节点的判断。
NonfairSync和FairSync区别示例代码
package com.lizba.p6; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; /** * <p> * 公平和非公平锁测试 * </p> * * @Author: Liziba * @Date: 2021/6/21 23:33 */ public class FairAndUnfairTest { /** 定义公平锁 */ private static Lock fairLock = new ReentrantLockCustomize(true); /** 定义非公平锁 */ private static Lock unfairLock = new ReentrantLockCustomize(false); /** * 测试公平锁和非公平锁 * @param lock */ private static void testFairAndUnfairLock(Lock lock) { for (int i = 1; i <= 5; i++) { new Job(lock, ""+i).start(); } } /** * 定义线程实现,打印当前线程和等待队列中的线程 */ private static class Job extends Thread { private Lock lock; public Job(Lock lock,String name) { this.lock = lock; setName(name); } @Override public void run() { // 通过两次输出,来判断是否与队列中一致 for (int i = 0; i < 2; i++) { lock.lock(); try { System.out.println("获取锁的线程:" + Thread.currentThread().getName()); System.out.println("同步队列中的线程:" + ((ReentrantLockCustomize)lock).getQueuedThreads().stream().map(t -> t.getName()).collect(Collectors.joining(","))); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } } /** * 自定义可重入锁,主要新增getQueuedThreads()方法,用于获取等待队列中的线程 */ private static class ReentrantLockCustomize extends ReentrantLock { public ReentrantLockCustomize(boolean fair) { super(fair); } /** * 返回正在等待获取锁的线程列表,获取的实现列表逆序输出,反转后则为FIFO队列的原本顺序 * * @return 等待队列中的线程顺序集合 */ public Collection<Thread> getQueuedThreads() { List<Thread> ts = new ArrayList<>(super.getQueuedThreads()); Collections.reverse(ts); return ts; } } }
存在问题:
非公平锁很明显存在线程“饥饿”问题,也就是一个线程获取到锁后会继续的再次获取到锁的可能性比较大,导致其他线程等待时间较长,那么为何ReentrantLock还有继续设置其为默认实现呢?这个主要原因是,公平锁会带来大量的线程切换的开销,而非公平锁虽然可能会导致线程“饥饿”问题,但是其吞吐量是远远大于公平锁的,相比之下非公平锁优势更大。