JAVA中常见并发类简单使用

116 阅读3分钟

前言

最近有一点空,准备研究一下JDK自带的JUC包中的常见几个类,编写CRUD代码时,基本使用过CountDownLatch这个类,其他相关的类都没太使用或者很少使用。

常见的有以下三种:

flowchart LR
    A[JUC]
    A --> B[CountDownLatch]
    A --> C[CyclicBarrier]
    A --> D[Semaphore]

测试

分别编写一些测试类来探究这些类的使用场景和范围。了解其使用原理和底层的逻辑使用方式。

CountDownLatch

又称为门阀,设置一个基础数值,每次操作减一,当减少到0时,执行主线程。

主线程业务线程循环
阻塞不阻塞不支持
@Test
public void execute() throws InterruptedException {

    for (int i = 0; i < 10; i++) {
        int x = i;
        new Thread(() -> {
            log.info("execute task order is => [{}]", x);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            cdl.countDown();
        }).start();
    }

    cdl.await();

    log.info("execute core method");

}

执行后结果

image.png

如我们所想,执行完10个任务后,再执行我们阻塞的主线程,不支持循环使用,每次都要重复创建门阀对象,进行批量任务的计数。

Semaphore

即信号量,内部维护了一个池子,线程通过向池子中获取信号量,如果获取到了则执行,没有获取到阻塞当前线程。获取到信号量的线程使用完后,将信号量归还到池子中,最多同时允许池子容量大小的线程在执行。其他的需要进行等待。

@Test
public void execute() throws InterruptedException {

    for (int i = 0; i < 1000; i++) {
        int x = i;
        new Thread(() -> {
            try {
                semaphore.acquire();
                log.info("execute task , order is [{}]", x);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                semaphore.release();
            }
        }).start();
    }

    Thread.sleep(Integer.MAX_VALUE);

}

结果:

image.png

按照信号量池子的大小来进行任务的分批执行,可以用来实现一个小型的资源线程池。

CyclicBarrier

循环屏障,同步在初始化时也会初始化一个基础数值。当数值为0时,执行一个异步线程操作。同步会恢复循环屏障到初始化状态,重新进行任务处理。处理循环场景时是优于门阀。

初始化

CyclicBarrier cb = new CyclicBarrier(10, () -> {
    log.info("execute core method");
});

初始化可以传一个Runnable对象进去。当基础数值为0时,会执行对应的run方法。

image.png 执行10个线程后,执行对应的异步线程。

不同

CountDownLatch和Semaphore底层是使用AQS同步器进行处理,CyclicBarrier向上抽象了一层,使用的可重入锁(ReentrantLock)进行加锁处理。

flowchart LR
    A[[AbstractQueuedSynchronizer]]

CountDownLatch

countDown方法
  • 调用countDown方法
public void countDown() {
    sync.releaseShared(1); // 释放同步数量1
}
  • 进行state数值的扣减
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {  // 如果有其他线程操作了state,循环会继续执行,直到得到我们想要的结果
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;  // 减1
        if (compareAndSetState(c, nextc))
            return nextc == 0; // 返回基础数值是否减到了0
    }
}
  • 如果state==0时,需要进行唤醒线程操作
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h); // 唤醒线程,直到没有线程在await状态即可
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
await方法

实际调用的是下面这个方法

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted()) // 响应中断
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0) // 尝试获取共享锁状态,初始时返回的是-1
        doAcquireSharedInterruptibly(arg); // 阻塞线程
}

这里还有一个知识点,采用的是共享模式,ReetrantLock采用的是非共享模式,内部维护一个队列来存放需要获取锁的线程Node对象。

Semaphore

acquire方法

调用获取共享锁值,信号量一次性是可以获取>1的值。

image.png

release方法
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0; // 判断是否池中的数量为0,为0则阻塞正在获取的线程
    }
}

所有的核心都是获取state,操作加锁,释放锁。重入锁也是同理,如果是当前线程,state可以重入+1,释放的时候同理也是需要释放多次。锁的竞争分为公平和非公平,主要是判断是否是直接让多个活跃线程去竞争还是从队列中唤醒头部的线程执行。

未完待续