Java explicit lock -- ReentrantLock

foreword

 

Using synchronized for locking is implemented internally by jvm called: built-in lock. Since java1.5 , jdk api has introduced a new lock API . They all inherit from Lock , called: explicit lock, such as today's topic ReentrantLock . There are two main reasons why it is called an explicit lock: 1. Compared with the built-in lock, it is implemented internally by the jvm , and the explicit lock is implemented using the java api , specifically based on AQS ( for AQS To understand, click here ); 2. Using Lock to lock requires explicit locking and release of the lock, which is more troublesome than using synchronized for built-in locks. Let's take a look at the basic usage of ReentrantLock :

 

public class ReentrantLockTest {
 
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try{
            // business method
        }catch (Exception e){
            // business exception
        }finally {
            lock.unlock();
        }
    }
}

 

On the surface, using explicit locks is more cumbersome than using built-in locks. You need to manually call lock to lock and unlock to unlock. If you forget to unlock , it will cause serious errors that other threads will never be able to acquire the lock. Since why add an explicit lock?

 

To put it simply, the complement of built-in locks of explicit locks: explicit locks Lock provides implementations such as interrupt locks, timed locks, and polling, which are not available in built-in locks synchronized ; explicit locks can be specified as fair locks or Unfair locks, and built-in locks synchronized locks are unfair. Using synchronized locking can easily lead to deadlock in some cases. In this case, when the function does not acquire the lock for a period of time when the explicit locking is used, the deadlock can be avoided by giving up acquiring the lock.

 

Another obvious difference from built-in locks is that built-in locks can be used on methods, while explicit locks can only be used on code blocks, which means that more fine-grained locking is enforced.

 

It can be said that built-in locks and explicit locks are complementary: explicit locks cannot be used in methods, and it is easy to forget to release the lock; built-in locks cannot be interrupted, and deadlocks are prone to occur in some cases, and built-in locks cannot achieve fair locks. According to these differences, you can selectively use it in your own actual business scenarios. It should be noted that the performance of explicit locks is slightly better than that of built-in locks.

 

The theoretical summary ends everywhere, and the following begins to analyze the implementation principle of explicit locks with ReentrantLock locks. It mainly includes: the implementation principles of exclusive locks, fair locks, unfair locks, interrupt locks, delayed locks, polling locks, and reentrant locks.

 

ReentrantLock implementation principle

 

Implementation of AQS

First of all, the ReentrantLock that needs to be explained is an exclusive lock like the built-in lock. From the name, it is as reentrant as the built-in lock. ReentrantLock is implemented based on AQS like Semaphore and CountDownLatch mentioned in the previous two articles , except that Semaphore and CountDownLatch are both shared lock implementation methods, while ReentrantLock is an exclusive lock implementation, that is to say , the inner class of ReentrantLock is implementing AQS When the two methods tryAcquire and tryRelease are extended. First look at the implementation of AQS by the inner class Sync (Sync also has two subclasses, corresponding to fair and unfair lock implementations respectively ) :

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;
 
    //交给公平和非公平的子类去实现
    abstract void lock();
 
    //非公平的排它尝试获取锁实现
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        //如果AQS的state为0说明获得锁,并且对state加1,其他线程获取锁时被阻塞
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
       
        //判断线程是不是重新获取锁,如果是 无需排队,对AQS的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;
        }
        return false;//都不满足获取锁失败,进入AQS队列阻塞
    }
 
    //公平的排它尝试释放锁实现
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;//对应重入锁而言,在释放锁时对AQS的state字段减1
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {//如果AQS的状态字段已变为0,说明该锁被释放
            free = true;
            setExclusiveOwnerThread(null);
        }
        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();
    }
 
    //获取条件队列
    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;
    }
 
    /**
     * 说明ReentrantLock是可序列化的
     */
    private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}

 

 

非公平锁

非公平锁实现:

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() {
        //判断当前state是否为0,如果为0直接通过cas修改状态,并获取锁
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);//否则进行排队
    }
 
    //调用父类的的非公平尝试获取锁
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

 

非公平锁的实现很简单,在lock获取锁时首先判断判断当前锁是否可以用(AQSstate状态值是否为0),如果是 直接“插队”获取锁,否则进入排队队列,并阻塞当前线程。

 

公平锁

公平锁实现:

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) {
            //AQS队列为空,或者当前线程是头节点 即可获的锁
            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;
    }
}

公平锁的实现,跟Semaphore一样在tryAcquire方法实现中通过hasQueuedPredecessors方法判断当前线程是否是AQS队列中的头结点或者AQS队列为空,并且当前锁状态可用 可以直接获取锁,否则需要排队。

 

重入锁

不论是公平锁还是非公平锁的实现中,在tryAcquire方法中判断如果锁已经被占用,都会判断是否是当前线程占用,如果是 可以再次获取锁(无需排队),并对AQSstate字段加1;在释放锁时每次都减1,直到为0时,其他线程在可用。顺便提一下内置锁synchronized也是可以重入的。

//重入锁实现
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
 

 

排它锁

从两个tryAcquire的实现可以看出ReentrantLock的排它锁实现,根本上是通过AQSstate字段保证的,每次获取锁时都是首先判断state是否为0,并且只有1个线程能获取到锁。这个特性与内置锁synchronized相同。

 

关于ReentrantLockAQS的三个内部类实现分析完毕,接下来看下ReentrantLock的核心方法,实现都很简单基本都是直接调用FairSync或者NonfairSyncAQS的实现。比如lockunlock方法:

public void lock() {
        sync.lock();
    }
 
    public void unlock() {
        sync.release(1);
    }
 

 

延迟锁

延迟锁,指的是在指定时间内没有获取到锁,就取消阻塞并返回获取锁失败,由调用线程自己决定后续操作,比如放弃操作或者创建轮询获取锁。

public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        //调用AQS带延时功能获取方法
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

 

中断锁

tryLock(long timeout, TimeUnit unit)这个方法会抛出InterruptedException异常,可以用于实现中断锁,即等待的时间还未到,可以直接调用interrupt方法以中断获取锁。另外ReentrantLock还有一个方法可以实现中断锁,即:lockInterruptibly方法

public void lockInterruptibly() throws InterruptedException {
        //直接调用AQS的可中断获取方法
        sync.acquireInterruptibly(1);
    }

 

通过调用该方法获取锁跟lock方法一样,如果获取不到会阻塞,不同的是使用这个方法获取锁是可以在外部中断的,但lock方法不行。使用灵活使用可中断锁,可以防止死锁。

 

Ps:中断锁是对获取锁的中断,注意与线程被中断的区别(虽然本质上都是中断线程)。不管是使用显式锁还是内置锁的线程阻塞都是可以被中断的,而中断锁是指线程在获取锁的过程中被阻塞 可以中断现在继续排队获取锁的过程。中断锁:是中断获取锁排队阻塞,可以使用ReentrantLocktryLock(long timeout, TimeUnit unit)lockInterruptibly()这两个方法实现,而使用内置锁synchronized 如果线程已经在排队获取锁是无法被中断的;中断线程:是中断线程执行业务方法过程中的阻塞(如sleep阻塞,BlookingQueue阻塞)。

 

轮询锁

tryLock(long timeout, TimeUnit unit)这个方法延迟获取锁的方法,另外ReentrantLock还有一个非延迟也不阻塞的获取锁方法tryLock(),尝试获取锁,如果没有获取到直接反回false 不阻塞线程:

public boolean tryLock() {
        //默认只有非公平实现
        return sync.nonfairTryAcquire(1);
}

ReentrantLock不直接提供轮询锁api,但可以用tryLock(long timeout, TimeUnit unit)tryLock() 这两个方法实现。即没有获取到锁,可以使用while循环 隔一段时间再次获取,直到获取到为止,这种方式是解决死锁的常用手段。这两个方法的使用区别:tryLock(long timeout, TimeUnit unit)不用在whilesleep,而tryLock()需要自己在whilesleep一会儿,减少资源开销。以tryLock()为例 实现轮询锁:

public class ReentrantLockTest {
 
    public static void main(String[] args) throws Exception{
        ReentrantLock lock = new ReentrantLock(true);
        while (true){
            if(lock.tryLock()){//这里不阻塞
                try{
                    System.out.println("执行业务方法");
                    //业务方法
                    return;
                }catch (Exception e){
                    //业务异常
                }finally {
                    lock.unlock();
                }
 
            }
            //如果没有获取到睡一会儿,再取锁
            Thread.sleep(1000);
        }
    }
}

ReentrantLock中还有一个重要方法newCondition获取条件队列方法,其作用类似使用内置锁时ObjectwaitnotifynotifyAll。这部分内容比较多,在这里就不展开讲解,后面抽时间单独总结下。另外ReentrantLock还有一些其他辅助方法,都比较好理解,就不一一列举,在使用过程中自行查阅即可。

 

总结

 

 

最后再强调下显式锁Lock与内置锁是互补关系,有些场景下只能使用内置锁(比如对方法加锁);有些场景下只能使用Lock(比如 需要防止死锁、或者实现公平锁)。显式锁在java API中常用的还有读写锁ReentrantReadWriteLock,本次主要讲了重入锁ReentrantLock的实现原理和基本用法。

 

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326441246&siteId=291194637