AQS运用之ReentrantLock实现

   在共享资源同步器AQS详解中讲了AQS的底层实现原理,现在来看一下它的具体运用,我们知道AQS定义了几个空的抛异常的方法让用户自定义实现。自定义同步器实现时主要实现以下几种方法:
    isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
    tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
    tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
    tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
    tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

这里面其实用到了模板方法设计模式,公共逻辑在AQS中封装好了,那些个具体要用AQS的类只要重写上面的方法即可,我们来看一下JUC中运用了AQS的类:

首先来看一下ReentrantLock的运用,我们要保证一个成员变量的线程安全,可以使用synchronized关键字实现:


    private int count =0;

    public synchronized int calculateCount(){
        return count++;
    }

我们也可以使用ReentrantLock来实现线程安全:

    private int count =0;
    private ReentrantLock lock = new ReentrantLock();

    public int calculateCount(){
        try{
            lock.lock();
            return count++;
        }finally {
            lock.unlock();
        }
    }

接下来我们深入ReentrantLock的源代码,一起来看看它是怎么结合AQS来保证线程安全的,首先从构造方法入手,ReentrantLock有一个同步的类Sync,提供两个构造方法,一个是默认的构造方法,一个是需要传入一个bool类型的参数,这个参数是表示锁的公平与否,true代表公平锁,false代表非公平锁,默认的构造方法提供的是非公平锁

    private final Sync sync;
	public ReentrantLock() {
        sync = new NonfairSync();
    }

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

ReentrantLock中的提供了一个同步的内部类:Sync,它继承自AQS,NonfairSync和FairSync都是基于Sync来实现的,它里面提供了对非公平锁上锁的实现,即非公平锁对于AQS中tryAcquire方法的实现,也提供了释放锁的实现,即AQS中tryRelease方法的实现,具体的实现就是对AQS中的state变量进行操作,设置独占线程,有了AQS这个同步框架,会发现我们对于自定义锁的实现不是一般的简单:

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        // 提供一个抽象的lock方法,供NonfairSync和FairSync实现
        abstract void lock();

        // 父类Sync提供非公平锁对于tryAcquire方法实现
        final boolean nonfairTryAcquire(int acquires) {
        // 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取AQS中的state的值
            int c = getState();
            // 没有线程上锁,通过自旋操作将state加1,并设置该线程为当前独占线程
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    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");
                // 设置AQS中state的值
                setState(nextc);
                return true;
            }
            return false;
        }

        // 提供AQS中tryRelease方法的实现
        protected final boolean tryRelease(int releases) {
            // 每次释放掉锁,将当前AQS中的state的值减1
            int c = getState() - releases;
            // 如果当前的独占线程不是自己,那调用释放锁的方法会抛异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 判断当前线程是否已完全释放锁(有可能线程重复获取锁)
            if (c == 0) {
                free = true;
                // 完全释放锁,将独占线程设置为空
                setExclusiveOwnerThread(null);
            }
            // 设置AQS中的state为最新的值
            setState(c);
            return free;
        }

        protected final boolean isHeldExclusively() {
            // 该方法保证获取独占锁的线程为当前线程
            return getExclusiveOwnerThread() == Thread.currentThread();
        }
    }

定义好了父类的Sync方法,我们首先来看非公平锁NonefairSync的实现,那就是很简单的几行代码搞定:

static final class NonfairSync extends Sync {
	private static final long serialVersionUID = 7316153563782823691L;
	
    final void lock() {
        // 首先通过CAS自旋操作将state的值设置为1,调用AQS中的compareAndSetState方法
		if (compareAndSetState(0, 1))
            // 设置成功将当前线程设置为独占线程
			setExclusiveOwnerThread(Thread.currentThread());
		else
            // CAS自旋失败,就调用AQS中的acquire方法入等待队列
			acquire(1);
	}

    // 实现AQS中的tryAcquire方法,在Sync父类中具体实现
	protected final boolean tryAcquire(int acquires) {
		return nonfairTryAcquire(acquires);
	}
}

我们看公平锁FairSync对于Sync的实现:

static final class FairSync extends Sync {
	
    // 直接调用AQS的acquire方法
	final void lock() {
        // 相比非公平锁,没有compareAndSetState操作
		acquire(1);
	}
    
    // 实现AQS中的tryAcquire方法
	protected final boolean tryAcquire(int acquires) {
        // 获取当前线程
		final Thread current = Thread.currentThread();
        // 获取当前state的值
		int c = getState();
        // 如果此时没有线程加锁,则判断等待队列中是否有等待的线程
		// 如果没有等待的线程,则自旋将state加1,成功则设置独占线程为当前线程
		if (c == 0) {
			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;
	}
}

我们会发现,公平锁和非公平锁的区别就是非公平锁在判断state=0的时候会通过CAS操作尝试去加一次锁,加锁失败再去调用AQS中的acquire方法,相当于先插一次队,插队不成功,再乖乖去后面排队,而公平锁则不会,因为它需要看等待队列里面有没有线程已经在排队了,所以会直接调用AQS中的acquire方法,流程图如下:

到这里,是不是感觉有了AQS这么个玩意,我们自定义实现锁的操作很easy,在获取锁的时候就是获取当前state,给state自增操作,然后通过CAS自旋设置state,拿不到就去排队,而且入队和出队的实现AQS都帮你干好了,释放锁的时候就是获取当前state,给state自减操作,然后通过CAS设置state,当state为0的时候,表示已经完全是否掉锁了,所以我们在使用ReentrantLock的时候,在调用lock后,一定要在finally中调用unlock操作,如果不这样,那么state状态无法一致,就会引起很多问题,所以这个一定要注意:调用lock方法一定要调用unlock方法,成对出现

我们在使用ReentrantLock的时候,会发现它里面还提供两个方法,一个是tryLock,一个是带参数的tryLock方法:

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

其实上面两个方法的底层实现和lock方法差不多,但是也有区别,还是用刚开始的例子,当我们在调用ReentrantLock中的lock方法而没有调用unlock方法的时候,会发现后面的线程会一直阻塞,所以lock方法是属于阻塞方法,我们调用的tryLock方法是一个有返回值的方法,当调用tryLock()方法的时候,它会获取锁,没有获取到锁,就返回false,获取到返回true,而tryLock(long,TimeUnit)方法表示在规定时间内阻塞,如果规定的时间内获取到锁,就返回true,否则返回false,注意:如果在当前线程中以将中断表示位设为true,例如Thread.currentThread().interrupt();执行lock.lock(long time,TimeUnit unit)就会报java.lang.InterruptedException异常

    private int count =0;
    private ReentrantLock lock = new ReentrantLock();

    public int calculateCount(){
        try{
            lock.lock();
            return count++;
        }finally {
            //lock.unlock();
        }
    }

借用别人的写的,下面对lock和tryLock方法做一个对比:

lock()
当锁可用,并且当前线程没有持有该锁,直接获取锁并把state set为1.
当锁可用,并且当前线程已经持有该锁,直接获取锁并把state增加1.
当锁不可用,那么当前线程被阻塞,休眠一直到该锁可以获取,然后把持有state设置为1.

tryLock()
当获取锁时,只有当该锁资源没有被其他线程持有才可以获取到,并且返回true,同时设置持有state为1;
当获取锁时,当前线程已持有该锁,那么锁可用时,返回true,同时设置持有state加1;
当获取锁时,如果其他线程持有该锁,无可用锁资源,直接返回false,这时候线程不用阻塞等待,可以先去做其他事情;
即使该锁是公平锁fairLock,使用tryLock()的方式获取锁也会是非公平的方式,只要获取锁时该锁可用那么就会直接获取并返回true。这种直接插入的特性在一些特定场景是很有用的。但是如果就是想使用公平的方式的话,可以试一试tryLock(0, TimeUnit.SECONDS),几乎跟公平锁没区别,只是会监测中断事件。

tryLock(long timeout, TimeUnit unit)
从上面代码中可以看出,获取锁成功或者超时之后返回。而且在公平锁和非公平锁的场景下都可以使用,只是会增加对中断事件的监测。
当获取锁时,锁资源在超时时间之内变为可用,并且在等待时没有被中断,那么当前线程成功获取锁,返回true,同时当前线程持有锁的state设置为1.
当获取锁时,在超时时间之内没有锁资源可用,那么当前线程获取失败,不再继续等待,返回false.
当获取锁时,在超时等待时间之内,被中断了,那么抛出InterruptedException,不再继续等待.
当获取锁时,在超时时间之内锁可用,并且当前线程之前已持有该锁,那么成功获取锁,同时持有state加1.

发布了241 篇原创文章 · 获赞 305 · 访问量 54万+

猜你喜欢

转载自blog.csdn.net/HarderXin/article/details/93749770