Semaphore和CountDownLatch
一、Semaphore (信号量)
说简单点,Semaphore维护了一个许可集合,在创建Semaphore的时候,设置上许可数,每条线程在只有在获得一个许可的时候才可以继续往下执行逻辑(申请一个许可,则Semaphore的许可池中减少一个许可),没有获得许可的线程会进入阻塞状态。
例如:
public static void main(String[] args) {
//创建一个Semaphore 有5条许可
final Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 10; i++) {
final int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
//申请一个许可 许可池-1 若许可池为0则申请许可失败,阻塞线程
semaphore.acquire();
System.out.print(finalI);
//模仿耗时操作
Thread.sleep(1000);
//释放一个许可 许可池+1
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
然后看log的打印:
12-19 11:06:52.720 13842-13978/lbx.myapplication E/lbxlog: 0
12-19 11:06:52.720 13842-13981/lbx.myapplication E/lbxlog: 3
12-19 11:06:52.720 13842-13979/lbx.myapplication E/lbxlog: 1
12-19 11:06:52.720 13842-13980/lbx.myapplication E/lbxlog: 2
12-19 11:06:52.720 13842-13982/lbx.myapplication E/lbxlog: 4
//这里注意一下 仔细对比时间
12-19 11:06:53.720 13842-13983/lbx.myapplication E/lbxlog: 5
12-19 11:06:53.720 13842-13985/lbx.myapplication E/lbxlog: 7
12-19 11:06:53.720 13842-13987/lbx.myapplication E/lbxlog: 9
12-19 11:06:53.720 13842-13986/lbx.myapplication E/lbxlog: 8
12-19 11:06:53.720 13842-13984/lbx.myapplication E/lbxlog: 6
看了上面的log,发现前五个是52秒左右打印出来的,而后五个是53秒左右打印出来的,因为许可池里只有5个许可,前五条线程把5个许可占用了,后五条线程需要等到有许可后才会继续执行逻辑。
//释放1个许可
semaphore.release();
//使acquire()后正在等待的线程不可被终止
semaphore.acquireUninterruptibly();
//获取当前可用的许可数量,并把可用的许可数置0
semaphore.drainPermits();
//是否有正在等待的线程
semaphore.hasQueuedThreads();
//获取正在等待的线程数量
semaphore.getQueueLength();
//获取当前可用的许可数量
semaphore.availablePermits();
二、信号量的另一种形式:非公平信号量
非公平信号量,先运行的线程不一定可以申请到许可,后运行的线程不一定不可以申请到许可。
/创建5个公平信号量
Semaphore semaphore = new Semaphore(5,true);
//尝试获得许可,并返回获取结果 if(acquireSuc)...else...
boolean acquireSuc = semaphore.tryAcquire();
boolean acquireSuc = semaphore.tryAcquire(2);
//在3秒内尝试获得1个许可,并返回获取结果
boolean acquireSuc = semaphore.tryAcquire(3, TimeUnit.SECONDS);
//在3秒内尝试获得2个许可,并返回获取结果
boolean acquireSuc = semaphore.tryAcquire(2, 3, TimeUnit.SECONDS);
三、CountDownLatch的应用
这个东西和信号量正相反,CountDownLatch的内部维护着一个计数器,同时只有一个线程才可对计数器进行操作,当计数器为0的时候,释放所有阻塞的线程。
例如:
public static void main(String[] args) {
//4条线程
int threadCount = 4;
//创建一个拥有5个计数器的countDownLatch
final CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < threadCount; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//使计数器-1
countDownLatch.countDown();
System.out.println("计数器还剩" + countDownLatch.getCount());
}
}).start();
}
try {
//在这里阻塞 只有计数器为0的时候这行代码才会释放
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行完成啦!");
}
12-19 11:52:25.350 22929-23074/lbx.myapplication I/System.out: 计数器还剩4
12-19 11:52:25.350 22929-23073/lbx.myapplication I/System.out: 计数器还剩3
12-19 11:52:25.350 22929-23075/lbx.myapplication I/System.out: 计数器还剩2
12-19 11:52:25.350 22929-23076/lbx.myapplication I/System.out: 计数器还剩1
因为计数器没有到0,所以“执行完成”这句话没有打印,将线程改成5条:
//5线程
int threadCount = 5;
12-19 11:58:46.570 30347-30471/lbx.myapplication I/System.out: 计数器还剩4
12-19 11:58:46.570 30347-30473/lbx.myapplication I/System.out: 计数器还剩3
12-19 11:58:46.570 30347-30472/lbx.myapplication I/System.out: 计数器还剩2
12-19 11:58:46.570 30347-30474/lbx.myapplication I/System.out: 计数器还剩1
12-19 11:58:46.570 30347-30475/lbx.myapplication I/System.out: 计数器还剩0
12-19 11:58:46.570 30347-30347/lbx.myapplication I/System.out: 执行完成啦!
这时候完整的流程就执行完了。常用方法:
//使线程进入阻塞状态,计数为0的时候释放线程
countDownLatch.await();
//使线程进入阻塞状态,计数为0的时候释放线程或者阻塞到3秒的时候自动释放线程
countDownLatch.await(3, TimeUnit.SECONDS);
//使计数减1
countDownLatch.countDown();
//获取当前的计数
countDownLatch.getCount();
四、区别
- 前者为信号量,信号量只有持有信号的线程才能够执行,其他没有持有信号的就进入阻塞状态,等待持有线程执行完成释放信号。
- 后者为计数器,只有计数器为0的时候才释放所有线程,否则持有计数器的线程都将进入阻塞状态。
五、使用场景
CountDownLatch:
假设计数器的值为2,线程A调用await()
方法之后,A线程就进入了等待状态,之后其它线程中执行countDown()
,计数器就会-1,该操作线程继续执行,当计数器从2编程0,线程A继续执行。
Semaphore
就是信号量,Semaphore
可以阻塞线程并且可以控制同时访问线程的个数,通过acquire()
获取一个许可,如果没有获取到就继续等待,通过release()
释放一个许可。Semaphore
和锁有点类似,都可以控制对某个资源的访问权限。
CountDownLatch
和Semaphore
通常和线程池配合使用。Semaphore适合控制并发数,CountDownLatch比较适合保证线程执行完后再执行其他处理,因此模拟并发时,使用两者结合起来是最好的。
Semaphore可以用来做流量分流,特别是对公共资源有限的场景,比如数据库连接。 假设有这个的需求,读取几万个文件的数据到数据库中,由于文件读取是IO密集型任务,可以启动几十个线程并发读取,但是数据库连接数只有10个,这时就必须控制最多只有10个线程能够拿到数据库连接进行操作。这个时候,就可以使用Semaphore做流量控制。