可口的JAVA-并发控制之Semaphore

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。 本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

注:掘金潜水怪今日起开更 注:老后端不水图

前言

前文介绍了CountDownLatchCyclicBarrier跨线程通信,现在给大家介绍一个平时使用较多的线程信号同步工具Semaphore,我们通常都把他和ReentrantLock与synchronized进行比较,后两者只允许一个线程访问某一个资源(下次着重讲一下,这里主要讲前者),而前者Semaphore可以允许多个线程同时访问某一段资源。我们还是从介绍、方法、场景、原理四个角度开展学习,马上开始。

一:介绍

官方介绍:计数信号量。 从概念上讲,信号量维护一组许可。 如有必要,每个acquire块直到许可可用,然后获取它。 每个release增加一个许可,可能会释放一个阻塞的收单方。 但是,没有使用实际的许可对象; Semaphore只是计算可用的数量并相应地采取行动。
信号量通常用于限制可以访问某些(物理或逻辑)资源的线程数。

我来解释一下:

  • 这是一个信号量工具,里面可以维护自定义数量的令牌信号数量。
  • 我们可以使用acquire方法来拿到一个令牌然后继续进行业务操作,如果获取不到线程将会阻塞,直到获取到令牌或者其他线程发生interrupted操作。
  • release可以释放令牌。

    二:方法

    Semaphore的主要方法有:

    void acquire() //获取令牌,没获取直接阻塞(可以被中断),方法(可传参 permits 令牌个数)
    复制代码
    void acquireUninterruptibly()//同上(不可被中断),方法(可传参 permits 令牌个数)
    复制代码
     boolean tryAcquire() //尝试获取一个令牌,没有则返回false,不会阻塞,方法(可传参 permits 令牌个数)
    复制代码
    void release() // 释放令牌,方法(可传参 permits 令牌个数)
    复制代码
    int availablePermits() //当前剩余可用令牌个数
    复制代码
    int drainPermits() // 获取并返回所有立即可用的许可证
    复制代码
    void reducePermits(int reduction) //减少部分令牌
    复制代码
    boolean isFair() //是否公平锁
    复制代码
    boolean hasQueuedThreads() //是否有线程在阻塞等待获取令牌
    复制代码
     int getQueueLength() //获取阻塞线程个数
    复制代码

    Semaphore 构造方法有两个,公平锁和非公平锁,官方解释是:非公平锁效率要优于公平队列,默认构造方法采用的是非公平锁
    new Semaphore(2,true);可以创建公平锁对象。

    来一个简单的示例帮助大家理解:

    @SneakyThrows
    static void singleSema(){
        Semaphore semaphore = new Semaphore(1);
        new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"获取令牌中");
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName()+"拿到令牌,执行业务操作");
                Thread.sleep((int)(Math.random()*1000));
                semaphore.release();
                System.out.println(Thread.currentThread().getName()+"释放令牌");
            }
        }, "张三").start();
        new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"获取令牌中");
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName()+"拿到令牌,执行业务操作");
                Thread.sleep((int)(Math.random()*1000));
                semaphore.release();
                System.out.println(Thread.currentThread().getName()+"释放令牌");
            }
        },"李四").start();
        System.out.println("主线程获取令牌中");
        semaphore.acquire();
        System.out.println("主线程获取到令牌");
        Thread.sleep(100);
        System.out.println("主线程释放令牌");
        semaphore.release();
    }
    复制代码

    这是一个令牌传递操作,令牌资源只能同时一个人拥有,跟lock和syn实现的效果是一样的。
    一起来看运行结果:

    张三获取令牌中
    主线程获取令牌中
    主线程获取到令牌
    李四获取令牌中
    主线程释放令牌
    张三拿到令牌,执行业务操作
    张三释放令牌
    李四拿到令牌,执行业务操作
    李四释放令牌
    复制代码

    令牌只能在一个人手中。

    三:场景

    Semaphore的使用场景比较简单,令牌数量是可控的,它既可以用来控制线程资源独占,也可以允许多个线程共享某一资源。在使用的过程中为了避免阻塞可以采用tryQcuire方法来获取令牌。

    场景一:线程资源独占

    如上所示,实现类似syn的锁效果。参考上述示例代码。。。

    场景二:线程资源共享

    学校进行短跑测评,要求测出每个学生的100m短跑耗时,但是跑道只有3条,全校共有5名学生,为了最大限度的利用跑道,决定每个学生跑完之后立即让下一个人进入跑道,不再凑齐3跳跑道全满后再开始。

    源代码:

    @SneakyThrows
    static void mulitSema(){
        Semaphore semaphore = new Semaphore(3);
        new Thread(new Dog(semaphore),"张三").start();
        new Thread(new Dog(semaphore),"李四").start();
        new Thread(new Dog(semaphore),"王五").start();
        new Thread(new Dog(semaphore),"马六").start();
        new Thread(new Dog(semaphore),"韩七").start();
        Thread.sleep(2000);
        while (semaphore.hasQueuedThreads()){
            System.out.println("isfair::"+semaphore.isFair()+",availablepermits::"+semaphore.availablePermits()
                    +",queuelength::"+semaphore.getQueueLength()+",tryacquire::"+semaphore.tryAcquire());
            semaphore.release();
            Thread.sleep(100);
        }
        System.out.println("主线程结束::"+Thread.currentThread().getName());
    }
    复制代码
    class Dog implements Runnable{
        private Semaphore semaphore;
        public Dog(Semaphore semaphores){
            semaphore = semaphores;
        }
        @Override
        public void run() {
            try {
                Thread.sleep((int)(Math.random()*1000));
                System.out.println(Thread.currentThread().getName()+"::正在等待起跑指令");
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName()+"::接到起跑指令");
                Thread.sleep((int)(Math.random()*1000));
                System.out.println(Thread.currentThread().getName()+"::跑步完成,释放信号");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    复制代码

    简单分析代码,当某个学生跑完后立即通知下个学生占用该跑道开始跑步。
    运行结果:

    马六::正在等待起跑指令
    马六::接到起跑指令
    张三::正在等待起跑指令
    张三::接到起跑指令
    韩七::正在等待起跑指令
    韩七::接到起跑指令
    张三::跑步完成,释放信号
    马六::跑步完成,释放信号
    李四::正在等待起跑指令
    韩七::跑步完成,释放信号
    王五::正在等待起跑指令
    isfair::false,availablepermits::0,queuelength::2,tryacquire::false
    李四::接到起跑指令
    isfair::false,availablepermits::0,queuelength::1,tryacquire::false
    王五::接到起跑指令
    主线程结束::main
    李四::跑步完成,释放信号
    王五::跑步完成,释放信号
    复制代码

    我们可以发现三条跑道得到了充分的利用,整体测试时间也得到了极大的缩短。

    四:原理

      第一:初始化

    • 我们构造new Semaphore(2) 方法后,会创建一个非公平的锁的同步阻塞队列,也可以构建公平锁同步阻塞队列。
    • 然后会把传入的令牌数量赋值给队列的state状态,state表示当前剩余的令牌信号数目。

      image.png
      image.png

        第二:获取令牌

      • 当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列的state ,获取一个令牌则修改为state=state-1。
      • 如果state>=0,表示获取令牌成功。
      • 如果state<0,令牌获取数量不足,此时会创建一个Node节点并加入到阻塞队列,直到有空余令牌。

        image.png

          第三:释放令牌

        • 线程释放一个令牌,state修改为state=state+1,表示释放成功,释放失败或返回false需要再次尝试。
        • 当有个令牌被释放了,系统会唤醒一个等待中的阻塞线程。
        • 被唤醒的线程会运算state=state-1,判断state>=0表示获取令牌成功。

          image.png

          感谢大家的阅读,欢迎评论区留言讨论。