Java并发之ReentrantLock吐血总结

本文详细介绍了Java并发编程中的ReentrantLock,包括重入锁的概念、与Synchronized的区别、公平锁与非公平锁的实现原理,以及可重入、可中断和可超时获取锁的机制。通过源码分析,帮助读者深入理解ReentrantLock的工作原理。
摘要由CSDN通过智能技术生成

一、ReentrantLock重入锁基本概念

ReentrantLock,重入锁,是JDK5中添加在并发包下的一个高性能的工具。顾名思义,ReentrantLock支持重入功能,即同一个线程在未释放锁的情况下可以重复获取锁,因此,重入的前提必须是同一个线程,后面会基于源码展示重入功能具体是如何实现的。

本文内容将以介绍ReentrantLock的基本功能为主体,辅以相应源码的剖析,使读者深入记忆ReentrantLock锁的各种机制,以及实现原理。


二、ReentrantLock与Synchronized的区别

对比项SynchronizedReentrantLock
性能相对较差优于synchronized 20%左右
公平性只支持非公平锁同时支持公平锁与非公平锁
尝试获取锁不支持,一旦到了同步块,且没有获取到锁,就阻塞在这里支持,通过tryLock方法实现,可通过其返回值判断是否成功获取锁,所以即使获取锁失败也不会阻塞在这里
超时的获取锁不支持,如果一直获取不到锁,就会一直等待下去支持,通过tryLock(time, TimeUnit)方法实现,如果超时了还没获取锁,就放弃获取锁,不会一直阻塞下去
是否可响应中断不支持,不可响应线程的interrupt信号支持,通过lockInterruptibly方法实现,通过此方法获取锁之后,线程可响应interrupt信号,并抛出InterruptedException异常
等待条件的支持支持,通过wait、notify、notifyAll来实现支持,通过Conditon接口实现,支持多个Condition,比synchronized更加灵活

从上面的表格中可以看出为什么要引入ReentrantLock锁了,因为其在很多方面都比Synchronized锁更灵活,当存在大量线程竞争锁时,ReentrantLock的性能也要优于synchronized。因此,学好ReentrantLock锁就更有必要了。

三、ReentrantLock公平锁与非公平锁

上面说到了ReentrantLock锁支持两种实现方式,即公平锁与非公平锁。

我们先来看一下ReentrantLock的具体实现架构图:

ReentrantLock锁实现了Lock接口,里面具体锁的实现使用了抽象内部类Sync。Sync有两个实现类,NonfairSync和FairSync,即公平锁与非公平锁。这两个也是ReentrantLock的内部类,继承自Sync。

源码中ReentrantLock实例化的过程:

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

在创建ReentrantLock锁对象的时候,默认的无参构造函数创建的是非公平锁,即NonfairSync。构造函数中可以指定一个boolean变量,即公平性,可以根据这个参数创建公平锁或者非公平锁。

知道了ReentrantLock内部创建的过程,那么什么是公平锁?什么是非公平锁呢?

如果是公平锁,多个线程之间获取锁的时候要排队,依次获取锁;如果是不公平锁,多个线程获取锁的时候一哄而上,谁抢到是谁的。这样讲比较笼统,下面结合源码剖析两种锁的区别。

非公平锁的实现原理

假设一下一个场景:线程t1先进来,拿到了锁,后面的others_t线程们都没有获取到锁,进入AQS同步队列中等待。当t1执行完之后,释放了锁,其他线程开始非公平的竞争锁。以下根据这个场景来进行讲解。

非公平锁的实现:

static final class NonfairSync extends ReentrantLock.Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    final void lock() {
        //线程t1进来,判断当前锁没人持有,自己获取到了锁
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //others_t线程们没有获取到锁
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

 如果获取锁失败,会调用AQS的acquire方法

public final void acquire(int arg) {
    // 如果在tryAcquire方法中依然获取锁失败,会将当前线程加入同步队列中等待(addWaiter)
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire的实现如下,其实是调用了父类Sync的nonfairTryAcquire方法,尝试获取锁,获取不到,就加入到同步队列中。

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

 非公平锁尝试获取:

final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取当前锁的状态
    int c = getState();
    //如果锁没有被持有,就尝试获取锁,才不会管同步队列有没有等待的线程
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果当前线程即为持有锁的线程,获取到锁,state+1
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //没有获取到锁,返回false
    return false;
}

此时,others_t线程都加入到了同步队列中排队。

某一时刻,t1执行完毕了,调用了unlock()方法释放锁。

public void unlock() {
    // 调用AQS的release方法释放资源
    sync.release(1);
}
public final boolean release(int arg) {
    // tryRelease方法,在Sync中实现
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 成功释放锁后,唤醒同步队列中的下一个节点,使之可以重新竞争锁
            // 注意此时不会唤醒队列第一个节点之后的节点,这些节点此时还是无法竞争锁
            unparkSuccessor(h);
        return true;
    }
    return false;
}

尝试释放锁:

protected final boolean tryRelease(int releases) {
    // 将state的值-1,如果-1之后等于0,释放锁成功
    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;
}

这时锁被释放了,被唤醒的线程和新来的线程重新竞争锁(不包含同步队列后面的那些线程)

回到lock方法中,由于此时所有线程都能通过CAS来获取锁,并不能保证被唤醒的那个线程能竞争过新来的线程,所以是非公平的。这就是非公平锁的实现。

为方便大家巩固记忆,借鉴一幅图来加深记忆:

公平锁的实现原理

公平锁与非公平锁的释放锁的逻辑是一样的,都是调用上述的unlock方法,最大区别在于获取锁的时候。

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
    // 获取锁,与非公平锁的不同的地方在于,这里直接调用的AQS的acquire方法,没有先尝试获取锁,因为要保证公平性,就要确保同步队列中没有等待的线程时,才可以获取到锁
    // acquire又调用了下面的tryAcquire方法,核心在于这个方法
    final void lock() {
        acquire(1);
    }

    /**
     * 这个方法和nonfairTryAcquire方法只有一点不同,在标注为#1的地方
     * 多了一个判断hasQueuedPredecessors,这个方法是判断当前AQS的同步队列中是否还有等待的线程
     * 如果有,返回true,否则返回false。
     * 由此可知,当队列中没有等待的线程时,当前线程才能尝试通过CAS的方式获取锁。
     * 否则就让这个线程去队列后面排队。
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // #1
            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;
    }
}

通过注释可知,在公平锁的机制下,任何线程想要获取锁,都要先看一下同步队列中有没有等待的线程,如果有,就必须排队,不可能出现插队的情况。这就是公平锁的实现原理。

四、可重入功能的实现原理

ReentrantLock的实现基于队列同步器(AbstractQueuedSynchronizer,后面简称AQS),ReentrantLock的可重入功能基于AQS的同步状态:state,通过修改AQS中state的值来同步锁的状态。 通过这个方式,实现了可重入。

其原理大致为:当锁没有被持有时,state为0,当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,如果不是,阻塞线程。 当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,然后将记录当前持有锁的线程的那个字段设置为null,并唤醒其他线程,使其重新竞争锁。

// acquires的值是1
final boolean nonfairTryAcquire(int acquires) {
	// 获取当前线程
	final Thread current = Thread.currentThread();
	// 获取state的值
	int c = getState();
	// 如果state的值等于0,表示当前没有线程持有锁
	// 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true
	if (c == 0) {
		if (compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	// state的值不等于0,表示已经有其他线程持有锁
	// 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true
	// 如果不是当前线程,获取锁失败,返回false
	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;
}

五、tryLock的原理

tryLock做的事情很简单:让当前线程尝试获取一次锁,成功的话获取到锁并返回true,否则false。

其实现,其实就是调用了nonfairTryAcquire方法来获取锁。

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

与lock()的区别就是,获取失败的话,他不会将自己添加到同步队列中等待,直接返回false,让业务调用代码自己处理。

六、可被中断的获取锁

中断,也就是通过Thread的interrupt方法将某个线程中断,中断一个阻塞状态的线程,会抛出一个InterruptedException异常。

如果获取锁是可中断的,当一个线程长时间获取不到锁时,我们可以主动将其中断,可避免死锁的产生。

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

会调用AQS的acquireInterruptibly方法

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    // 判断当前线程是否已经中断,如果已中断,抛出InterruptedException异常
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

此时会优先通过tryAcquire尝试获取锁,如果获取失败,会将自己加入到队列中等待,并可随时响应程序主动发起的中断。 

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    // 将自己添加到队列中等待
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        // 自旋的获取锁
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            // 获取锁失败,在parkAndCheckInterrupt方法中,通过LockSupport.park()阻塞当前线程,
            // 并调用Thread.interrupted()判断当前线程是否已经被中断
            // 如果被中断,直接抛出InterruptedException异常,退出锁的竞争队列
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // #1
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

七、可超时的获取锁

通过如下方法实现,timeout是超时时间,unit代表时间的单位(毫秒、秒...)

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

可以发现,这也是一个可以响应中断的方法。然后调用AQS的tryAcquireNanos方法:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

doAcquireNanos方法与中断里面的方法大同小异,下面在注释中说明一下不同的地方:

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    // 计算超时截止时间
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            // 计算到截止时间的剩余时间
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L) // 超时了,获取失败
                return false;
            // 超时时间大于1000纳秒时,才阻塞
            // 因为如果小于1000纳秒,基本可以认为超时了(系统调用的时间可能都比这个长)
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            // 响应中断
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

最后,贴一下Lock()接口的代码,看一下ReentrantLock都实现的功能列表:

public interface Lock {
	//上锁(不响应Thread.interrupt()直到获取锁)
    void lock();
	//上锁(响应Thread.interrupt())
    void lockInterruptibly() throws InterruptedException;
	//尝试获取锁(以nonFair方式获取锁)
    boolean tryLock();
  	//在指定时间内尝试获取锁(响应Thread.interrupt(),支持公平/非公平)
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
	//解锁
    void unlock();
	//获取Condition
    Condition newCondition();
}

接口中有六个方法,除了Condition的,上面都讲解到了,因此如有什么地方有疑问,可在下方留言区留言讨论,笔者将第一时间进行回复。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值