理解 ReentrantLock 这一篇就够了(同步锁,重入锁,独占锁,公平锁)

204 阅读7分钟

前言

  本文已参与「新人创作礼」活动,一起开启掘金创作之路。

  提到同步首先想到的可能就是 Synchronized,但 Synchronized 不足够灵活,这个时候 ReentrantLock 的好处就显示出来了!对于 ReentrantLock 有很多的名称(包含且不限于上方标题),这个地方我们就不过多地描述他俩直接的优劣了,简单的对比一下,主要还是讲一下 ReentrantLock

优劣比较

  知道都不喜欢看废话,就捡重点的直接进入主题,首先要讲的还是这两个的优劣对比(后边会根据每个关键词拓展,尽量做到描述清楚每一个关键点)
  首先说一下 Synchronized:
    1) 是独占锁,加锁和解锁过程都是自动的,不太灵活,但操作简便(直接在方法块上使用就可以)
    2) 可重入,并且不需要担心最后锁释放的问题,因为第一点说了,加解锁都是自动的
    3) 不可中断(同步等待:一个线程一直获取不到锁就会一直等待,直到获取到锁)
    4) 无公平锁机制(线程是随机分配的)
  而 ReentrantLock:
    1) 也是独占锁,但加锁和解锁过程是手动的,比较灵活,不易操作(需要自己创建锁,关闭锁)
    2) 也是可重入,但因为是手动加锁解锁,且次数需要是一样的,不然其他线程会无法获得锁
    3) 可以相应中断的,可轮询的
    4) 有公平锁机制(白话理解)谁等待的时间长谁就先执行,(官话)如果有另一个线程持有锁或者有其他线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中
  下边是我们测试的例子,至于什么是重入,什么是独占,什么是公平锁,会在后边一一补充

示例

创建锁,使用锁

  先创建一个 ReentrantLock 对象(最后有完整的示例代码)

// 创建对象
final static ReentrantLock lock = new ReentrantLock();

  在上边我们比较优劣的时候讲了一下说这个 ReentrantLock 的锁是手动开启释放的,所以这个地方我们创建了对象之后就要手动的去开启锁,我们写一个测试方法,来看一下具体是怎么使用的 在这里插入图片描述   附上一个完整的测试方法和运行结果

// 创建对象
final static ReentrantLock lock = new ReentrantLock();

// 使用 mian 方法测试
public static void main(String[] args) {
    // ReentrantLock 测试示例
    new Thread(ReentrantLockUtils::ReentrantLockTest,"ReentrantLockTest -- 线程1").start();
}

// 要执行的测试方法
public static void ReentrantLockTest(){
    // 开启锁
    lock.lock();
    try {
        // 获取线程名称
        String tr1 = Thread.currentThread().getName();
        log.info(tr1+"获取了锁");
        // 线程暂停两秒(作用和 Thread.sleep() 一样,相对来说可读性更高了一些)
        TimeUnit.SECONDS.sleep(2);
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        String tr1 = Thread.currentThread().getName();
        log.info(tr1+"释放了锁");
        lock.unlock();
    }
}

在这里插入图片描述

可重入

  什么是可重入?他的作用是什么?可重入(也有叫重入锁的)就是,当一个持有锁的线程,在释放锁之前,被重复访问或者访问了此锁的其他方法,那么这个线程不需要进行抢占锁,只会被记录重入次数,重入锁的作用就是为了防止死锁现象

独占锁

  独占锁顾名思义, 就是一个线程获得了锁,别的线程就不能获得锁,必须等锁释放了,才能可能获取到锁

公平锁

  什么是公平锁?为啥说 ReentrantLock 自带公平锁机制呢?我们接着往下看!可以点开 ReentrantLock 的源码看下,我们创建的 ReentrantLock 对象完整的应该是这个样子的,ReentrantLock 默认使用的是非公平锁,如果不传参数,默认的是 false,然后看下边第二段 ReentrantLock(boolean fair),意思是传入 true 或者 false ,传入 true 就是开启公平锁机制

final static ReentrantLock lock = new ReentrantLock(false);

在这里插入图片描述   我们继续跟进去看下公平锁和非公平锁有什么区别,我们先看非公平锁源码,可以看到进入方法的时候会先进行一次 CAS 判断(是否需要修改状态,详情看下方注释所在位置),成功则直接返回线程,继续往下看,进入 tryAcquire 方法可以看到 return NonfairSync

/**
 * Sync object for non-fair locks
 */
static final class NonfairSync extends Sync {
	private static final long serialVersionUID = 7316153563782823691L;
	/**
	 * Performs lock.  Try immediate barge, backing up to normal
	 * acquire on failure.
	 */
	final void lock() {
		// compareAndSetState:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值
		// 第一个参数:expect - 预期值 第二个参数:update - 新值
		// 如果成功,则返回 true,返回 false 指示实际值与预期值不相等。
		// 白话理解就是:修改 state 为 1 即代表加锁成功,设置当前 AQS 独占线程为当前线程
		if (compareAndSetState(0, 1))
			setExclusiveOwnerThread(Thread.currentThread());
		else
		// 否则就加锁失败调用 acquire()
			acquire(1);
	}
	protected final boolean tryAcquire(int acquires) {
		return nonfairTryAcquire(acquires);
	}
}

  继续跟进去,也就是下方,直接获取到线程状态,如果状态等于 0 则直接返回成功(状态为 0 就代表当前锁还未被其他线程获取,可以直接获取锁,成功就设置当前线程为独占线程),如果不为 0 (这个地方就涉及到了重入锁)判断锁独占线程为当前线程,同步状态+1,设置同步状态结束

/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
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;
}

  然后在来看一下公平锁的源码,可以看到我们下方注释的地方多了一层对线程的判断,也就是说,只有在当前队列中没有节点的时候,才会去修改 CAS 变量,从而实现了公平锁的

/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {
	private static final long serialVersionUID = -3000897897090466540L;

	final void lock() {
		acquire(1);
	}

	/**
	 * Fair version of tryAcquire.  Don't grant access unless
	 * recursive call or no waiters or is first.
	 */
	protected final boolean tryAcquire(int acquires) {
		final Thread current = Thread.currentThread();
		int c = getState();
		if (c == 0) {
			// 这个相比非公平锁的源码多了一层判断
			// hasQueuedPredecessors:用来判断线程需不需要排队
			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;
	}
}

  二则的实现区别就在于公平锁 是先判断当前队列中有没有存在的队列节点,如果没有才会去进行修改状态, 非公平锁 是先进行一次 CAS 判断,只有在不成功才会到 acquire 方法中并且在 nonfairTryAcquire 方法中,并没有添加 hasQueuedPredecessors 此参数,而是直接使用 CAS 尝试获取锁   公平锁和非公平锁的区别就在于(借鉴于这个老哥的总结:理解ReentrantLock的公平锁和非公平锁):
    1.非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
    2.非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
  如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
  相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。(白话来讲:理解方式就是公平锁,排着队一个一个的来,谁等的时间最长谁就先执行。非公平锁就是不排队,谁先抢到谁就执行)