• Lock系列原理和源码分析


    一、Lock简介

    concurent包下的lock子包。锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而java SE5之后,并发包中增加了lock接口,它提供了与synchronized一样的锁功能。虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性Lock接口的方法:

    //获取锁
    void lock(); 
    //获取锁的过程能够响应中断
    void lockInterruptibly() throws InterruptedException;
    //非阻塞式响应中断能立即返回,获取锁放回true反之返回fasle
    boolean tryLock();
    //超时获取锁,在超时内或者未中断的情况下能够获取锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    //获取与lock绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进行等待时会先释放锁,当再次获取锁时才能从等待中返回
    Condition newCondition();
    // 释放锁。
    unlock();

    二、AQS(AbstractQueuedSynchronizer,称队列同步器)

    可以查看源码解释,很详细;

    同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作。锁和同步器很好的隔离了使用者和实现者所需关注的领域。

    AQS的设计是使用模板方法设计模式,它将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法。

    1.同步组件(这里不仅仅指锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用 AQS的方式被推荐定义继承AQS的静态内存类;

    2.AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法;3.AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义;4.在重写AQS的方式时,使用AQS提供的getState(),setState(),compareAndSetState()方法进行修改同步状态

    AQS提供的模板方法可以分为3类:

    1. 独占式获取与释放同步状态;

    2. 共享式获取与释放同步状态;

    3. 查询同步队列中等待线程情况;

    独占式锁:

    
    
    void acquire(int arg);// 独占式获取同步状态,如果获取失败则插入同步队列进行等待;
    void acquireInterruptibly(int arg);// 与acquire方法相同,但在同步队列中进行等待的时候可以检测中断;
    boolean tryAcquireNanos(int arg, long nanosTimeout);// 在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false;
    boolean release(int arg);// 释放同步状态,该方法会唤醒在同步队列中的下一个节点

    共享式锁:

    void acquireShared(int arg);// 共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态;
    void acquireSharedInterruptibly(int arg);// 在acquireShared方法基础上增加了能响应中断的功能;
    boolean tryAcquireSharedNanos(int arg, long nanosTimeout);// 在acquireSharedInterruptibly基础上增加了超时等待的功能;
    boolean releaseShared(int arg);// 共享式释放同步状态

    同步队列:(难啃的骨头,还需自己理解)

    AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法

    1. 节点的数据结构,即AQS的静态内部类Node,节点的等待状态等信息

    2. 同步队列是一个双向队列,AQS通过持有头尾指针管理同步队列

    三、ReentrantLock

    ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性(关于synchronized可以看这篇文章),synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1. 重入性的实现原理;2. 公平锁和非公平锁。1.重入性的实现原理

    要想支持重入性,就要解决两个问题:1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功

    源码分析:(非公平)

    获取:

    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()) {
                    // 3. 再次获取,计数加一
                    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) {
                //1. 同步状态减1
                int c = getState() - releases;
                if (Thread.currentThread() != getExclusiveOwnerThread())
                    throw new IllegalMonitorStateException();
                boolean free = false;
                if (c == 0) {
                    //2. 只有当同步状态为0时,锁成功被释放,返回true
                    free = true;
                    setExclusiveOwnerThread(null);
                }
                // 3. 锁未被完全释放,返回false
                setState(c);
                return free;
            }

    获取n次,需要释放n次,才能释放锁。

    2.公平锁与非公平锁

    //默认构造器是非公平的

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

    获取:(公平锁)

    protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                  //增加了hasQueuedPredecessors的逻辑判断
                    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;
            }

    比较非公平,增加了hasQueuedPredecessors的逻辑判断,方法名就可知道该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。

    公平锁与非公平锁的比较

    1.公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。

    2.公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。

    四、读写锁ReentrantReadWriteLock

    读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。

    1. 公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;

    2. 重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;

    3. 锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁

    写锁的获取:

    当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。

    protected final boolean tryAcquire(int acquires) {
        /*
         * Walkthrough:
         * 1. If read count nonzero or write count nonzero
         *    and owner is a different thread, fail.
         * 2. If count would saturate, fail. (This can only
         *    happen if count is already nonzero.)
         * 3. Otherwise, this thread is eligible for lock if
         *    it is either a reentrant acquire or
         *    queue policy allows it. If so, update state
         *    and set owner.
         */
        Thread current = Thread.currentThread();
        // 1. 获取写锁当前的同步状态
        int c = getState();
        // 2. 获取写锁获取的次数
        int w = exclusiveCount(c);
        if (c != 0) {
            // (Note: if c != 0 and w == 0 then shared count != 0)
            // 3.1 当读锁已被读线程获取或者当前线程不是已经获取写锁的线程的话
            // 当前线程获取写锁失败
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            if (w + exclusiveCount(acquires) > MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            // Reentrant acquire
            // 3.2 当前线程获取写锁,支持可重复加锁
            setState(c + acquires);
            return true;
        }
        // 3.3 写锁未被任何线程获取,当前线程可获取写锁
        if (writerShouldBlock() ||
            !compareAndSetState(c, c + acquires))
            return false;
        setExclusiveOwnerThread(current);
        return true;
    }

    同步状态的低16位用来表示写锁的获取次数

    同步状态的高16位用来表示读锁被获取的次数.

    写锁的释放:

    protected final boolean tryRelease(int releases) {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        //1. 同步状态减去写状态
        int nextc = getState() - releases;
        //2. 当前写状态是否为0,为0则释放写锁
        boolean free = exclusiveCount(nextc) == 0;
        if (free)
            setExclusiveOwnerThread(null);
        //3. 不为0则更新同步状态
        setState(nextc);
        return free;
    }

    读锁的获取:

    protected final int tryAcquireShared(int unused) {
        /*
         * Walkthrough:
         * 1. If write lock held by another thread, fail.
         * 2. Otherwise, this thread is eligible for
         *    lock wrt state, so ask if it should block
         *    because of queue policy. If not, try
         *    to grant by CASing state and updating count.
         *    Note that step does not check for reentrant
         *    acquires, which is postponed to full version
         *    to avoid having to check hold count in
         *    the more typical non-reentrant case.
         * 3. If step 2 fails either because thread
         *    apparently not eligible or CAS fails or count
         *    saturated, chain to version with full retry loop.
         */
        Thread current = Thread.currentThread();
        int c = getState();
        //1. 如果写锁已经被获取并且获取写锁的线程不是当前线程的话,当前
        // 线程获取读锁失败返回-1
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return -1;
        int r = sharedCount(c);
        if (!readerShouldBlock() &&
            r < MAX_COUNT &&
            //2. 当前线程获取读锁
            compareAndSetState(c, c + SHARED_UNIT)) {
            //3. 下面的代码主要是新增的一些功能,比如getReadHoldCount()方法
            //返回当前获取读锁的次数
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return 1;
        }
        //4. 处理在第二步中CAS操作失败的自旋已经实现重入性
        return fullTryAcquireShared(current);
    }

    读锁的释放:

    protected final boolean tryReleaseShared(int unused) {
        Thread current = Thread.currentThread();
        // 前面还是为了实现getReadHoldCount等新功能
        if (firstReader == current) {
            // assert firstReaderHoldCount > 0;
            if (firstReaderHoldCount == 1)
                firstReader = null;
            else
                firstReaderHoldCount--;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                rh = readHolds.get();
            int count = rh.count;
            if (count <= 1) {
                readHolds.remove();
                if (count <= 0)
                    throw unmatchedUnlockException();
            }
            --rh.count;
        }
        for (;;) {
            int c = getState();
            // 读锁释放 将同步状态减去读状态即可
            int nextc = c - SHARED_UNIT;
            if (compareAndSetState(c, nextc))
                // Releasing the read lock has no effect on readers,
                // but it may allow waiting writers to proceed if
                // both read and write locks are now free.
                return nextc == 0;
        }
    }

    锁降级:

    读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级。

    
    
    void processCachedData() {
            rwl.readLock().lock();
            if (!cacheValid) {
                // Must release read lock before acquiring write lock
                rwl.readLock().unlock();
                rwl.writeLock().lock();
                try {
                    // Recheck state because another thread might have
                    // acquired write lock and changed state before we did.
                    if (!cacheValid) {
                        data = ...
                cacheValid = true;
              }
              // Downgrade by acquiring read lock before releasing write lock
              rwl.readLock().lock();
            } finally {
              rwl.writeLock().unlock(); // Unlock write, still hold read
            }
          }
     
          try {
            use(data);
          } finally {
            rwl.readLock().unlock();
          }
        }
    }

    五、Condition

    任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制,同样的, 在java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。

    Condition实现原理

    等待队列要想能够深入的掌握condition还是应该知道它的实现原理,现在我们一起来看看condiiton的源码。创建一个condition对象是通过lock.newCondition(),而这个方法实际上是会new出一个ConditionObject对象,该类是AQS(AQS的实现原理的文章)的一个内部类,有兴趣可以去看看。前面我们说过,condition是要和lock配合使用的也就是condition和Lock是绑定在一起的,而lock的实现原理又依赖于AQS,自然而然ConditionObject作为AQS的一个内部类无可厚非。我们知道在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列,同样的,condition内部也是使用同样的方式,内部维护了一个 等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。另外注意到ConditionObject中有两个成员变量:

    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;

    这样我们就可以看出来ConditionObject通过持有等待队列的头尾指针来管理等待队列。主要注意的是Node类复用了在AQS中的Node类,其节点状态和相关属性可以去看AQS的实现原理的文章,如果您仔细看完这篇文章对condition的理解易如反掌,对lock体系的实现也会有一个质的提升。Node类有这样一个属性:

    //后继节点
    Node nextWaiter;

    进一步说明,等待队列是一个单向队列,而在之前说AQS时知道同步队列是一个双向队列

    await实现原理当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock。接下来,我们还是从源码的角度去看,只有熟悉了源码的逻辑我们的理解才是最深的。await()方法源码为:

    public final void await() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        // 1. 将当前线程包装成Node,尾插入到等待队列中
        Node node = addConditionWaiter();
        // 2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
        int savedState = fullyRelease(node);
        int interruptMode = 0;
        while (!isOnSyncQueue(node)) {
            // 3. 当前线程进入到等待状态
            LockSupport.park(this);
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
        // 4. 自旋等待获取到同步状态(即获取到lock)
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null) // clean up if cancelled
            unlinkCancelledWaiters();
        // 5. 处理被中断的情况
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }

    代码的主要逻辑请看注释,我们都知道当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalAll后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理。

    signal/signalAll实现原理调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。我们来通过看源码的方式来看这样的猜想是不是对的,signal方法源码为:

    public final void signal() {
        //1. 先检测当前线程是否已经获取lock
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        //2. 获取等待队列中第一个节点,之后的操作都是针对这个节点
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);
    }

    示例:

    public class Test2 {
        private static ReentrantLock lock = new ReentrantLock();
        private static Condition condition = lock.newCondition();
        private static volatile boolean flag = false;
    ​
        public static void main(String[] args) {
            Thread waiter = new Thread(new waiter());
            waiter.start();
            Thread signaler = new Thread(new signaler());
            signaler.start();
        }
    ​
        static class waiter implements Runnable {
    ​
            @Override
            public void run() {
                lock.lock();
                try {
                    while (!flag) {
                        System.out.println(Thread.currentThread().getName() + "当前条件不满足等待");
                        try {
                            condition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(Thread.currentThread().getName() + "接收到通知条件满足");
                } finally {
                    System.out.println("finally");
                    lock.unlock();
                }
            }
        }
    ​
        static class signaler implements Runnable {
    ​
            @Override
            public void run() {
                lock.lock();
                try {
                    System.out.println("signal");
                    flag = true;
                    condition.signalAll();
                } finally {
                    lock.unlock();
                }
            }
        }
    ​
    }

    开启了两个线程waiter和signaler,waiter线程开始执行的时候由于条件不满足,执行condition.await方法使该线程进入等待状态同时释放锁,signaler线程获取到锁之后更改条件,并通知所有的等待线程后释放锁。这时,waiter线程获取到锁,并由于signaler线程更改了条件此时相对于waiter来说条件满足,继续执行。

    六、LockSupport

    方法:

    1.void park():阻塞当前线程,如果调用unpark方法或者当前线程被中断,从能从park()方法中返回2.void park(Object blocker):功能同方法1,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;3.void parkNanos(long nanos):阻塞当前线程,最长不超过nanos纳秒,增加了超时返回的特性;4.void parkNanos(Object blocker, long nanos):功能同方法3,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;5.void parkUntil(long deadline):阻塞当前线程,知道deadline;6.void parkUntil(Object blocker, long deadline):功能同方法5,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;

    示例:

    public class LockSupportDemo {
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + "被唤醒");
            });
            thread.start();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.unpark(thread);
        }
    }

    thread线程调用LockSupport.park()致使thread阻塞,当main线程睡眠3秒结束后通过LockSupport.unpark(thread)方法唤醒thread线程,thread线程被唤醒执行后续操作。另外,还有一点值得关注的是,LockSupport.unpark(thread)可以指定线程对象唤醒指定的线程。

     

  • 相关阅读:
    vue 富文本编译器 vue-quill-editor
    vue-拖拽的使用awe-dnd
    Dapper是什么?
    如何运用领域驱动设计
    面试官问我MySQL索引,我
    MySQL:你知道什么是覆盖索引吗?
    mysql覆盖索引与回表
    C#.NET 字符串转数组,数组转字符串
    MYSQL如何让主键使用BTREE索引
    MySQL大表优化方案 Mysql的row_format(fixed与dynamic)
  • 原文地址:https://www.cnblogs.com/mufeng07/p/12890423.html
Copyright © 2020-2023  润新知