(2.1.27.10)Java并发编程:Lock之ReentrantLock独享式重入锁

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/fei20121106/article/details/83268571

ReentrantLock,即重入锁,是一个和synchronized关键字等价的,支持线程重入的互斥锁。只是在synchronized已有功能基础上添加了一些扩展功能。

除了支持可中断获取锁、超时获取锁、非阻塞获取锁这些显示锁的常见功能外,ReentrantLock还支持公平锁(synchronized只支持非公平锁)。

一、使用

		ReentrantLock takeLock = new ReentrantLock();
		
		// 获取锁
		takeLock.lock();
		
		try {
		  
		  // 业务逻辑
		  
		} finally {
		  // 释放锁
		  takeLock.unlock();
		}

二、总体结构

重入锁的大体结构如下:

public class ReentrantLock implements Lock, java.io.Serializable {

    private final ReentrantLock.Sync sync;

	public ReentrantLock() {
        this.sync = new ReentrantLock.NonfairSync();
    }
    public ReentrantLock(boolean var1) {
        this.sync = (ReentrantLock.Sync)(var1?new ReentrantLock.FairSync():new ReentrantLock.NonfairSync());
    }


    abstract static class Sync extends AbstractQueuedSynchronizer {}
	
    static final class NonfairSync extends Sync {}
	
    static final class FairSync extends Sync {}
	
	
	//[Lock接口]获取锁. 成功则向下运行,失败则阻塞
    public void lock() {
        this.sync.lock();
    }

	//[Lock接口]可中断地获取锁,在当前线程获取锁的过程中可以响应中断信号
    public void lockInterruptibly() throws InterruptedException {
        this.sync.acquireInterruptibly(1);
    }

	//[Lock接口]尝试非阻塞获取锁,调用方法后立即返回,成功返回true,失败返回false
    public boolean tryLock() {
        return this.sync.nonfairTryAcquire(1);
    }

	//[Lock接口]在超时时间内获取锁,到达超时时间将返回false,也可以响应中断
    public boolean tryLock(long var1, TimeUnit var3) throws InterruptedException {
        return this.sync.tryAcquireNanos(1, var3.toNanos(var1));
    }

	//[Lock接口]释放锁
    public void unlock() {
        this.sync.release(1);
    }

	//[Lock接口]获取等待通知组件实现信号控制,等待通知组件实现类似于Object.wait()方法的功能
    public Condition newCondition() {
        return this.sync.newCondition();
    }
	
}

通过代码我们可以看到,基本全部的业务都是交给 Sync去实现的

  • Sync类是AQS的子类,而NonfairSync和FairSync是Sync的子类。
  • AQS里的state在重入锁里代表线程重入的次数,state=1代表重入锁当前已被某个线程独占,这个线程每重入一次,state++。因为state是int型变量,因此重入锁可以重入的最大次数是2^31-1。
  • 公平锁和非公平锁的区别就在于获取锁时候的逻辑略有不同,其他操作都是一样的,因此公用的操作都放在Sync类里,NonfairSync和FairSync里只是实现自己的tryAcquire(int acquires)方法。
方法名称 描述
void lock() 获取锁. 成功则向下运行,失败则阻塞
void lockInterruptibly() throws InterruptedException 可中断地获取锁,在当前线程获取锁的过程中可以响应中断信号
boolean tryLock() 尝试非阻塞获取锁,调用方法后立即返回,成功返回true,失败返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 在超时时间内获取锁,到达超时时间将返回false,也可以响应中断
void unlock(); 释放锁
Condition newCondition(); 获取等待通知组件实现信号控制,等待通知组件实现类似于Object.wait()方法的功能

三、lock()与unlock()大致流程

lock()获取锁的主要流程如下:

  1. 首先,ReentrantLock的lock()方法会调用其内部成员变量sync的lock()方法;
  2. 其次,sync的非公平锁NonfairSync或公平锁FairSync实现了父类AbstractQueuedSynchronizer的lock()方法,其会调用acquire()方法;
  3. 然后,acquire()方法则在sync父类AbstractQueuedSynchronizer中实现

在这里插入图片描述
【ReentrantLock获取锁主体流程】

3.1 AQS的实现

在上篇我们已经讲到了,AQS需要重写的钩子方法:

方法名称 描述
boolean tryAcquire(int arg) 独占式尝试获取同步状态(通过CAS操作设置同步状态),如果成功返回true,反之返回false
boolean tryRelease(int arg) 独占式释放同步状态,成功返回true,失败返回false。
int tryAcquireShared(int arg) 共享式的获取同步状态,返回大于等于0的值,表示获取成功,反之失败。
boolean tryReleaseShared(int arg) 共享式释放同步状态,成功返回true,失败返回false。
boolean isHeldExclusively() 判断同步器是否在独占模式下被占用,一般用来表示同步器是否被当前线程占用

我们来看下Sync对其的实现,可以看出Sync中:

  1. 重写了tryRelease独占式释放同步状态
  2. 并没有重写tryAcquire(int arg)独占式尝试获取同步状态方法, 而是交给子类去实现
  3. 新建了nonfairTryAcquire(),这其实是个同步状态获取方法,在”非公平模式“的子类中被调用
  4. 定义了一个抽象的 lock()方法,其实是暴漏给外部调用的独占式尝试获取同步状态方法。 在公平锁中,调用tryAcquire();在非公平所中调用nonfairTryAcquire()
  5. 一系列的Utils方法
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        Sync() {
        }

		/**************1. 重写了`tryRelease`独占式释放同步状态*********/
		protected final boolean tryRelease(int var1) {
            int var2 = this.getState() - var1;
            if(Thread.currentThread() != this.getExclusiveOwnerThread()) {
                throw new IllegalMonitorStateException();
            } else {
                boolean var3 = false;
                if(var2 == 0) {//作为可重入数,state在大于0时标识重入次数,必须退出对应次数时,才算真正的退出
                    var3 = true;
                    this.setExclusiveOwnerThread((Thread)null);
                }

                this.setState(var2);//更新锁状态
                return var3;
            }
        }
		
		//判断同步器是否在独占模式下被占用,一般用来表示**同步器是否被当前线程占用
		protected final boolean isHeldExclusively() {
            return this.getExclusiveOwnerThread() == Thread.currentThread();
        }

		
	    /******2. 并没有重写`tryAcquire(int arg)`独占式尝试获取同步状态方法, 而是交给子类去实现***********/
		//TODO
		
		
	    /******3. 新建了`nonfairTryAcquire()`,这其实是个同步状态获取方法,在”非公平模式“的子类中被调用*****/
		final boolean nonfairTryAcquire(int var1) {
            //下文讲解
        }
		
		
		/*********4. 定义了一个抽象的 `lock()`方法,其实是**暴漏给外部调用的独占式尝试获取同步状态方法**。*******/
        abstract void lock();

		
	    /**************************5. 一系列的Utils方法******************************/
		//当前锁是否已被占用
		final boolean isLocked() {
            return this.getState() != 0;
        }

        final Thread getOwner() {
            return this.getState() == 0?null:this.getExclusiveOwnerThread();
        }

	    final int getHoldCount() {
            return this.isHeldExclusively()?this.getState():0;
        }

		final ConditionObject newCondition() {
            return new ConditionObject(this);
        }

		private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
            var1.defaultReadObject();
            this.setState(0);
        }

    }
}

	
	

3.1.1 非公平锁

通过代码可以看到,非公平锁上来就无视等待队列的存在而抢占锁

扫描二维码关注公众号,回复: 3751068 查看本文章
  1. 通过基于CAS操作的compareAndSetState(0, 1)方法,试图修改当前锁的状态
    • 这个0表示AbstractQueuedSynchronizer内部的一种状态,针对互斥锁则是尚未有线程持有该锁,而>=1则表示存在线程持有该锁,并重入对应次数
    • 这个上来就CAS的操作也是非公共锁的一种体现,CAS操作成功的话,则将当前线程设置为该锁的唯一拥有者。
  2. 抢占不成功的话,则调用父类的acquire()方法
    • 我们已经知道这个acquire(1) AQS暴漏给外部使用的模板方法,内部其实会调用tryAcquire(int var1)钩子方法
    • acquire(1) 独占式获取同步状态(该方法会调用子类重写的tryAcquire(int arg))。 如果当前线程获取同步状态成功,则返回以便继续执行,否则进入同步队列的尾部等待,该方法会调用tryAcquire(int arg)方法。
    static final class NonfairSync extends ReentrantLock.Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        NonfairSync() {
        }

		//外部调用ReentrantLock.lock()其实会调用到这里
        final void lock() {
            if(this.compareAndSetState(0, 1)) {
                this.setExclusiveOwnerThread(Thread.currentThread());
            } else {
                this.acquire(1);
            }

        }

        protected final boolean tryAcquire(int var1) {
            return this.nonfairTryAcquire(var1);
        }
		
		final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {//如果当前没有被占用
                if (compareAndSetState(0, acquires)) {//则通过CAS来抢占,抢占成功,直接返回true
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//如果已经被占用,且就是当前线程占用。则将state+1,标识再次重入
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

}

3.1.2 公平锁

我们来看下公平锁:

  • 调用父类的acquire()方法
    • 我们已经知道这个acquire(1) AQS暴漏给外部使用的模板方法,内部其实会调用tryAcquire(int var1)钩子方法
    • acquire(1) 独占式获取同步状态(该方法会调用子类重写的tryAcquire(int arg))。 如果当前线程获取同步状态成功,则返回以便继续执行,否则进入同步队列的尾部等待,该方法会调用tryAcquire(int arg)方法。

前线程会在得到当前锁状态为0,即没有线程持有该锁,并且通过!hasQueuedPredecessors()判断当前等待队列没有前继线程(也就是说,没有比我优先级更高的线程在请求锁了)获取锁的情况下,通过CAS抢占锁,并设置自己为锁的当前拥有者,当然,如果是重入的话,和非公平锁处理一样,通过累加状态位标记重入次数。

而一旦等待队列中有等待者,或当前线程抢占锁失败,则它会乖乖的进入等待队列排队等待。 这部分由AQS的cquire()方法实现

    static final class FairSync extends ReentrantLock.Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        FairSync() {
        }

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

        protected final boolean tryAcquire(int var1) {
            Thread var2 = Thread.currentThread();
            int var3 = this.getState();
            if(var3 == 0) {//如果当前没有被占用
                if(!this.hasQueuedPredecessors() && this.compareAndSetState(0, var1)) {//先判断队列中是否有前置等待节点,如果没有。则通过CAS来抢占,抢占成功,直接返回true
                    this.setExclusiveOwnerThread(var2);
                    return true;
                }
            } else if(var2 == this.getExclusiveOwnerThread()) {//如果已经被占用,且就是当前线程占用。则将state+1,标识再次重入
                int var4 = var3 + var1;
                if(var4 < 0) {
                    throw new Error("Maximum lock count exceeded");
                }

                this.setState(var4);
                return true;
            }

            return false;
        }
    }
	

我们可以发现,这段代码与上面的 nonfairTryAcquire方法就只多了一句代码,而就是这一句代码就实现了公平锁。

hasQueuedPredecessors()方法判断同步队列中是否有更早开始等待锁的线程。如果有,则tryAcquire方法直接返回false让当前线程进入同步队列排队。

四、tryLock()的实现

即便是公平锁,如果通过不带超时时间限制的tryLock()的方式获取锁的话,它也是不公平的,因为其内部调用的是sync.nonfairTryAcquire()方法,无论抢到与否,都会同步返回。如下:

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

但是带有超时时间限制的tryLock(long timeout, TimeUnit unit)方法则不一样,还是会遵循公平或非公平的原则的,如下:

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

为什么这么实现呢?

我们可以想一下tryLock() 的语义,tryLock() 要实现的效果是尝试获取一次锁,如果获取失败不阻塞而是直接返回false。

如果在公平锁模式下严格按照公平锁的定义来实现这个方法,那么当同步队列中有其他线程等待的时候,tryLock()都不可能获取到锁,只能返回false。而事实上,当我们调用tryLock()的时候,很多时候应该都是希望尽可能的成功的,而此时要不要让tryLock()的线程严格排队,其实不是那么重要,因此公平锁下tryLock()方法在获取锁时使用非公平获取模式,即可以插队

那么如果我们在公平锁模式下就希望tryLock()方法获取锁严格排队呢?可以用tryLock(0, TimeUnit.SECONDS),这个方法等效于一个严格排队的tryLock()方法,之所以等效,是因为tryLock(long timeout, TimeUnit unit)的实现是区分公平锁和非公平锁的,在公平锁的模式下,获取锁的操作是严格按同步队列排队等待的。

那么如果我们在公平锁模式下希望 tryLock(long timeout, TimeUnit unit) 不严格排队,表现的像一个支持超时的tryLock()呢?也是有办法的:

 if (lock.tryLock() || lock.tryLock(timeout, unit)) {
   ...
 }

可以这样组合一下,左边的tryLock()有机会插队获取一次锁,如果没获取到,在用tryLock(timeout, unit)做一次可超时的同步队列排队。

猜你喜欢

转载自blog.csdn.net/fei20121106/article/details/83268571
今日推荐