浅析ReentrantLock

浅析JUC-ReentrantLock

首先,我们来看看ReentrantLock的继承关系(通过idea查看):

image-20210413102003779

从上面的类图中,我们可以发现,ReentrantLock实现了Lock接口,通过Lock接口实现了更多可扩展性的锁操作,它有更加灵活的结构,并且支持Condition锁。

ReentrantLock的内部类FairSync和NonfairSync继承自另一个内部类Sync,而Sync继承自大名鼎鼎的AQS,站在巨人的肩膀上实现了这一系列的锁机制。

Lock

首先我们来看看Lock接口。

/*
Lock接口的实现比使用synchronized方法和语句能获得更多可扩展性的锁操作
它们允许更灵活的结构,可能拥有完全不同的属性,并且可能支持多个关联的Condition对象

锁是允许多线程访问共享资源的工具。
通常情况下,锁可以独占共享资源的访问:在同一时间只有一个线程能够获取到锁,
并且所有对共享资源的访问都回被该锁首先获得。但是,一些锁也支持并发访问共享资源,
例如ReadWriteLock

使用synchronized方法或语句可以访问与每个对象关联的隐式监视器锁,但是会
强制以块结构的方式获取和释放所有锁:当获取多个锁时,必须以相反的顺序释放它们,
所有锁都必须在获取它们的统一范围内释放。

虽然synchronized方法和语句的范围机制使使用监视器锁编程变得更加容易,并且有助于
避免许多涉及锁的常见编程错误,但有时我们需要更加灵活的方式使用锁。

当加锁和解锁发生在不同的作用域时,我们必须保证加锁和解锁的代码被执行了,此时可以通过
try-finally或者try-catch确保锁被释放。

Lock接口的实现通过提供非阻塞尝试获取锁(tryLock)、可中断的尝试获取锁(lockInterruptibly)、
带有超时时间的获取锁机制(tryLock(long, TimeUnit)),这些附加功能来超过synchronized方法和语句的使用

Lock class还可以提供与隐式监视器锁完全不同的行为和语义,例如保证顺序、不可重入使用或死锁检测。如果一个实现提供了这种专门的语义,那么这个实现必须记录这些语义

Lock实例也是普通的对象,并且它们也能够作为synchronized语句的目标对象
获取Lock实例的监视器锁与调用该实例的任何锁方法没有指定关系
建议不要以这种方式获取锁实例,除非它们在自己的实现中

所有锁实现都必须强制执行与内置监视器锁提供的相同的内存同步语义:
1、成功的锁操作与成功的锁定操作具有相同的内存同步效果
2、成功的解锁操作与成功的解锁操作具有相同的内存同步效果

不成功的加锁与解锁操作以及可重入的加锁/解锁操作,不会对内存同步造成影响
*/
public interface Lock {
    /*
    获得资源的锁
    如果无法获得锁,当前线程就会一致尝试获取锁直到成功,在此期间无法被线程调度,
    当前线程将处于休眠状态。也就是此时线程只获取锁,不做其他事,可能会造成资源的浪费
    
    锁实现可以检测到锁的错误使用,例如会导致死锁的调用,并且在这种情况下可能
    抛出(未检查)异常。这种锁实现必须记录情况和异常类型。
    */
    void lock();
    /*
    获取资源的锁,直到线程被中断才停止;如果锁可用就会立即返回
    如果锁不可用,那么当前线程将不能被调度并处于休眠状态,直到一下事件发生:
    1、锁被当前线程获取
    2、其他线程中断当前线程,并且支持中断锁获取
    如果当前线程:在进入此方法时设置其中断状态或者当获取锁时被中断并且支持中断获取锁
    那么会抛出InterruptedException异常,并且当前线程的中断状态会被清空
    
    在某些实现中中断锁的获取可能是不可能的,如果可能的话会是很昂贵的操作
    程序员应该意识到这一点
    在这种情况下实现应该记录下来
    与正常方法返回相比,实现更有利于响应中断
    */
    void lockInterruptibly() throws InterruptedException;
    /*
    仅在调用时,锁是空闲的才获取锁
    如果锁是可用的就立即获取锁并返回true,否则立即返回false
    
    使用方法:
     Lock lock = ...;
     if (lock.tryLock()) {
       try {
         // manipulate protected state
       } finally {
         lock.unlock();
       }
     } else {
       // perform alternative actions
     }
     此用法确保在获取锁时将其解锁,而在未获取锁时不会尝试解锁
    */
    boolean tryLock();
    /*
    如果在给定时长内锁是可用的并且当前线程没有被中断,就获取锁
    
    如果锁可以被获取,该方法会立即返回true
    如果锁不可用,当前线程会被禁止调用,并且进入休眠状态,直到:
    1、锁被当前线程获取
    2、其他线程中断了当前线程
    3、达到了指定的等待时间
    如果获得了锁就会立即返回true
    如果在进入此方法时设置其中断状态或者当获取锁时被中断并且支持中断获取锁
    那么会抛出InterruptedException异常,并且当前线程的中断状态会被清空
    */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    /*
    释放当前线程持有的锁
    锁实现通常会对哪个线程可以释放锁施加限制(通常只有锁的持有者可以释放锁),
    如果违反了限制,则可能抛出(未选中的)异常
    */
    void unlock();
    /*
    返回一个与Lock实例绑定的Condition实例
    等待条件对象之前,锁必须被当前线程持有,调用Condition.await()会在等待之前自动释放锁
    并且在wait返回之前会再次尝试获取锁
    
    Condition实例的精确操作依赖其实现
    */
    Condition newCondition();
}

上面就是Lock接口对应的源码(翻译了源码的部分英文注释),通过源码的阅读可以总结Lock以下几点作用:

  • 获取锁,获取锁又有几种不同的方法:
    • lock()方法:获取锁,不响应中断,直到成功获取锁才返回,不然会一直阻塞
    • lockInterruptibly()方法:如果当前线程没有被中断则获取锁,该方法响应中断,可以被其他线程中断
    • tryLock()方法:获取到锁以后立即返回true,无法获取锁直接返回false,不会阻塞线程
    • tryLock(long time, TimeUnit unit):带有超时时间的tryLock方法,在给定时间内无法获取锁立即返回false,可以获取到锁就返回true,可以响应中断
  • 释放锁
  • 创建Condition对象

ReentrantLock方法继承了该接口,并实现了其对应的功能!

Sync

Sync是ReentrantLock的抽象内部类,继承自AbstractQueuedSynchronizer,我们首先来回顾下AQS。

回顾AQS

AQS通过队列的形式来管理多个线程对共享资源的访问,它是构建锁或者其他同步组件的基础架构。

如果我们想要实现自己的同步器的话,就需要实现以下方法:

  • isHeldExclusively():该线程是否正在独占资源
  • tryAcquire(int):以独占的方式尝试获取资源,成功则返回true,失败则返回false,成功获取同步状态以后,其他线程需要等待该线程释放同步状态才能获取同步状态
  • tryRelease(int):以独占的方式去尝试释放资源,成功则返回true,失败返回false
  • tryAcquireShared(int):以共享方式尝试获取资源,其中返回负数表示失败;0表示成功,但没有剩余资源可用;整数表示成功,且有剩余资源
  • tryReleaseShared(int):以共享方式尝试释放资源,成功则返回true,失败则返回false

其中isHeldExclusively()方法必须实现,通过该方法可以判断当前线程是否独占资源:

  • 如果独占资源的话,自定义同步器就需要实现tryAcquire方法和tryRelease方法
  • 否则,自定义同步器只需要实现tryAcquireShared方法和tryReleaseShared方法

想要详细了解的可以看看我的这篇博客AQS原理初探

再回到Sync。

Sync类

Sync是AQS中的独占类型的锁,可重入是对当前线程而言的,也就是当前线程可以多次获取锁!

/*
ReentrantLock同步控制的基础,其子类有公平锁和非公平锁的实现,使用AQS的state表示该锁的次数
*/	
	abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * 加锁,与Lock#lock方法类似
         */
        abstract void lock();

        /**
         * 非公平的tryLock, tryAcquire在其子类实现,其子类实现需要非公平的获取锁的方法
         * 直接尝试使用当前线程获取资源的锁,不进AQS的队列中,如果获取失败的话再进入到队列中
         * 这就体现了非公平性,直接插队尝试获取锁!
         */
        final boolean nonfairTryAcquire(int acquires) {
            // 当前尝试获取锁的线程
            final Thread current = Thread.currentThread();
            // 获取AQS的状态
            int c = getState();
            // 如果当前状态为0时表示没有线程获取当前资源的锁,可以尝试加锁
            if (c == 0) {
                // 使用cas操作设置AQS的状态
                if (compareAndSetState(0, acquires)) {
                    // 如果成功获取锁,那么就将当前线程设置为独占的线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果当前线程为持有锁的线程就进入该分支
            else if (current == getExclusiveOwnerThread()) {
                // 增加当前线程的加锁次数
                int nextc = c + acquires;
                // 如果nextc小于0就抛出异常,超过最大加锁次数
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 设置AQS的state
                setState(nextc);
                return true;
            }
            // 获取锁失败则返回false
            return false;
        }
		// 尝试释放锁
        protected final boolean tryRelease(int releases) {
            // 获取AQS的状态并与要释放的次数相减,得到释放锁后剩余的锁次数
            int c = getState() - releases;
            // 如果当前线程不是锁持有的线程,那就抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 如果c为0,那就要彻底释放锁,此时其他线程可以来争夺该锁了
            if (c == 0) {
                free = true;
                // 将锁的独占线程置为null
                setExclusiveOwnerThread(null);
            }
            // 设置AQS的状态
            setState(c);
            return free;
        }
		// 判断当前线程是否为独占资源的线程
        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }
		// 创建Condition对象
        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        // Methods relayed from outer class
		// 获取当前资源的独占对象
        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }
		// 获取当前线程持有的锁的数量
        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }
		// 判断当前的锁状态
        final boolean isLocked() {
            return getState() != 0;
        }

        /**
         * Reconstitutes the instance from a stream (that is, deserializes it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

可以看出,Sync这个同步器就是实现了AbstractQueuedSynchronizer的tryRelease方法,其tryAcquire方法又留给了子类实现。但是这里提供了nonfairTryAcquire方法,用于非公平的获取锁。

从这里我们可以发现,ReentrantLock可重入锁的意思是:当前线程可以重复获取锁,没有加锁次数的限制,对于当前线程是可重入的,其他线程无法获取当前线程占有的资源的锁。那么ReentrantLock就相当于AQS中的独占锁。

在上面AQS定义方法的基础上,Sync还实现了其他的方法:

  • getHoldCount & getOwner:获取持有锁的数量,获取锁的拥有者
  • lock & isLocked:加锁 & 判断是否已经被锁
  • newCondition:创建Condition对象

NonfairSync非公平锁

	/*
	非公平获取锁,主要体现在lock
	*/
	static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * 加锁
         */
        final void lock() {
            // 使用cas操作设置state的值,如果不能加锁成功就调用AQS的acquire方法
            // acquire方法又会调用tryAcquire方法尝试获取锁
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
		// 调用nonfairTryAcquire方法以非公平的方式获取锁
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

FairSync公平锁

	/*
	以公平的方式获取锁
	*/
	static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
		// 直接调用aqs的acquire方法尝试获取锁,而acquire又会调用tryAcquire方法
        final void lock() {
            acquire(1);
        }

        /**
         * tryAcquire的公平版本. 
         * 除非递归调用或者没有等待的线程或者该线程是第一个线程,否则不会授予访问权限
         */
        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取锁的状态
            int c = getState();
            // 如果当前没有加锁
            if (c == 0) {
                // 判断当前线程是否为AQS队列中要处理的下一个线程
                // 如果是就使用cas操作获取锁
                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;
        }
    }
	/*
	判断当前线程是否为AQS队列中要处理的下一个线程
	
	
	当AQS队列不为空且h.next为空或下一个要处理的节点不是当前线程,就返回true
	
	当AQS列队为空时直接返回false
	当head节点的下一个节点不为空且下一个节点就是当前线程时会返回false
	*/
	public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

总结

Sync是基于AQS实现的同步器,它有两个版本:FairSync(公平锁)和NonfairSync(非公平锁),非公平锁就是直接插队获取当前资源的锁,而公平锁就是按照顺序去获取锁。这点我们从他们的源码中就可以看出来区别。

这里提供了公平锁和非公平锁的版本使我们有了更加灵活的选择。

重回ReentrantLock

我们继续回到ReentrantLock中,看看它的其他源码,首先来看看它的构造函数。

构造函数

image-20210413151411272

从构造函数中我们可以看出,ReentrantLock默认是非公平锁的,当我们在构造函数中将fair设置为true时,ReentrantLock会设置为公平锁。

方法概览

首先我们来看看ReentrantLock包含了哪些方法:

image-20210413153224543

接下来我们将详细介绍其中的方法.

	/*
	调用Sync的getHoldCount方法,获取AQS的state值,该值就对应了当前线程获取的锁的数量
	*/
	public int getHoldCount() {
        return sync.getHoldCount();
    }
	/*
	调用Sync的getOwner方法,判断当前锁的拥有者
	*/
	protected Thread getOwner() {
        return sync.getOwner();
    }
	/*
	判断锁是否被当前线程持有,调用Sync的isHeldExclusively方法
	*/
	public boolean isHeldByCurrentThread() {
        return sync.isHeldExclusively();
    }
	/*
	调用AQS的getQueuedThreads方法获取现在有哪些线程正在等待该锁
	该方法的实现其实是对链表的遍历,比较简单
	*/
	protected Collection<Thread> getQueuedThreads() {
        return sync.getQueuedThreads();
    }
	/*
	调用AQS的getQueueLength方法,获取等待队列中线程的数量
	该方法的实现其实是对链表的遍历,比较简单
	*/
	public final int getQueueLength() {
        return sync.getQueueLength();
    }
	/*
	获取等待Condition的线程
	*/
	protected Collection<Thread> getWaitingThreads(Condition condition) {
        // 如果condition为null或者condition不存在与AQS中,那么就抛出异常
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        // 调用getWaitingThreads遍历队列,进行判断
        return sync.getWaitingThreads((AbstractQueuedSynchronizer.ConditionObject)condition);
    }

	public int getWaitQueueLength(Condition condition) {
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        return sync.getWaitQueueLength((AbstractQueuedSynchronizer.ConditionObject)condition);
    }
	/*
	判断线程是否在等待队列中,同样调用AQS的isQueued方法
	*/
	public final boolean hasQueuedThread(Thread thread) {
        return sync.isQueued(thread);
    }
	/*判断等待队列中是否还有线程在等待*/
	public final boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
	/*
	判断是否有线程在等待对应的条件
	*/
	public boolean hasWaiters(Condition condition) {
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        return sync.hasWaiters((AbstractQueuedSynchronizer.ConditionObject)condition);
    }
	
	public final boolean isFair() {
        return sync instanceof FairSync;
    }
	public boolean isHeldByCurrentThread() {
        return sync.isHeldExclusively();
    }
	/*判断是否被加锁,调用Sync的isLocked方法,判断AQS的状态是否为0*/
	public boolean isLocked() {
        return sync.isLocked();
    }
	/*
	该方法会调用公平锁和非公平锁对应的方法,公平锁和非公平锁的实现方式不同
	这两者最终会调用AQS的acquire方法,该方法又会调用FairSync和NonFairSync的tryAcquire方法
	*/
	public void lock() {
        sync.lock();
    }
	/*
	响应中断的加锁方式
	*/
	public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

	public Condition newCondition() {
        return sync.newCondition();
    }
	/*
	以非公平的方式尝试加锁
	*/
	public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
	/*
	以非公平的方式尝试加锁
	*/
	public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
	/*
	调用AQS的release方法解锁,最终会调用Sync#tryRelease方法
	*/
	public void unlock() {
        sync.release(1);
    }

从上面的源码分析来看,可重入锁真是站在了巨人的肩膀上,它的大部分方法都是调用AQS以及继承自AQS的Sync还有其子类FairSync和NonFairSync方法。由此可见AQS是多么的重要。

总结

ReentrantLock通过AQS实现了一套同步的机制,并通过Lock接口的约束实现了加锁的方法,通过灵活使用ReentrantLock可以帮助我们解决很多并发编程的问题。

理解ReentrantLock可重入的含义:从源码来看,ReentrantLock是独占锁,任意时间有且仅有一个线程能占有共享资源,但是当前线程可以对资源进行重复的加锁,这就是可重入锁

有什么问题大家可以一起讨论啊,文章如果有什么问题欢迎各位大佬指正!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值