并发编程必会之-Lock

并发编程必会之-Lock

并发编程中,synchronized关键字是隐式的加锁和释放锁。JVM层面实现的
Lock是显示的加锁和释放锁。代码层面实现的
代码层面的实现大量使用了CAS,如果对CAS不了解的,可以看我的另一篇博客
并发编程必会之-CAS
下面是锁的最简单使用

        Lock lock = new ReentrantLock();
        try {
            lock.lock();
            //同步块。。。。。
        } finally {
            //必须在finally里释放锁
            lock.unlock();
        }

Lock是接口,是锁的顶级接口
里面就是定义了加锁解锁的方法,以及一个Condition
在这里插入图片描述
其中的加锁方法有四个,下面就是简单的介绍。

        //获取不到锁一直等待,调用线程的interrupt()方法也无法中断
        lock.lock();
        //获得锁返回true,获取不到直接返回false,执行下面的逻辑。不会进行等待
        lock.tryLock();
        //获得锁返回true,获取不到等待参数设定的时间后返回false,执行下面的逻辑。等待过程中可以通过interrupt()中断。
        lock.tryLock(2, TimeUnit.SECONDS);
        //与lock.lock()不同的是可以可以通过interrupt()中断,其余的一样
        lock.lockInterruptibly();

至于Condition后面再讲解

Lock最重要的实现类ReentrantLock

ReentrantLock实现了上面的Lock接口,其构造函数,默认无参是非公平锁,加参数true是公平锁,顾名思义,公平锁就是获取锁的顺序是按照先来后到获取的,非公平锁讲就是可能会出现插队的情况。效率上当然是非公平锁高,如果有场景要求获取锁的顺序要先来后到,那就要使用公平锁。

	//默认无参构造函数是非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
	//根据参入的参数来构造公平锁和非公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

我们再看下它的加锁解锁方法,至于加锁方法的区别,在上面已经提到过了

	//普通加锁
    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);
    }

从上面我们看出加锁解锁方法其实都是通过调用sync这个类来完成的。核心功能就是加锁解锁,我们这里重点讲解的也就是加锁解锁方法。我们可以先看下ReentrantLock这个类的其他方法,基本上属于一些辅助的方法,不必要所有的方法都知道原理,只知道功能就可以了。

//获得当前线程获取锁的次数,当前线程没有获得锁为0,获得一次加1
int getHoldCount()
//当前线程是否持有锁
boolean isHeldByCurrentThread()
//该锁是否被任意线程持有,即有没有线程已经获得了该锁
boolean isLocked()
//该锁是否为公平锁
boolean isFair()
//返回该锁的持有线程,如果没有则为null
Thread getOwner()
//是否有其他线程在等待获得该锁
boolean hasQueuedThreads()
//参数中的线程是否在等待获得锁
boolean hasQueuedThread(Thread thread)
//等待获得该锁的线程数量
int getQueueLength()
//等待获得该锁的线程数量集合
Collection<Thread> getQueuedThreads()
//是否有线程等待该锁持有的Condition条件,打个比方就是有线程wait了,等待notify,那么就会返回true,这里的wait就相当于Condition.await
boolean hasWaiters(Condition condition)
//在该锁指定Conditoin上等待线程的长度
int getWaitQueueLength(Condition condition)
//在该锁指定Conditoin上等待线程的集合
Collection<Thread> getWaitingThreads(Condition condition)

ReentrantLock的内部类Sync

ReentrantLock最重要的加锁解锁方法都是通过它自己的内部类Sync来实现的

在这里插入图片描述
Sync继承了AQS。接下来会重点讲解AQS,他是并发的基石。

abstract static class Sync extends AbstractQueuedSynchronizer

NonfairSync 和 FairSync 继承是Sync的子类,分别对应非公平锁和公平锁的实现。同样他们是ReentrantLock的内部类

//公平锁
static final class FairSync extends Sync
//非公平锁
static final class NonfairSync extends Sync

这里我们主要就是围绕加锁解锁通过代码来看ReentrantLock的实现原理

    //ReentrantLock.lock方法就是调用的这儿
    //上面我们知道如果实例化的ReentrantLock是公平锁
    public void lock() {
    //如果是公平锁就调用FairSync.lock方法
    //如果是非公平锁就调用NonfairSync.lock方法
        sync.lock();
    }

//FairSync.lock方法
static final class FairSync extends Sync {
        final void lock() {
            acquire(1);
        }
    }
//NonfairSync.lock方法
static final class NonfairSync extends Sync {
        final void lock() {
        //利用CAS将状态0变为1,如果成功那么获取锁,否则去排队
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
    }

从上面的代码中我们可以看出来,公平锁和非公平锁就只是差了一行代码而已。
那么就这么理解:
公平锁是遵循先进先出(FIFO)原则,来了以后加入队列慢慢排队获取锁。
非公平锁是一进来先试图获取锁,如果没有获取锁,那么就乖乖的去排队。
公平锁和非公平锁知道了他们的差别,就是一行代码的差别,那么这里他们的实现关键代码就是acquire()了,我们接下来看acquire()这个方法。

因为Sync是继承自AbstractQueuedSynchronizer的,这里的acquire()方法点进去以后发现它是AbstractQueuedSynchronizer这个类里面的方法,我们在看到这儿的时候,就不得不先去了解一下AbstractQueuedSynchronizer了。因为核心逻辑都是AQS实现的。

并发基石AQS

AQS原理简介

定义:
AQS
就是AbstractQueuedSynchronizer的简称,翻译过来就是队列同步器,它是锁或者其他同步组件的基础框架
原理:
AQS通过自己的成员变量state的值来确定是否获得锁,如果state=0,说明没有线程获得锁,当state = 1时,说明有线程已经获取锁,如果其它线程要进同步块,那么就会到同步队列中等待,AQS有个内部类Node,保存了线程的状态,通过双向链表的形式来维护一个同步队列,当有新线程来获取锁的时候,就会将新线程封装成Node加入到链表的尾部。另外AQS内部还维护着一个等待队列,创建一个Conditon,就会有与之相对应的等待队列,当调用Condition.await方法之后,线程就会释放锁,然后到与之对应的Condition的等待队列中,当线程被唤醒后,即调用**Condition.signal()**方法后,就会将该线程从等待队列中移除,然后放入同步队列当中。

AQS中关键的成员变量有四个

//state很重要,设计核心就是围绕state来的,state默认是0,代表没有线程获取锁
//有线程获取就变为了1,同一个线程可以重复获取,即可重入锁,重入一次,值便会
//相应加1。
private volatile int state;
//AQS的内部类。维护着线程的状态,这里是指向同步队列的头部,
//head是空节点,不存储信息。
private transient volatile Node head;
//同步队列队尾
private transient volatile Node tail;
//记录获取锁的线程
private transient Thread exclusiveOwnerThread;

state我们知道了,它就是一个int型的成员变量,然后接下来我们看下Node

static final class Node {
        //表明该节点的模式是共享模式
        static final Node SHARED = new Node();
        //表明该节点的模式是独占模式
        static final Node EXCLUSIVE = null;
		//取消状态:标识该节点超时或中断,需将该节点从同步队列中剔除
        static final int CANCELLED =  1;
		//唤醒状态:标识该节点被唤醒,可以去尝试获取锁
        static final int SIGNAL    = -1;
		//condition状态:标识该节点在condition的等待队列中
        static final int CONDITION = -2;
		//与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
        static final int PROPAGATE = -3;
        //该值代表该节点的状态,默认是0,表示等待状态,其他状态有上面的四种
        volatile int waitStatus;
		//该节点的前驱节点
        volatile Node prev;
		//该节点的后继节点,这里看出同步队列是双向列表。
        volatile Node next;
		//该节点封装的线程
        volatile Thread thread;
		//该Node在同步队列中时,其值为EXCLUSIVE或SHARED,独占模式下,
		//该节点无意义,共享模式下,是指向同步队列中的下一个共享节点。
		//该Node在Condition的等待队列中时,代表下一个等待队列中的下一个节点
		//这里可以知道是,同步队列和Condition的等待队列用的是同一个实体类 
		//Node来表示。所以在不同队列中,Node的成员变量的含义可能不同。
        Node nextWaiter;

		//用来判断该节点是否是共享模式
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

		//返回该节点的前驱节点。
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {  
        }
		//同步队列构建的时候用此构造函数
        Node(Thread thread, Node mode) {
            this.nextWaiter = mode;
            this.thread = thread;
        }
		//在Condition构建等待队列时用此构造函数,将waitStatus标识为Condition状态
        Node(Thread thread, int waitStatus) {
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

在这里插入图片描述
这里可以知道Node分为共享模式和独占模式
共享模式:顾名思义,那就是同一个锁可以有多个线程同时获得,比如Semaphore(信号量),CountDownLatch,或者读锁等都是基于共享锁模式实现的。
独占模式:这就更好理解了,锁只能由一个线程获取,比如我们今天的主角ReentrantLock。

从上面的分析中可以看出AQS作为并发组件,已经保证了并发实现的框架,通过state是否为0判断是否有线程持有锁,然后又提供了同步队列和等待队列。相当于锁已经给做好了,至于怎么用,那么具体的类就需要具体的实现。即如何获取锁,释放锁的实现,是需要子类去实现的。其提供了四个方法。
tryAcquire: 获取独占锁
tryRelease:释放独占锁
tryAcquireShared:获取共享锁
tryReleaseShared:释放共享锁
在这里插入图片描述
共享锁我们在这里不进行讲解,因为我们是基于ReentrantLock来分析,ReentrantLock又是独占锁,所以我们这里讲解的就是独占锁。共享锁和独占锁很类似,有兴趣的朋友可以看下Semaphore的具体实现,他们都是基于AQS来实现的。

通过ReentrantLock来进一步分析AQS的实现

上面我们代码分析到acquire方法。

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

这里acquire(1) 参数为1,因为是独占模式,所以要想获得锁,就要将AQS中的state变量值变为1。acquire()方法是AQS中的方法。逻辑是尝试去获取锁,如果成功了,那么就可以去同步块里面工作了,如果失败了,那么就加入同步队列,并且阻塞该线程。

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

tryAcquire()方法上面说过,具体实现是在子类中实现,我们这里就是在ReentrantLock的NonfairSync和FairSync中实现的,分别对应非公平锁的实现和公平锁的实现。从下面的代码可以看出来,非公平锁是上来就尝试获取锁,获取不到才返回false,公平锁是判断同步队列为空,或者同步队列中只有一个节点才尝试去获取锁,否则就老老实实的返回false。
我们知道非公平锁在调用这个方法之前就已经尝试获取过一次锁,这里又尝试获取了一次,所以非公平锁总共尝试获取了两次锁,都没有成功的话才被放入了同步队列中的。

//NonfairSync中实现的tryAcquire方法
        final boolean nonfairTryAcquire(int acquires) {
        //获取当前线程
            final Thread current = Thread.currentThread();
            //获取AQS中status的值
            int c = getState();
            //如果值为0,那么利用CAS将值变为1,并且将AQS中标记独占线程设为当前线程,返回true,代表获取锁成功。
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //否则判断获取锁的线程是不是当前线程,如果是,那么说明是重入,state值加1,返回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;
        }

 --------------------------------------------------------------
 --------------------------------------------------------------
 --------------------------------------------------------------

        //FairSync中实现的tryAcquire方法
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //如果state=0,如果没有同步队列
            if (c == 0) {
            //如果没有同步队列或者同步队列中只有一个前驱节点,那么就用CAS尝试去获取锁,成功返回True
                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;
        }

下面继续回到acquire(int arg)方法。

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

如果获取锁了,皆大欢喜,接下来的代码就不用执行了,但是如果获取锁失败的话,接下来就会执行addWaiter(Node.EXCLUSIVE), arg)方法,将该线程封装成节点放入同步队列的尾部。因为这里是独占锁,所以线程封装的就是独占锁。

    private Node addWaiter(Node mode) {
    //将该线程封装为一个Node
        Node node = new Node(Thread.currentThread(), mode);
        //拿到同步队列的队尾节点
        Node pred = tail;
        //同步队列尾节点不为null执行入队操作
        if (pred != null) {
        //将该节点的前驱指向同步队列的尾节点,相当于入队操作
            node.prev = pred;
            //利用CAS操作将主内存中tail变量的值变为当前节点
            if (compareAndSetTail(pred, node)) {
            //双向链表,该节点加入队列以后将之前队尾的节点的下一个节点指向该节点
                pred.next = node;
                return node;
            }
        }
        //走到这里说明同步队列中没有节点,那么就会走enq方法去初始化队列
        enq(node);
        return node;
    }

--------------------------------------------------------
--------------------------------------------------------
--------------------------------------------------------
    private Node enq(final Node node) {
    //这里是一个死循环,从下面的代码中可以看出,如果队列为空的话
    //那么会初始化队列,即初始化head和tail,指向new Node()一个
    //空白,然后将当前节点放入插入这个队列,tail指向当前节点。
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

走到这一步说明当前线程已经封装成Node放入了双向链表构成的同步队列当中。接下来继续回到acquire(int arg)方法。

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

现在这里就是该执行acquireQueued()这个方法了,第一个参数就是已经放入到同步队列的当前Node,第二个参数是1,就是想要更改AQS中state的值。下面我们就看下这个方法。

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //这里依然是个死循环,直到执行成功为止
            for (;;) {
            //这里是获得当前节点的上一个节点。
                final Node p = node.predecessor();
                //如果上一个节点是Head,说明当前Node就是同步队列中
                //的第一个线程了,head节点是空的,里面不包含线程。
                //然后当前节点就开始尝试获取锁。获取锁成功以后那么
                //就不需要当前这个节点了,就把当前这个节点的封装的线程
                //信息设为null,将当前节点当成头节点。
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //这里干的事就是如果前驱节点是SIGNAL即等待唤醒状态,
                //那么就挂起当前线程,如果前驱节点是CANCLE即取消状态
                //那么就将前面凡是CANCLE的节点都剔除,直到找到不是
                //CANCLE状态的节点,如果是其他状态那么就将前驱节点的
                //的状态改为SIGNAL状态
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
        //如果最终获取锁失败了,那么就将该节点剔除
            if (failed)
                cancelAcquire(node);
        }
    }
--------------------------------------------------------
--------------------------------------------------------
--------------------------------------------------------
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

上述过程总结一下就是,当新来的线程获取锁失败后,加入到同步队列当中,该线程就会进入一个自旋状态,要么获取锁退出自旋,要么当前线程挂起等待被唤醒并退出自旋。
当且仅当前驱节点是head才去获取锁,因为这是同步队列,肯定要排队的,获取到锁以后,就把head剔除,当前节点head,并且将当前节点的封装的Thread为空,因为都获取到锁了,就会执行同步代码,这里也就不需要当成节点放在同步队列中了,相当于出队的操作。
在这里插入图片描述
如果当前节点的前驱节点不是头节点,那么看看前驱节点的状态是不是等待被唤醒的状态,如果是那么就挂起当前线程,退出自旋,如果不是,那么看前驱节点是不是取消状态,如果是那么就将前驱节点剔除,剔除以后再看下下一个前驱节点,直到找到一个取消状态的节点,然后挂起当前线程退出,如果前驱节点的状态是其他的,即不是等待被唤醒和取消,那么就将前驱节点的状态设置为等待被唤醒,退出自旋挂起。挂起后的线程在释放锁的时候就会被唤醒下一个队列中的节点来让尝试获取锁。
这是总的流程。
在这里插入图片描述

下面我们简单的再看下释放锁的流程,因为加锁流程已经很详细了,释放锁的和加锁的流程大同小异
首先是ReentrantLock里面的unlock

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

调用的是Sync里面的release()方法,调用tryRelease释放锁,无非就是操作AQS的state变量,将其置为0,具体这里不讲了,感兴趣的下去看看就好了,然后就是拿到头节点,然后去唤醒头节点的下一个线程,让下一个线程来尝试获取锁。unparkSuccessor()方法里面

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

这里主要就是在最后面用 LockSupport.unpark(s.thread);这个方法来唤醒下一个线程的。

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
	            LockSupport.unpark(s.thread);
    }

到这里Lock的一系列加锁解锁操作的内部实现原理我们就已经讲解完了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值