Semaphore的作用:
在Java中对于并发访问资源有多种控制方式,例如synchronized和可重入锁,CountDownLatch 、CyclicBarrier 等,这些工具的共同特征是某个时刻只允许一个线程访问某个共享资源,但是还是有很多情况下是多个线程访问多个共享资源的,例如数量有限的停车位、旅游区、商场里厕所的蹲坑,都是多个客户访问有限的共享资源的情况。这些情况下,Java提供了另外的并发访问控制--多线程访问多个资源的并发访问控制。
Semaphore实现原理:
Semaphore内部其实有个计数器,表示目前可访问的资源数量,当某个线程需要访问这些可用资源之一时先获得信号量,如果信号量的计数器值>1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源
如果计数器值=0,线程进入休眠。当某个线程使用完共享资源后,要释放信号量,并将信号量内部的可用资源计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
打个比方,比如某个停车场的停车位总数是100个,有1个入口和1个出口,入口有管理员老大爷来记录当前进入了多少车和出去了多少车,以便计算出目前的空余车位,在最初时进入了100辆车,当没有车出来时,后续来的车只能排队等着车位,如果有车从出口出来,说明有空余车位,排队的车就可以进入。
在Java的世界里多个车进入停车场后,相当N辆车需要N个车位,停车位是共享资源,为避免多个车来同时竞争同一个停车位,在内部仍然使用锁来控制资源的同步访问。
Semaphore的使用:
Semaphore使用时需要先构建一个参数来指定共享资源的数量,Semaphore构造完成后即是获取Semaphore、共享资源使用完毕后释放Semaphore。
其实商场里的厕所也是同样的情形,假设某个商场里男士厕所有10个坑位,此时有20个顾客需要蹲坑,每个人蹲坑的时间都不相同,有人速度快,有人速度慢。
定义厕所类:
public class Tolit {
private static final Logger log = LoggerFactory.getLogger(Tolit.class);
private final Semaphore semaphore ;
private boolean resourceArray[];
private final ReentrantLock lock;
// 共享资源总数
private final int RESOURCE_COUNT=10;
public Tolit(){
//存放厕所状态
this.resourceArray = new boolean[RESOURCE_COUNT];
//控制10个共享资源的使用,使用先进先出的公平模式进行共享;公平模式的信号量,先来的先获得信号量
this.semaphore = new Semaphore(RESOURCE_COUNT,true);
//公平模式的锁,先来的先选
this.lock = new ReentrantLock(true);
for(int i=0 ;i<RESOURCE_COUNT; i++){
//初始化为资源可用的情况
resourceArray[i] = true;
}
}
public void useResource(int userID){
try{
semaphore.acquire();
int id = getResourceId();//占到一个坑
// 创建一个随机的时间,因为每个人蹲坑时间不确定,用随机时间比更为合适
Random random = new Random();
int sleepTime = random.nextInt(10*1000-1000+1)+1000;
log.info("USER_ID="+userID +"\t 开始蹲坑,位置="+id);
Thread.sleep(sleepTime);
log.info("USER_ID="+userID +"\t "+" 结束蹲坑,用时 "+sleepTime/1000+" 秒");
resourceArray[id] = true;//退出这个坑
}catch(InterruptedException e){
e.printStackTrace();
}finally{
semaphore.release();
}
}
// 返回用户可以使用的坑位
public int getResourceId(){
int id = -1;
lock.lock();
try{
//lock.lock();
// 虽然使用了锁控制同步,但由于只是简单的一个数组遍历,
// 效率还是很高的,所以基本不影响性能。
for(int i=0; i<RESOURCE_COUNT; i++){
if(resourceArray[i]){
resourceArray[i] = false;
id = i;
break;
}
}
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
return id;
}
}
定义用户类
public class User implements Runnable {
private Tolit tolit;
private int userId;
public User(Tolit tolit, int userId) {
this.tolit = tolit;
this.userId = userId;
}
public void run(){
tolit.useResource(userId);
}
}
定义测试类:
public class Test {
// 用户数
private static final int USERS=20;
public static void main(String[] argc){
Tolit tolit = new Tolit();
//定义线程数组,创建多个资源使用者
Thread[] threads = new Thread[USERS];
for (int i = 1; i < USERS; i++) {
Thread thread = new Thread(new User(tolit,i));
threads[i] = thread;
}
for(int i = 1; i < USERS; i++){
Thread thread = threads[i];
try {
thread.start();//启动线程
}catch (Exception e){
e.printStackTrace();
}
}
}
}
运行测试:
17:25:11.483 [Thread-1] USER_ID=2 开始蹲坑,位置=0
17:25:11.484 [Thread-14] USER_ID=15 开始蹲坑,位置=4
17:25:11.495 [Thread-10] USER_ID=11 开始蹲坑,位置=3
17:25:11.496 [Thread-3] USER_ID=4 开始蹲坑,位置=5
17:25:11.496 [Thread-2] USER_ID=3 开始蹲坑,位置=1
17:25:11.496 [Thread-6] USER_ID=7 开始蹲坑,位置=2
17:25:11.497 [Thread-5] USER_ID=6 开始蹲坑,位置=6
17:25:11.498 [Thread-0] USER_ID=1 开始蹲坑,位置=7
17:25:11.498 [Thread-4] USER_ID=5 开始蹲坑,位置=8
17:25:11.498 [Thread-7] USER_ID=8 开始蹲坑,位置=9
17:25:12.621 [Thread-2] USER_ID=3 结束蹲坑,用时 1 秒
17:25:12.621 [Thread-9] USER_ID=10 开始蹲坑,位置=1
17:25:12.741 [Thread-3] USER_ID=4 结束蹲坑,用时 1 秒
17:25:12.741 [Thread-11] USER_ID=12 开始蹲坑,位置=5
17:25:14.744 [Thread-5] USER_ID=6 结束蹲坑,用时 3 秒
17:25:14.744 [Thread-13] USER_ID=14 开始蹲坑,位置=6
17:25:15.080 [Thread-10] USER_ID=11 结束蹲坑,用时 3 秒
17:25:15.080 [Thread-15] USER_ID=16 开始蹲坑,位置=3
17:25:15.322 [Thread-6] USER_ID=7 结束蹲坑,用时 3 秒
17:25:15.322 [Thread-17] USER_ID=18 开始蹲坑,位置=2
17:25:15.459 [Thread-1] USER_ID=2 结束蹲坑,用时 3 秒
17:25:15.459 [Thread-8] USER_ID=9 开始蹲坑,位置=0
17:25:15.813 [Thread-11] USER_ID=12 结束蹲坑,用时 3 秒
17:25:15.813 [Thread-12] USER_ID=13 开始蹲坑,位置=5
17:25:17.358 [Thread-12] USER_ID=13 结束蹲坑,用时 1 秒
17:25:17.358 [Thread-16] USER_ID=17 开始蹲坑,位置=5
17:25:17.857 [Thread-9] USER_ID=10 结束蹲坑,用时 5 秒
17:25:17.858 [Thread-18] USER_ID=19 开始蹲坑,位置=1
17:25:18.633 [Thread-14] USER_ID=15 结束蹲坑,用时 7 秒
17:25:18.845 [Thread-4] USER_ID=5 结束蹲坑,用时 7 秒
17:25:19.038 [Thread-0] USER_ID=1 结束蹲坑,用时 7 秒
17:25:19.126 [Thread-16] USER_ID=17 结束蹲坑,用时 1 秒
17:25:19.921 [Thread-7] USER_ID=8 结束蹲坑,用时 8 秒
17:25:22.075 [Thread-13] USER_ID=14 结束蹲坑,用时 7 秒
17:25:23.383 [Thread-18] USER_ID=19 结束蹲坑,用时 5 秒
17:25:23.702 [Thread-15] USER_ID=16 结束蹲坑,用时 8 秒
17:25:25.106 [Thread-17] USER_ID=18 结束蹲坑,用时 9 秒
17:25:25.322 [Thread-8] USER_ID=9 结束蹲坑,用时 9 秒
分析运行结果:
在这个案例中,模拟某个商场男士厕所有10个坑位,此时有20个顾客需要蹲坑,每个人蹲坑的时间都不相同,有人速度快有人速度慢。
比如前10行,10个用户占用了0-9共10个坑位后开始蹲坑,我们根据时间顺序来看,比如看USER_ID=3的使用坑位的情况
17:25:11.496 [Thread-2] USER_ID=3 开始蹲坑,位置=1
17:25:12.621 [Thread-2] USER_ID=3 结束蹲坑,用时 1 秒
17:25:12.621 [Thread-9] USER_ID=10 开始蹲坑,位置=1
17:25:17.857 [Thread-9] USER_ID=10 结束蹲坑,用时 5 秒
USER_ID=3使用了1号坑位,1秒钟解决了问题,结束蹲坑释放资源,USER_ID=10抢占到了这个坑位,使用了5秒钟之后结束蹲坑释放资源。
其他的用户访问其他的共享资源也是同样的情况,确实可以看到这些共享被使用并且释放资源之后,其他的用户重复使用了被释放的资源,同时还确保了资源被有效满负荷的使用。
在这个案例中使用Semaphore完美的解决了多个线程访问多个共享资源的情况,也可以通过使用二进制信号量来实现类似synchronized关键字和Lock锁的并发访问控制功能
本案例参考了https://blog.csdn.net/zbc1090549839/article/details/53389602
对部分内容进行了优化,感谢这位作者