Java并发编程之锁机制之(ReentrantLock)重入锁

最近在忙公司的项目,如今终于有时间来写博客啦~开心开心java

前言

经过前面的文章,咱们已经了解了AQS(AbstractQueuedSynchronizer)内部的实现与基本原理。如今咱们来了解一下,Java中为咱们提供的Lock机制下的锁实现--ReentrantLock(重入锁),阅读该篇文章以前,但愿你已阅读如下文章。编程

ReentrantLock基本介绍

ReentrantLock是一种可重入互斥锁,它具备与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。bash

ReentrantLock 将由最近成功得到锁,而且尚未释放该锁的线程所拥有。当锁没有被另外一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。若是当前线程已经拥有该锁,此方法将当即返回。可使用isHeldByCurrentThread()getHoldCount()方法来检查此状况是否发生。多线程

此类的构造方法接受一个可选的公平参数。当设置为 true 时(也是当前ReentrantLock为公平锁的状况),在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。不然此锁将没法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的整体吞吐量(即速度很慢,经常极其慢),可是在得到锁和保证锁分配的均衡性时差别较小。不过要注意的是,公平锁不能保证线程调度的公平性。所以,使用公平锁的众多线程中的一员可能得到多倍的成功机会,这种状况发生在其余活动线程没有被处理而且目前并未持有锁时。还要注意的是,未定时的 tryLock 方法并无使用公平设置。由于即便其余线程正在等待,只要该锁是可用的,此方法就能够得到成功。并发

ReentrantLock 类基本结构

经过上文的简单介绍后,我相信不少小伙伴仍是一脸懵逼,只知道上文咱们提到了ReentrantLocksynchronized相比有相同的语义,同时其内部分为了公平锁非公平锁两种锁的类型,且该锁是支持重进入的。那么为了方便你们理解这些知识点,咱们先从其类的基本结构讲起。具体类结构以下图所示:ide

ReentrantLock.png

从上图中咱们能够看出,在ReentrantLock类中,定义了三个静态内部类,SyncFairSync(公平锁)NonfairSync(非公平锁)。其中Sync继承了AQS(AbstractQueuedSynchronizer),而FairSyncNonfairSync又分别继承了Sync。关于ReentrantLock基本类结构以下所示:函数

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

	//默认无参构造函数,默认为非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
	//带参数的构造函数,用户本身来决定是公平锁仍是非公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    //抽象基类继承AQS,公平锁与非公平锁继承该类,并分别实现其lock()方法
    abstract static class Sync extends AbstractQueuedSynchronizer {
        abstract void lock();
        //省略部分代码..
    }
    
	//非公平锁实现
    static final class NonfairSync extends Sync {...}
    
    //公平锁实现
    static final class FairSync extends Sync {....}
   
    //锁实习,根据具体子类实现调用
    public void lock() {
        sync.lock();
    }
	//响应中断的获取锁
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
	//尝试获取锁,默认采用非公平锁方法实现
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
	//超时获取锁
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
	//释放锁
    public void unlock() {
        sync.release(1);
    }
    //建立锁条件(从Condetion来理解,就是建立等待队列)
    public Condition newCondition() {
        return sync.newCondition();
    }
    //省略部分代码....
}
复制代码

这里为了方便你们理解ReentrantLock类的总体结构,我省略了一些代码及从新排列了一些代码的顺序。工具

从代码中咱们能够看出。整个ReentrantLock类的实现其实都是交给了其内部FairSyncNonfairSync两个类。在ReentrantLock类中有两个构造函数,其中不带参数的构造函数中默认使用的NonfairSync(非公平锁)。另外一个带参数的构造函数,用户本身来决定是FairSync(公平锁)仍是非公平锁。post

重进入实现

在上文中,咱们提到了ReentrantLock是支持重进入的,那什么是重进入呢?重进入是指任意线程在获取到锁以后可以再次获取该锁,而不会被锁阻塞。那接下来咱们看看这个例子,以下所示:ui

class ReentrantLockDemo {
    private static final ReentrantLock lock = new ReentrantLock();
    
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                methodA();
            }
        });
        thread.start();
    }
    
    public static void methodA() {
        lock.lock();
        try {
            System.out.println("我已经进入methodA方法了");
            methodB();//方法A中继续调用方法B
        } finally {
            lock.unlock();
        }
    }

    public static void methodB() {
        lock.lock();
        try {
            System.out.println("我已经进入methodB方法了");
        } finally {
            lock.unlock();
        }
    }
}
//输出结果
我已经进入methodA方法了
我已经进入methodB方法了
复制代码

在上述代码中咱们声明了一个线程调用methodA()方法。同时在该方法内部咱们又调用了methodB()方法。从实际的代码运行结果来看,当前线程进入方法A以后。在方法B中再次调用lock.lock();时,该线程并无被阻塞。也就是说ReentrantLock是支持重进入的。那下面咱们就一块儿来看看其内部的实现原理。

由于ReenTrantLock将具体实现交给了NonfairSync(非公平锁)FairSync(公平锁)。同时又由于上述提到的两个锁,关于重进入的实现又很是类似。因此这里将采用NonfairSync(非公平锁)的重进入的实现,来进行分析。但愿读者朋友们阅读到这里的时候须要注意,不是我懒哦,是真的很类似哦。

好了下面咱们来看代码。关于NonfairSync代码以下所示:

static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))////直接获取同步状态成功,那么就再也不走尝试获取锁的过程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
复制代码

当咱们调用lock()方法时,经过CAS操做将AQS中的state的状态设置为1,若是成功,那么表示获取同步状态成功。那么会接着调用setExclusiveOwnerThread(Thread thread)方法来设置当前占有锁的线程。若是失败,则调用acquire(int arg)方法来获取同步状态(该方法是属于AQS中的独占式获取同步状态的方法,对该方法不熟悉的小伙伴,建议阅读Java并发编程之锁机制之AQS(AbstractQueuedSynchronizer))。而该方法内部会调用tryAcquire(int acquires)来尝试获取同步状态。经过观察,咱们发现最终会调用Sync类中的nonfairTryAcquire(int acquires)方法。咱们继续跟踪。

final boolean nonfairTryAcquire(int acquires) {
		    //获取当前线程
            final Thread current = Thread.currentThread();
            int c = getState();
            //(1)判断同步状态,若是未设置,则设置同步状态
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //(2)若是当前线程已经获取了同步状态,则增长同步状态的值。
            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;
        }
复制代码

从代码上来看,该方法主要走两个步骤,具体以下所示:

  • (1)先判断同步状态, 若是不曾设置,则设置同步状态,并设置当前占有锁的线程。
  • (2)判断是不是同一线程,若是当前线程已经获取了同步状态(也就是获取了锁),那么增长同步状态的值。

也就是说,若是同一个锁获取了锁N(N为正整数)次,那么对应的同步状态(state)也就等于N。那么接下来的问题来了,若是当前线程重复N次获取了锁,那么该线程是否须要释放锁N次呢?答案固然是必须的。当咱们调用ReenTrantLock的unlock()方法来释放同步状态(也就是释放锁)时,内部会调用sync.release(1);。最终会调用Sync类的tryRelease(int releases)方法。具体代码以下所示:

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;
        }
复制代码

从代码中,咱们能够知道,每调用一次unlock()方法会将当前同步状态减一。也就是说若是当前线程获取了锁N次,那么获取锁的相应线程也须要调用unlock()方法N次。这也是为何咱们在以前的重入锁例子中,为何methodB方法中也要释放锁的缘由。

非公平锁

在ReentrantLock中有着非公平锁公平锁的概念,这里我先简单的介绍一下公平这两个字的含义。这里的公平是指线程获取锁的顺序。也就是说锁的获取顺序是按照当前线程请求的绝对时间顺序,固然前提条件下是该线程获取锁成功

那么接下来,咱们来分析在ReentrantLock中的非公平锁的具体实现。

这里须要你们具有AQS(AbstractQueuedSynchronizer)类的相关知识。若是你们不熟悉这块的知识。建议你们阅读Java并发编程之锁机制之AQS(AbstractQueuedSynchronizer)

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            if (compareAndSetState(0, 1))//直接获取同步状态成功,那么就再也不走尝试获取锁的过程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        //省略部分代码...
    }
复制代码

当在ReentrantLock在非公平锁的模式下,去调用lock()方法。那么接下来最终会走AQS(AbstractQueuedSynchronizer)下的acquire(int arg)(独占式的获取同步状态),也就是以下代码:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
复制代码

那么结合以前咱们所讲的AQS知识,在多个线程在独占式请求共享状态下(也就是请求锁)的状况下,在AQS中的同步队列中的线程节点状况以下图所示:

aqs同步队列中线程节点的状况.png

那么咱们试想一种状况,当Nod1中的线程执行完相应任务后,释放锁后。这个时候原本该唤醒当前线程节点的下一个节点,也就是Node2中的线程。这个时候忽然另外一线程忽然来获取线程(这里咱们用节点Node5来表示)。具体状况以下图所示:

忽然线程请求锁的状况.png

那么根据AQS中独占式获取同步状态的逻辑。只要Node5对应的线程获取同步状态成功。那么就会出现下面的这种状况,具体状况以下图所示:

线程抢占后最终的状况.png

从上图中咱们能够看出,因为Node5对象的线程抢占了获取同步状态(获取锁)的机会,自己应该被唤醒的Node2线程节点。由于获取同步状态失败。因此只有再次的陷入阻塞。那么综上。咱们能够知道。非公平锁获取同步状态(获取锁)时不会考虑同步队列中中等待的问题。会直接尝试获取锁。也就是会存在后申请,可是会先得到同步状态(获取锁)的状况。

公平锁

理解了非公平锁,再来理解公平锁就很是简单了。下面咱们来看一下公平锁与非公平锁的加锁的源码:

非公平锁与公平锁源码区别.png
从源码咱们能够看出,非公平锁与公平锁之间的代码惟一区别就是多了一个判断条件 !hasQueuedPredecessors()(图中红框所示)。那咱们查看其源码(该代码在AQS中,强烈建议阅读 Java并发编程之锁机制之AQS(AbstractQueuedSynchronizer)

public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
复制代码

代码理解理解起来很是简单,就是判断当前当前head节点的next节点是否是当前请求同步状态(请求锁)的线程。也就是语句 ((s = h.next) == null || s.thread != Thread.currentThread()。那么接下来结合AQS中的同步队列咱们能够获得下图:

公平锁抢占状况.png

那么综上咱们能够得出,公平锁保证了线程请求的同步状态(请求锁)的顺序。不会出现另外一个线程抢占的状况。

最后

该文章参考如下图书,站在巨人的肩膀上。能够看得更远。

  • 《Java并发编程的艺术》

推荐阅读

相关文章
相关标签/搜索