ReentrantLock,可重入锁,是一种递归无阻塞的同步机制,比synchronized更强大、灵活的锁机制,可以减少死锁发生的概率。

一、定义

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
    abstract static class Sync extends AbstractQueuedSynchronizer {

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            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;
        }

        protected final boolean tryRelease(int releases) {
            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;
        }

    }
    //默认非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    //fair为false时,采用公平锁策略
    public ReentrantLock(boolean fair) {    
        sync = fair ? new FairSync() : new NonfairSync();
    }
    public void lock() {
        sync.lock();
    }
    public void unlock() {    sync.release(1);}
    public Condition newCondition() {    
        return sync.newCondition();
    }
    ...
}

从源代码可以Doug lea巧妙的采用组合模式把lock和unlock方法委托给同步器完成。、

二、使用方式

Lock lock = new ReentrantLock();
lock.lock();
try {
    // 处理逻辑
} finally {
    lock.unlock();
}

需要显示的获取锁,并在finally块中显示的释放锁,目的是保证在获取到锁之后,最终能够被释放。

三、公平锁/非公平锁

ReentrantLock分为公平锁和非公平锁,默认是非公平锁

公平锁:锁的获取顺序是按照FIFO原则,代价是大量的线程切换。公平的锁机制往往没有非公平的效率高,公平锁能够减少饥饿的概率,等待越久的请求越是容易的到优先满足

非公平锁:CAS设置同步状态成功,表示当前线程成功获得锁,获得锁的线程释放锁后,再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。 非公平锁可能使得线程节,但是极少的线程切换,保证了较大的吞吐量。 获取锁的枪战机制,是随机获得锁的。

3.1 非公平锁实现

在非公平锁中,每当线程执行lock方法时,都尝试利用CAS把state从0设置为1。

那么Doug lea是如何实现锁的非公平性呢?
我们假设这样一个场景:

  1. 持有锁的线程A正在running,队列中有线程BCDEF被挂起并等待被唤醒;
  2. 在某一个时间点,线程A执行unlock,唤醒线程B;
  3. 同时线程G执行lock,这个时候会发生什么?线程B和G拥有相同的优先级,这里讲的优先级是指获取锁的优先级,同时执行CAS指令竞争锁。如果恰好线程G成功了,线程B就得重新挂起等待被唤醒。

通过上述场景描述,我们可以看书,即使线程B等了很长时间也得和新来的线程G同时竞争锁,如此的不公平。

    /**
     * Sync object for non-fair locks
     */
    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() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

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

下面我们用线程A和线程B来描述非公平锁的竞争过程。

  1. 线程A和B同时执行CAS指令,假设线程A成功,线程B失败,则表明线程A成功获取锁,并把同步器中的exclusiveOwnerThread设置为线程A。
  2. 竞争失败的线程B,在nonfairTryAcquire方法中,会再次尝试获取锁,Doug lea会在多处尝试重新获取锁,应该是在这段时间如果线程A释放锁,线程B就可以直接获取锁而不用挂起。完整的执行流程如下

java并发12-细说ReentrantLock

3.2 公平锁实现

在公平锁中,每当线程执行lock方法时,如果同步器的队列中有线程在等待,则直接加入到队列中。
场景分析:

  1. 持有锁的线程A正在running,对列中有线程BCDEF被挂起并等待被唤醒;
  2. 线程G执行lock,队列中有线程BCDEF在等待,线程G直接加入到队列的对尾。

所以每个线程获取锁的过程是公平的,等待时间最长的会最先被唤醒获取锁。

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) {
                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;
        }
    }

3.3 使用实例

public class ReentrantLockTest {
        //公平锁
    static Lock lock = new ReentrantLock(true);

    public static void main(String[] args) throws InterruptedException {

        for(int i=0;i<2;i++){
            new Thread(new ThreadDemo(i)).start();
        }

    }

    static class ThreadDemo implements Runnable {
        Integer id;

        public ThreadDemo(Integer id) {
            this.id = id;
        }

        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<5;i++){
                lock.lock();
                System.out.println("获得锁的线程:"+id);
                lock.unlock();
            }
        }
    }
}

运行以上代码,运行结果如下:

获得锁的线程:0
获得锁的线程:1
获得锁的线程:0
获得锁的线程:1
获得锁的线程:0
获得锁的线程:1
获得锁的线程:0
获得锁的线程:1
获得锁的线程:0
获得锁的线程:1

我们可以看到,线程0和线程1轮流获得锁,很公平

如果将lock改为非公平锁,运行结果如下:

获得锁的线程:1
获得锁的线程:1
获得锁的线程:1
获得锁的线程:1
获得锁的线程:1
获得锁的线程:0
获得锁的线程:0
获得锁的线程:0
获得锁的线程:0
获得锁的线程:0

可以看到,线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题。

大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。

四、可重入性

ReentrantLock是独占锁且可重入的。重入锁,即线程可以重复获取已经持有的锁。在非公平和公平锁中,都对重入锁进行了实现。

    if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }

实现可重入锁需要满足以下两个条件

  • 线程再次获得锁时, 锁需要识别获取锁的线程是否为当前已获得该锁的线程,如果是,则再次获取成功
  • 锁的最终释放:线程重复N次获取了锁,随后在第N次释放该锁时,其他线程才能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,当锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
public class ReentrantLockTest {
    
    private static int i = 0;

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        lock.lock();
        try {
            i++;
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }
}

参考资料

https://www.cnblogs.com/takumicx/p/9338983.html

https://www.jianshu.com/p/4358b1466ec9https://blog.csdn.net/hanchao5272/article/details/79681037)