关于java:面经手册-第18篇AQS-共享锁SemaphoreCountDownLatch听说数据库连接池可以用到

36次阅读

共计 6853 个字符,预计需要花费 18 分钟才能阅读完成。


作者:小傅哥
博客:https://bugstack.cn
Github:https://github.com/fuzhengwei/CodeGuide/wiki

积淀、分享、成长,让本人和别人都能有所播种!????

一、前言

学 Java 怎么能,突飞猛进的成长?

是不是你看见过的突飞猛进都是他人,但本人却很难!

其实并没有一天的突飞猛进,也没有一口吃进去的瘦子。有得更多的时候与日俱增、一直积淀,最初能力暴发、破局!

举个简略的例子,如果你大学毕业时候曾经写了 40 万行代码,还找不到工作吗?但 40 万行均匀到每天并不会很多,重要的是坚持不懈的保持。

二、面试题

谢飞机,小记! 东风吹、战鼓擂,不加班、谁怕谁!哈哈哈,找我大哥去。

谢飞机 :喂,大哥。我女友面试卡住了,强人难,锁我也不会!

面试官:你不应该不会呀,问你一个,基于 AQS 实现的锁都有哪些?

谢飞机:嗯,有 ReentrantLock…

面试官:还有呢?

谢飞机:如同想不起来了,sync 也不是!

面试官:哎,学点漏点,不思考、不总结、不记录。你这样人家面试你就没法聊了,最起码你要有点深度。

谢飞机:嘿嘿,记住了。来我家吃火锅吧,细聊。

三、共享锁 和 AQS

1. 基于 AQS 实现的锁有哪些?

AQS(AbstractQueuedSynchronizer),是 Java 并发包中十分重要的一个类,大部分锁的实现也是基于 AQS 实现的,包含:

  • ReentrantLock,可重入锁。这个是咱们最开始介绍的锁,也是最罕用的锁。通常会与 synchronized 做比拟应用。
  • ReentrantReadWriteLock,读写锁。读锁是共享锁、写锁是独占锁。
  • Semaphore,信号量锁。次要用于管制流量,比方:数据库连接池给你调配 10 个链接,那么让你来一个连一个,连到 10 个还没有人开释,那你就等等。
  • CountDownLatch,闭锁。Latch 门闩的意思,比方:说四个人一个漂流艇,坐满了就推上水。

这一章节咱们次要来介绍 Semaphore,信号量锁的实现,其实也就是介绍一个对于共享锁的应用和源码剖析。

2. Semaphore 共享锁应用

Semaphore semaphore = new Semaphore(2, false); // 构造函数入参,permits:信号量、fair:偏心锁 / 非偏心锁
for (int i = 0; i < 8; i++) {new Thread(() -> {
        try {semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + "蹲坑");
            Thread.sleep(1000L);
        } catch (InterruptedException ignore) { } finally {semaphore.release();
        }
    }, "蹲坑编号:" + i).start();}

这里咱们模仿了一个在高速服务区,厕所排队蹲坑的场景。因为坑位无限,为了防止造成拥挤和踩踏,保安人员在门口拦着,感觉差不多,一次开释两个进去,始终到都开释。你也能够想成早上坐地铁下班,或者淡季去公园,都是一批一批的放行

测试后果

蹲坑编号:0 蹲坑
蹲坑编号:1 蹲坑

蹲坑编号:2 蹲坑
蹲坑编号:3 蹲坑

蹲坑编号:4 蹲坑
蹲坑编号:5 蹲坑

蹲坑编号:6 蹲坑
蹲坑编号:7 蹲坑

Process finished with exit code 0
  • Semaphore 的构造函数能够传递是偏心锁还是非偏心锁,最终的测试后果也不同,能够自行尝试。
  • 测试运行时,会先输入 0 坑、1 坑 之后 2 坑、3 坑…,每次都是这样两个,两个的开释。这就是 Semaphore 信号量锁的作用。

3. Semaphore 源码剖析

3.1 构造函数

public Semaphore(int permits) {sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

permits:n. 许可证,特许证(尤指限期的)

默认状况下只须要传入 permits 许可证数量即可,也就是一次容许放行几个线程。构造函数会创立非偏心锁。如果你须要应用 Semaphore 共享锁中的偏心锁,那么能够传入第二个构造函数的参数 fair = false/true。true:FairSync,偏心锁。在咱们后面的章节曾经介绍了偏心锁相干内容和实现,以及 CLH、MCS《偏心锁介绍》

初始 许可证 数量

FairSync/NonfairSync(int permits) {super(permits);
}

Sync(int permits) {setState(permits);
}

protected final void setState(int newState) {state = newState;}

在构造函数初始化的时候,无论是偏心锁还是非偏心锁,都会设置 AQS 中 state 数量值。这个值也就是为了下文中能够获取的信号量扣减和减少的值。

3.2 acquire 获取信号量

办法 形容
semaphore.acquire() 一次获取一个信号量,响应中断
semaphore.acquire(2) 一次获取 n 个信号量,响应中断(一次占 2 个坑)
semaphore.acquireUninterruptibly() 一次获取一个信号量,不响应中断
semaphore.acquireUninterruptibly(2) 一次获取 n 个信号量,不响应中断
  • 其实获取信号量的这四个办法,次要就是,一次获取几个和是否响应中断的组合。
  • semaphore.acquire(),源码中理论调用的办法是, sync.acquireSharedInterruptibly(1)。也就是相应中断,一次只占一个坑。
  • semaphore.acquire(2),同理这个就是一次要占两个名额,也就是许可证。生存中的场景就是我给我敌人排的对,她来了,进来吧。

3.3 acquire 开释信号量

办法 形容
semaphore.release() 一次开释一个信号量
semaphore.release(2) 一次获取 n 个信号量

有获取就得有开释,获取了几个信号量就要开释几个信号量。当然你能够尝试一下,获取信号量 semaphore.acquire(2) 两个,开释信号量 semaphore.release(1),看看运行成果

3.4 偏心锁实现

信号量获取过程,始终到偏心锁实现。semaphore.acquire -> sync.acquireSharedInterruptibly(permits) -> tryAcquireShared(arg)

semaphore.acquire(1);

public void acquire(int permits) throws InterruptedException {if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

FairSync.tryAcquireShared

protected int tryAcquireShared(int acquires) {for (;;) {if (hasQueuedPredecessors())
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}
  • hasQueuedPredecessors,偏心锁的次要实现逻辑都在于这个办法的应用。它的目标就是判断有线程排在本人后面没,以及把线程增加到队列中的逻辑实现。在后面咱们介绍过 CLH 等实现,能够往前一章节浏览
  • for (;;),是一个自旋的过程,通过 CAS 来设置 state 偏移量对应值。这样就能够防止多线程下竞争获取信号量抵触。
  • getState(),在构造函数中曾经初始化 state 值,在这里获取信号量时就是应用 CAS 一直的扣减。
  • 另外须要留神,共享锁和独占锁在这里是有区别的,独占锁间接返回 true/false,共享锁返回的是 int 值。

    • 如果该值小于 0,则以后线程获取共享锁失败。
    • 如果该值大于 0,则以后线程获取共享锁胜利,并且接下来其余线程尝试获取共享锁的行为很可能胜利。
    • 如果该值等于 0,则以后线程获取共享锁胜利,然而接下来其余线程尝试获取共享锁的行为会失败。

3.5 非偏心锁实现

NonfairSync.nonfairTryAcquireShared

protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {for (;;) {int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}
  • 有了偏心锁的实现,非偏心锁的了解就比较简单了,只是拿去了 if (hasQueuedPredecessors()) 的判断操作。
  • 其余的逻辑实现都和偏心锁统一。

3.6 获取信号量失败,退出同步期待队列

在偏心锁和非偏心锁的实现中,咱们曾经看到失常获取信号量的逻辑。那么如果此时不能失常获取信号量呢?其实这部分线程就须要退出到同步队列。

doAcquireSharedInterruptibly

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {for (;;) {final Node p = node.predecessor();
            if (p == head) {int r = tryAcquireShared(arg);
                if (r >= 0) {setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();}
    } finally {if (failed)
            cancelAcquire(node);
    }
}
  • 首先 doAcquireSharedInterruptibly 办法来自 AQS 的外部办法,与咱们在学习竞争锁时有局部知识点雷同,但也有一些差别。比方:addWaiter(Node.SHARED)tryAcquireShared,咱们次要介绍下这内容。
  • Node.SHARED,其实没有非凡含意,它只是一个标记作用,用于判断是否共享。`final boolean isShared() {
    return nextWaiter == SHARED;

}`

  • tryAcquireShared,次要是来自 Semaphore 共享锁中偏心锁和非偏心锁的实现。用来获取同步状态。
  • setHeadAndPropagate(node, r),如果 r > 0,同步胜利后则将以后线程结点设置为头结点,同时 helpGC,p.next = null,断链操作。
  • shouldParkAfterFailedAcquire(p, node),调整同步队列中 node 结点的状态,并判断是否应该被挂起。这在咱们之前对于锁的文章中曾经介绍。
  • parkAndCheckInterrupt(),判断是否须要被中断,如果中断间接抛出异样,以后结点申请也就完结。
  • cancelAcquire(node),勾销该节点的线程申请。

4. CountDownLatch 共享锁应用

CountDownLatch 也是共享锁的一种类型,之所以在这里体现下,是因为它和 Semaphore 共享锁,既类似有不同。

CountDownLatch 更多体现的组团一波的思维,同样是管制人数,然而须要够一窝。比方:咱们说过的 4 集体一起上皮划艇、两个人一起上跷跷板、 2 集体一起蹲坑我没见过,这样的形式就是门闩 CountDownLatch 锁的思维。

public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(10);
    ExecutorService exec = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 10; i++) {exec.execute(() -> {
            try {int millis = new Random().nextInt(10000);
                System.out.println("期待游客上船,耗时:" + millis + "(millis)");
                Thread.sleep(millis);
            } catch (Exception ignore) { } finally {latch.countDown(); // 完事一个扣减一个名额
            }
        });
    }
    // 期待游客
    latch.await();
    System.out.println("船长浮躁了,开船!");
    // 敞开线程池
    exec.shutdown();}
  • 这一个公园游船的场景案例,期待 10 个乘客上传,他们比拟墨迹。
  • 上一个扣减一个 latch.countDown()
  • 期待游客都上船 latch.await()
  • 最初船长开船!!浮躁了

测试后果

期待游客上船,耗时:6689(millis)
期待游客上船,耗时:2303(millis)
期待游客上船,耗时:8208(millis)
期待游客上船,耗时:435(millis)
期待游客上船,耗时:9489(millis)
期待游客上船,耗时:4937(millis)
期待游客上船,耗时:2771(millis)
期待游客上船,耗时:4823(millis)
期待游客上船,耗时:1989(millis)
期待游客上船,耗时:8506(millis)
船长浮躁了,开船!

Process finished with exit code 0
  • 在你理论的测试中会发现,船长浮躁了,开船!,会须要期待一段时间。
  • 这里体现的就是门闩的思维,组队、一波带走。
  • CountDownLatch 的实现与 Semaphore 基本相同、细节略有差别,就不再做源码剖析了。

四、总结

  • 在有了 AQS、CLH、MCS,等相干锁的常识理解后,在学习其余知识点也绝对容易。根本以上和前几章节对于锁的介绍,也是面试中容易问到的点。可能因为目前分布式开发较多,单机的多线程性能压迫个别较少,然而对这部分常识的理解十分重要
  • 得益于 Lee 老爷子的操刀,并发包锁的设计真的十分优良。每一处的实现都能够说是精益求精,所以在学习的时候能够把小傅哥的文章当作抛砖,之后持续深挖设计精华,不断深入。
  • 共享锁的应用可能平时并不多,但如果你须要设计一款相似数据库线程池的设计,那么这样的信号量锁的思维就十分重要了。所以在学习的时候也须要有技术迁徙的能,一直把这些常识复用到理论的业务开发中。

五、系列举荐

  • volatile 怎么实现的内存可见?没有 volatile 肯定不可见吗?
  • synchronized 解毒,分析源码深度剖析!
  • ReentrantLock 之偏心锁解说和实现
  • ReentrantLock 之 AQS 原理剖析和实际应用
  • 想去 BAT 大厂,应该怎么面?

正文完
 0