java并发编程 13:JUC之Semaphore、CountdownLatch、 CyclicBarrier

Semaphore

使用

Semaphore是一种计数信号量,它用于控制对共享资源的访问。它维护了一个许可计数器,表示可用的许可数量。线程在访问共享资源前必须先获得许可,如果许可数量大于0,则线程可以获得许可并继续执行,同时许可数量减少;如果许可数量为0,则线程必须等待,直到有其他线程释放许可。

Semaphore的构造函数有两种形式:

  1. Semaphore(int permits): 创建具有给定许可数量的Semaphore对象。
  2. Semaphore(int permits, boolean fair): 创建具有给定许可数量的Semaphore对象,并指定是否使用公平的排序策略。如果fair为true,则等待时间较长的线程优先获得许可。

Semaphore类的主要方法有:

  1. void acquire(): 获取一个许可,如果没有可用许可,则线程将阻塞,直到有可用许可为止。
  2. void acquire(int permits): 获取给定数量的许可,如果没有足够的可用许可,则线程将阻塞,直到有足够数量的许可为止。
  3. void release(): 释放一个许可,将许可数量加1。
  4. void release(int permits): 释放给定数量的许可,将许可数量加上指定数量。
  5. int availablePermits(): 返回当前可用的许可数量。
  6. boolean tryAcquire(): 尝试获取一个许可,如果有可用许可,则立即返回true,否则返回false。
  7. boolean tryAcquire(int permits): 尝试获取给定数量的许可,如果有足够的可用许可,则立即返回true,否则返回false。
  8. void acquireUninterruptibly(): 获取一个许可,不响应中断。
  9. void acquireUninterruptibly(int permits): 获取给定数量的许可,不响应中断。

示例:

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        // 创建一个Semaphore对象,初始许可数量为3
        Semaphore semaphore = new Semaphore(3);

        // 创建10个线程并启动
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Worker(semaphore, i));
            thread.start();
        }
    }

    static class Worker implements Runnable {
        private final Semaphore semaphore;
        private final int id;

        public Worker(Semaphore semaphore, int id) {
            this.semaphore = semaphore;
            this.id = id;
        }

        @Override
        public void run() {
            try {
                System.out.println("Worker " + id + " is waiting for a permit.");
                semaphore.acquire();
                System.out.println("Worker " + id + " got a permit and is doing some work.");
                Thread.sleep(2000);
                System.out.println("Worker " + id + " released the permit and finished.");
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在这个示例中,我们创建了一个Semaphore对象,并初始化许可数量为3。然后,我们创建了10个Worker线程,每个线程在执行前会先尝试获取一个许可,如果没有可用许可,它将会被阻塞直到有可用许可为止。每个Worker线程执行一段模拟的工作,然后释放许可,以便其他线程可以继续执行。

请注意,Semaphore适用于控制对有限资源的并发访问。通过适当地设置许可数量,您可以控制同时访问共享资源的线程数量,从而避免资源过度竞争和冲突。

常见应用

Semaphore在实际应用中有很多用途,它是一种非常有用的并发控制工具。下面列举一些Semaphore的实际应用:

  1. 连接池管理:在数据库连接池、线程池等资源池管理中,Semaphore可以控制可用资源的数量,限制同时处理的连接或线程数量,避免资源过度使用。

  2. 限流控制:在网络应用中,可以使用Semaphore实现流量控制,限制请求的处理速率,防止系统过载。

  3. 并发访问控制:在某些场景下,需要限制同时访问某个资源的线程数量,例如限制文件的并发读写、限制同时下载文件的数量等。

  4. 多任务协作:在一些多阶段任务中,需要控制各个阶段的并发执行,Semaphore可以用来实现阶段间的同步。

  5. 实现有界容器:例如使用Semaphore实现有界的阻塞队列,控制队列中元素的数量,防止内存溢出或者资源耗尽。

  6. 实现读写锁:Semaphore也可以用来实现读写锁的功能,用于读操作和写操作之间的互斥。

  7. 任务并行处理:在一些并行计算场景中,可以使用Semaphore来控制并行处理的任务数量,控制计算资源的使用。

  8. 分布式系统协调:在分布式系统中,Semaphore可以用来实现资源的分配和协调,保证分布式任务的有序执行。

这些仅是Semaphore的一部分应用场景,实际上,Semaphore是一种非常灵活和强大的工具,可以用于解决许多并发编程中的问题。在实际使用中,需要根据具体场景和需求合理地配置Semaphore的许可数量,从而达到控制并发的目的。

原理

源码

  1. 构造方法

构造方法如下:

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

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

构造方法里有个必须的参数permits,用来表示信号量的个数,第二可选参数表示公平还是非公平。

构造方法里会初始化一个同步器sync

先看下默认的非公平的NonfairSync,代码如下:

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

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

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

然后看下继承的内部的Sync类:

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 1192457210091910933L;

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

    final int getPermits() {
        return getState();
    }

    final int nonfairTryAcquireShared(int acquires) {
        for (;;) {
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }

    protected final boolean tryReleaseShared(int releases) {
        for (;;) {
            int current = getState();
            int next = current + releases;
            if (next < current) // overflow
                throw new Error("Maximum permit count exceeded");
            if (compareAndSetState(current, next))
                return true;
        }
    }

    final void reducePermits(int reductions) {
        for (;;) {
            int current = getState();
            int next = current - reductions;
            if (next > current) // underflow
                throw new Error("Permit count underflow");
            if (compareAndSetState(current, next))
                return;
        }
    }

    final int drainPermits() {
        for (;;) {
            int current = getState();
            if (current == 0 || compareAndSetState(current, 0))
                return current;
        }
    }
}

可以看到:参数permits又在调用AQS的setState方法时,作为方法参数,也就是传的permits参数称为了我们前面说的AQS的state参数。

  1. 获取锁

下面看下获取锁的代码。

首先是acquire方法:

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

然后看下acquireSharedInterruptibly:

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
  			// 如果尝试获取许可证后,剩余数量小于0,则执行下面的方法,否则就是获取成功了
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

然后调用tryAcquireShared方法,找到对应的实现类:

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

nonfairTryAcquireShared代码如下:

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
      	// 拿到permits,即这个信号量可用的许可证数量
        int available = getState();
      	// 可用的减去获取的,即剩余的许可证
        int remaining = available - acquires;
      	// 如果剩余的小于0。或者大于0且使用CAS修改available成功,则返回剩余的数量
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

nonfairTryAcquireShared方法返回值在acquireSharedInterruptibly方法里,做了一个判断如果小于0,还要执行doAcquireSharedInterruptibly方法:

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    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
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

上面的方法前面也已经讲解过,主要就是把后面获取不到锁的节点加入到队列中等待。

  1. 释放锁
public void release() {
    sync.releaseShared(1);
}

看下releaseShared方法:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

然后是tryReleaseShared方法,找到对应实现方法:

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
      	// 拿到当前的状态值
        int current = getState();
      	// 当前的 + 要释放的,即新的释放锁后的状态值
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
      	// CAS修改状态值,成功返回true
        if (compareAndSetState(current, next))
            return true;
    }
}

上面方法如果返回ture,在releaseShared方法里还要执行doReleaseShared方法:

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

这个方法前面也讲过类似,主要是唤醒等待队列中的节点。

流程

Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后停车场显示空余车位减一。

刚开始,permits(state)为 3,这时 5 个线程来获取资源

在这里插入图片描述

假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞

在这里插入图片描述

这时 Thread-4 释放了 permits,状态如下

在这里插入图片描述

接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
在这里插入图片描述

CountdownLatch

使用

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句。

CountDownLatch 主要有两个方法:

  • await:当一个或多个线程调用 await 方法时,这些线程会阻塞;
  • countDown:其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞),当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行。

示例:

package up.cys.chapter13;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;

@Slf4j
public class CountdownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        new Thread(() -> {
            log.info("begin...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            latch.countDown();
            log.info("end...{}", latch.getCount());
        }).start();
        new Thread(() -> {
            log.info("begin...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            latch.countDown();
            log.info("end...{}", latch.getCount());
        }).start();
        new Thread(() -> {
            log.info("begin...");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            latch.countDown();
            log.info("end...{}", latch.getCount());
        }).start();
        log.info("waiting...");
        latch.await();
        log.info("wait end...");
    }
}

运行结果如下:

2023-07-26 21:24:12,866 - 0    INFO  [Thread-2] up.cys.chapter13.CountdownLatchTest:37  - begin...
2023-07-26 21:24:12,866 - 0    INFO  [Thread-1] up.cys.chapter13.CountdownLatchTest:27  - begin...
2023-07-26 21:24:12,866 - 0    INFO  [main] up.cys.chapter13.CountdownLatchTest:46  - waiting...
2023-07-26 21:24:12,866 - 0    INFO  [Thread-0] up.cys.chapter13.CountdownLatchTest:17  - begin...
2023-07-26 21:24:13,884 - 1018 INFO  [Thread-0] up.cys.chapter13.CountdownLatchTest:24  - end...2
2023-07-26 21:24:14,375 - 1509 INFO  [Thread-2] up.cys.chapter13.CountdownLatchTest:44  - end...1
2023-07-26 21:24:14,880 - 2014 INFO  [Thread-1] up.cys.chapter13.CountdownLatchTest:34  - end...0
2023-07-26 21:24:14,880 - 2014 INFO  [main] up.cys.chapter13.CountdownLatchTest:48  - wait end...

可使用线程池来改进,如下:

package up.cys.chapter13;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class CountdownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        ExecutorService service = Executors.newFixedThreadPool(4);
        service.submit(() -> {
            log.info("begin...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            latch.countDown();
            log.info("end...{}", latch.getCount());
        });
        service.submit(() -> {
            log.info("begin...");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            latch.countDown();
            log.info("end...{}", latch.getCount());
        });
        service.submit(() -> {
            log.info("begin...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            latch.countDown();
            log.info("end...{}", latch.getCount());
        });
        service.submit(()->{
            try {
                log.info("waiting...");
                latch.await();
                log.info("wait end...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

运行结果如下:

2023-07-26 21:29:47,937 - 0    INFO  [pool-1-thread-1] up.cys.chapter13.CountdownLatchTest:20  - begin...
2023-07-26 21:29:47,939 - 2    INFO  [pool-1-thread-4] up.cys.chapter13.CountdownLatchTest:51  - waiting...
2023-07-26 21:29:47,938 - 1    INFO  [pool-1-thread-3] up.cys.chapter13.CountdownLatchTest:40  - begin...
2023-07-26 21:29:47,937 - 0    INFO  [pool-1-thread-2] up.cys.chapter13.CountdownLatchTest:30  - begin...
2023-07-26 21:29:48,959 - 1022 INFO  [pool-1-thread-1] up.cys.chapter13.CountdownLatchTest:27  - end...2
2023-07-26 21:29:49,456 - 1519 INFO  [pool-1-thread-2] up.cys.chapter13.CountdownLatchTest:37  - end...1
2023-07-26 21:29:49,956 - 2019 INFO  [pool-1-thread-3] up.cys.chapter13.CountdownLatchTest:47  - end...0
2023-07-26 21:29:49,956 - 2019 INFO  [pool-1-thread-4] up.cys.chapter13.CountdownLatchTest:53  - wait end...

原理

  1. CountdownLatch 类声明

CountdownLatch 是一个具有以下构造函数和方法的类:

public class CountdownLatch {
    public CountdownLatch(int count) { ... }

    public void await() throws InterruptedException { ... }

    public boolean await(long timeout, TimeUnit unit) throws InterruptedException { ... }

    public void countDown() { ... }

    public long getCount() { ... }
}
  1. 构造函数

CountdownLatch 的构造函数接受一个整数参数 count,表示需要等待的事件数量。这个 count 参数在创建 CountdownLatch 实例时被初始化,并且一旦初始化后,就不能再更改。

  1. await() 方法

await() 方法使调用线程等待,直到计数器变为零。如果计数器当前为零,则此方法立即返回。如果计数器大于零,则线程将被阻塞,直到另一个线程通过调用 countDown() 方法将计数器减少为零。

  1. await(long timeout, TimeUnit unit) 方法

await(long timeout, TimeUnit unit) 方法与 await() 方法类似,但是允许设置一个超时时间。如果在指定的时间内计数器变为零,该方法将返回 true,否则返回 false

  1. countDown() 方法

countDown() 方法将 CountdownLatch 的计数器减少1。如果计数器减少到零,所有在 await() 方法上等待的线程将被释放,并且它们将继续执行。

  1. getCount() 方法

getCount() 方法返回当前计数器的值。

CyclicBarrier

使用

CyclicBarrier 是 Java 并发包 (java.util.concurrent) 中提供的一种线程同步机制,它允许一组线程相互等待,直到所有线程都到达某个共同点,然后再同时继续执行后续操作。CyclicBarrier 的工作方式类似于一组线程相互等待集合齐全后再同时出发,就像是一个循环屏障。

特点:

  1. 循环性:与 CountdownLatch 不同,CyclicBarrier 可以被重用,即在一次等待完成后,它的计数器会被重置,可以再次使用。
  2. 等待点:所有线程都必须在 CyclicBarrier 中声明的等待点上等待,直到最后一个线程到达这个点,所有等待的线程将被释放。
  3. 计数器CyclicBarrier 内部维护一个计数器,用于跟踪还有多少个线程未到达等待点。

CyclicBarrier 的构造函数如下:

public CyclicBarrier(int parties, Runnable barrierAction)
  • parties 表示等待的线程数,即需要同步的线程数量。每个线程调用 await() 方法时,都会将计数器减1,直到计数器减至0时,所有等待的线程将被释放,继续执行后续操作。
  • barrierAction 是一个可选的 Runnable 参数,在所有线程释放后,将在最后一个线程到达等待点时执行。

使用示例:

下面是一个简单的例子,说明如何使用 CyclicBarrier

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class Example {
    public static void main(String[] args) {
        int numberOfThreads = 3;

        // 定义一个Runnable,用于在所有线程释放后执行的操作
        Runnable barrierAction = () -> System.out.println("All threads have reached the barrier.");

        CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, barrierAction);

        for (int i = 0; i < numberOfThreads; i++) {
            Thread thread = new Thread(() -> {
                try {
                    // 模拟一些任务执行
                    System.out.println("Task executed by thread: " + Thread.currentThread().getName());
                    barrier.await(); // 等待其他线程
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
        }
    }
}

在上面的示例中,我们创建了3个线程,每个线程都模拟执行一个任务,并在完成后调用 await() 方法等待其他线程。当所有线程都到达等待点时,barrierAction 中的操作会被执行,输出 “All threads have reached the barrier.”。

这就是 CyclicBarrier 的简单介绍和用法。它在一些多线程协作的场景中非常有用,例如在某个任务需要多个子任务都完成后再进行处理的情况下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ethan-running

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值