Java中Semaphore(信号量)的使用

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
对部分内容进行了优化,感谢这位作者

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值