目录
- Semaphore
- 使用
- 常见应用
- 原理
- 源码
- 流程
- CountdownLatch
- 使用
- 原理
- CyclicBarrier
- 使用
Semaphore
使用
Semaphore是一种计数信号量,它用于控制对共享资源的访问。它维护了一个许可计数器,表示可用的许可数量。线程在访问共享资源前必须先获得许可,如果许可数量大于0,则线程可以获得许可并继续执行,同时许可数量减少;如果许可数量为0,则线程必须等待,直到有其他线程释放许可。
Semaphore的构造函数有两种形式:
Semaphore(int permits)
: 创建具有给定许可数量的Semaphore对象。Semaphore(int permits, boolean fair)
: 创建具有给定许可数量的Semaphore对象,并指定是否使用公平的排序策略。如果fair为true,则等待时间较长的线程优先获得许可。
Semaphore类的主要方法有:
void acquire()
: 获取一个许可,如果没有可用许可,则线程将阻塞,直到有可用许可为止。void acquire(int permits)
: 获取给定数量的许可,如果没有足够的可用许可,则线程将阻塞,直到有足够数量的许可为止。void release()
: 释放一个许可,将许可数量加1。void release(int permits)
: 释放给定数量的许可,将许可数量加上指定数量。int availablePermits()
: 返回当前可用的许可数量。boolean tryAcquire()
: 尝试获取一个许可,如果有可用许可,则立即返回true,否则返回false。boolean tryAcquire(int permits)
: 尝试获取给定数量的许可,如果有足够的可用许可,则立即返回true,否则返回false。void acquireUninterruptibly()
: 获取一个许可,不响应中断。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的实际应用:
-
连接池管理:在数据库连接池、线程池等资源池管理中,Semaphore可以控制可用资源的数量,限制同时处理的连接或线程数量,避免资源过度使用。
-
限流控制:在网络应用中,可以使用Semaphore实现流量控制,限制请求的处理速率,防止系统过载。
-
并发访问控制:在某些场景下,需要限制同时访问某个资源的线程数量,例如限制文件的并发读写、限制同时下载文件的数量等。
-
多任务协作:在一些多阶段任务中,需要控制各个阶段的并发执行,Semaphore可以用来实现阶段间的同步。
-
实现有界容器:例如使用Semaphore实现有界的阻塞队列,控制队列中元素的数量,防止内存溢出或者资源耗尽。
-
实现读写锁:Semaphore也可以用来实现读写锁的功能,用于读操作和写操作之间的互斥。
-
任务并行处理:在一些并行计算场景中,可以使用Semaphore来控制并行处理的任务数量,控制计算资源的使用。
-
分布式系统协调:在分布式系统中,Semaphore可以用来实现资源的分配和协调,保证分布式任务的有序执行。
这些仅是Semaphore的一部分应用场景,实际上,Semaphore是一种非常灵活和强大的工具,可以用于解决许多并发编程中的问题。在实际使用中,需要根据具体场景和需求合理地配置Semaphore的许可数量,从而达到控制并发的目的。
原理
源码
- 构造方法
构造方法如下:
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参数。
- 获取锁
下面看下获取锁的代码。
首先是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;
}
}
上面的方法前面也已经讲解过,主要就是把后面获取不到锁的节点加入到队列中等待。
- 释放锁
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...
原理
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() { ... }
}
- 构造函数
CountdownLatch
的构造函数接受一个整数参数 count
,表示需要等待的事件数量。这个 count
参数在创建 CountdownLatch
实例时被初始化,并且一旦初始化后,就不能再更改。
await()
方法
await()
方法使调用线程等待,直到计数器变为零。如果计数器当前为零,则此方法立即返回。如果计数器大于零,则线程将被阻塞,直到另一个线程通过调用 countDown()
方法将计数器减少为零。
await(long timeout, TimeUnit unit)
方法
await(long timeout, TimeUnit unit)
方法与 await()
方法类似,但是允许设置一个超时时间。如果在指定的时间内计数器变为零,该方法将返回 true
,否则返回 false
。
countDown()
方法
countDown()
方法将 CountdownLatch
的计数器减少1。如果计数器减少到零,所有在 await()
方法上等待的线程将被释放,并且它们将继续执行。
getCount()
方法
getCount()
方法返回当前计数器的值。
CyclicBarrier
使用
CyclicBarrier
是 Java 并发包 (java.util.concurrent
) 中提供的一种线程同步机制,它允许一组线程相互等待,直到所有线程都到达某个共同点,然后再同时继续执行后续操作。CyclicBarrier
的工作方式类似于一组线程相互等待集合齐全后再同时出发,就像是一个循环屏障。
特点:
- 循环性:与
CountdownLatch
不同,CyclicBarrier
可以被重用,即在一次等待完成后,它的计数器会被重置,可以再次使用。 - 等待点:所有线程都必须在
CyclicBarrier
中声明的等待点上等待,直到最后一个线程到达这个点,所有等待的线程将被释放。 - 计数器:
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
的简单介绍和用法。它在一些多线程协作的场景中非常有用,例如在某个任务需要多个子任务都完成后再进行处理的情况下。