Semaphore 是并发工具类,我们在一些需要并发的地方会用到它。之前也有文章提到它,当时么有探讨一些细节,给出两个例子: https://blog.csdn.net/Deaht_Huimie/article/details/105628723 。 这篇文章直接从后半部分开始看就行了。今天讲些细节,算是个复盘。
Semaphore semp = new Semaphore(1); 这行代码中,Semaphore 的许可数字是1,意思是在获得许可(acquire())后,同一时间只允许一个可用,其他的就需要等待,直到第一个释放 (release())才会执行。 假如说有三条子线程同时触发,如果使用了 semp, 那么第一条执行完了才会执行第二条,第二条结束了才会执行第三条,举个栗子
private static void test102() {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semp = new Semaphore(1);
System.out.println("start: " + getNowDate());
// 模拟10个客户端访问
for (int index = 0; index < 10; index++) {
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
// 获取许可
semp.acquire();
System.out.println("Accessing: " + NO + " " + getNowDate());
//模拟实际业务逻辑
Thread.sleep((long) (2000));
// 访问完后,释放
semp.release();
} catch (InterruptedException e) {
}
}
};
exec.execute(run);
}
// 退出线程池
exec.shutdown();
}
public static String getNowDate() {
Date currentTime = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
String dateString = formatter.format(currentTime);
return dateString;
}
打印日志,如下
start: 2020-11-27 19:59:50:497
Accessing: 0 2020-11-27 19:59:50:537
Accessing: 4 2020-11-27 19:59:52:538
Accessing: 8 2020-11-27 19:59:54:538
Accessing: 3 2020-11-27 19:59:56:538
Accessing: 7 2020-11-27 19:59:58:538
Accessing: 2 2020-11-27 20:00:00:538
Accessing: 6 2020-11-27 20:00:02:539
Accessing: 1 2020-11-27 20:00:04:539
Accessing: 5 2020-11-27 20:00:06:539
Accessing: 9 2020-11-27 20:00:08:540
我们许可数字设置的是1,在开启10个子线程时,能同时执行的也是一条,可以从 start 时间和打印的第一条时间做个对比,相差无几;for循环中打印的时间,相隔基本都是2秒。我们可以判断出,首条子线程执行时,semp.acquire() 做了个许可拦截,自己继续往下执行,其他九条子线程也来了,但是由于只有一个许可,其他9条只能一起等候了,2秒后,首条执行完了,触发 semp.release() ,此时就是说它自己已经执行完了,剩下的你们赶紧,接着索引为4的抢到了,继续这个过程。打个比方,0-9 这10个小朋友一起进了一个屋子,屋子里只有一个板凳,一个坐板凳的许可证,要求他们每个人都要坐一次板凳,每次时间为2分钟,每次只能坐一个人,小朋友必须拿到许可证,然后才能坐板凳,2分钟后离开板凳,同时把许可证重新放回原处,供其他小朋友来抢许可证,获取许可证的小朋友继续坐板凳,两分钟后起立,交回许可证,供其他剩下的小朋友抢许可证...... 这样解释是不是好理解一点。 acquire() 和 release() 是成对出现的,相当于一个获取许可证,一个交还许可证。
上面的例子中,acquire() 和 release() 都是在同一个线程中,如果在不同线程中呢?把 acquire() 放到主线程中,修改如下
private static void test103() {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semp = new Semaphore(1);
System.out.println("start: " + getNowDate());
// 模拟10个客户端访问
for (int index = 0; index < 10; index++) {
try {
semp.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
System.out.println("Accessing: " + NO + " " + getNowDate());
//模拟实际业务逻辑
Thread.sleep((long) (2000));
semp.release();
} catch (InterruptedException e) {
}
}
};
exec.execute(run);
}
// 退出线程池
exec.shutdown();
}
打印日志
start: 2020-11-27 20:25:20:916
Accessing: 0 2020-11-27 20:25:20:967
Accessing: 1 2020-11-27 20:25:22:967
Accessing: 2 2020-11-27 20:25:24:968
Accessing: 3 2020-11-27 20:25:26:968
Accessing: 4 2020-11-27 20:25:28:968
Accessing: 5 2020-11-27 20:25:30:969
Accessing: 6 2020-11-27 20:25:32:969
Accessing: 7 2020-11-27 20:25:34:969
Accessing: 8 2020-11-27 20:25:36:969
Accessing: 9 2020-11-27 20:25:38:969
说明无影响。
现在说说 tryAcquire() 方法,它是 acquire() 和 release() 的集合体,有定时释放的功能,比如 tryAcquire(1500, TimeUnit.MILLISECONDS), 它意思是先执行 acquire() ,1.5秒后,执行 release() ,继续用上面的例子
private static void test103() {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semp = new Semaphore(1);
System.out.println("start: " + getNowDate());
// 模拟10个客户端访问
for (int index = 0; index < 10; index++) {
try {
semp.tryAcquire(1500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
System.out.println("Accessing: " + NO + " " + getNowDate());
//模拟实际业务逻辑
Thread.sleep((long) (2000));
} catch (InterruptedException e) {
}
}
};
exec.execute(run);
}
// 退出线程池
exec.shutdown();
}
打印日志
start: 2020-11-27 20:33:13:399
Accessing: 0 2020-11-27 20:33:13:423
Accessing: 1 2020-11-27 20:33:14:924
Accessing: 2 2020-11-27 20:33:16:425
Accessing: 3 2020-11-27 20:33:17:926
Accessing: 4 2020-11-27 20:33:19:426
Accessing: 5 2020-11-27 20:33:20:926
Accessing: 6 2020-11-27 20:33:22:428
Accessing: 7 2020-11-27 20:33:23:929
Accessing: 8 2020-11-27 20:33:25:430
Accessing: 9 2020-11-27 20:33:26:931
这里要注意, tryAcquire() 方法只能拦截同一个线程中的,就像上面,10个子线程是在同一个主线程执行,我们对主线程进行了许可和释放许可。如果我们把 tryAcquire() 移到 Runnable 中,诸位可以自己打印下日志看看。
tryAcquire() 是定时释放的,加入说里面设置的时间是2秒,但是在1秒就执行完了自己的逻辑,想释放许可让下一条线程进来怎么办?好说,执行 release() 方法即可。
private static void test103() {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semp = new Semaphore(1);
System.out.println("start: " + getNowDate());
// 模拟10个客户端访问
for (int index = 0; index < 10; index++) {
try {
semp.tryAcquire(2000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
System.out.println("Accessing: " + NO + " " + getNowDate());
//模拟实际业务逻辑
Thread.sleep((long) (1000));
semp.release();
} catch (InterruptedException e) {
}
}
};
exec.execute(run);
}
// 退出线程池
exec.shutdown();
}
打印日志
start: 2020-11-27 20:45:08:792
Accessing: 0 2020-11-27 20:45:08:849
Accessing: 1 2020-11-27 20:45:09:849
Accessing: 2 2020-11-27 20:45:10:849
Accessing: 3 2020-11-27 20:45:11:850
Accessing: 4 2020-11-27 20:45:12:850
Accessing: 5 2020-11-27 20:45:13:850
Accessing: 6 2020-11-27 20:45:14:850
Accessing: 7 2020-11-27 20:45:15:851
Accessing: 8 2020-11-27 20:45:16:851
Accessing: 9 2020-11-27 20:45:17:851
看到这里,再看看文章开头链接对应的文章后半部分,此时再看估计就好理解多了。