一、基本概念

Semaphor信号量是多线程中经常会用来解决多线程中可共享资源的工具计数辅助类。Semaphor本质上是一个共享锁,在内部维持一个信号许可state,线程通过acquire()获取许可即state-1;,如果信号量Semaphor中的许可没有发放完(state>0)时线程立即获取许可继续执行,否则线程阻塞等待其他线程释放许可,Semaphor再分配才可以进行执行;同时线程执行完毕必须调用release()释放许可state+1;

在多线程协同开发中线程同行通知与等待最长使用的有:

  • 1.Object.wait()和Object.notify()、Object.notifyAll();
  • 2.ReentrantLock.Condition.await()和ReentrantLock.Condition.signal();
  • 3.自旋锁模式 ;

Semaphore类也在java.util.concurrent包中的一个工具类,Semaphore内部主要通过AQS(AbstractQueuedSynchronizer)实现线程的管理。Semaphore通过构造函数参数permits设置许可数,它最后传递给了AQS的state值。Semaphore还支持公平锁和非公平锁,具体实现类似ReentrantLock在构造方法中设置一个boolean值,false为非公平锁,true为公平锁;

1.Semaphore中几个重要的函数
public Semaphore(int permits) 
/**
* permits:表示许可数;
* fair:表示是否为是否公平锁
*/
 public Semaphore(int permits, boolean fair)
 
/**
* 1.获取许可,默认申请1个许可,如果如果没有许可就会阻塞一直等待许可,直到其他线程释放release或者interrupt;
* 2.中断响应中断;
* /
 public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

 //和acquire()唯一不同的就是不响应中断
public void acquireUninterruptibly() {
        sync.acquireShared(1);
    }
 // 和acquire()不同的是,如何获取失败立即返回false,不会阻塞线程,没有阻塞自然就不需要中断了
 public boolean tryAcquire() {
        return sync.nonfairTryAcquireShared(1) >= 0;
    }
    
 //acquire()的扩展函数,超时放弃;
public boolean tryAcquire(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
    
 //acquire()的扩展函数,可以设置申请许可个数;
public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
}

//acquireUninterruptibly()的扩展函数,可以设置申请许可个数;
public void acquireUninterruptibly(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireShared(permits);
    }
    
 //线程执行完,释放许可,与acquire()相对
public void release() {
        sync.releaseShared(1);
    }
    
 // 线程执行完,释放许可,可以一次释放多个许可;
public void release(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.releaseShared(permits);
    }
    
 //查询Semaphor剩余多少许可;
public int availablePermits() {
        return sync.getPermits();
    }
    
//一次申请Semaphor中所有剩余的许可;返回许可个数
 public int drainPermits() 
        return sync.drainPermits();
    }
    
  //函数用来返回正在等待申请资源的线程的数量
 public final int getQueueLength() {
        return sync.getQueueLength();
    }
    //函数用来判断是否有现成正在等待申请资源
     public final boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
	//函数返回当前正在等待申请资源的线程集合
 protected Collection<Thread> getQueuedThreads() {
        return sync.getQueuedThreads();
    }
2.Semaphore 中一个重要的问题:

由于Semaphore锁的实现原理采用计数方式的state记录许可个数并没有某个特定线程所绑定,即在Semaphore 中,release函数和acquire并没有要求一定是同一个线程都调用,可以A线程申请资源,B线程释放资源【有点类LockSupport,LockSupport必须在线程中lock获取锁,但是可以在其他线程释放锁】;这就难以在代码中保证release、acquire调用的先后顺序,在这时候就出现了一个“bug”;如果在调用acquire之前调用release释放许可,这样就会出现Semaphore的许可state的值大于在构造函数设置的值;

public SemaphoreTest() {
        Semaphore semaphore = new Semaphore(3);
        ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        executorService.submit(() -> {
            try {
                semaphore.release();
                // TODO
                System.out.println("semaphore availablePermits:  " + semaphore.availablePermits() + "---ForEeachID : >");
                semaphore.acquire();
                Thread.sleep(2000);
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
//结果:semaphore availablePermits:  4---ForEeachID : >
  • 注意
    调用release函数之前并没有要求一定要先调用acquire函数,但是注意防止出现由于release导致的semaphore 的许可大于在构造函数中设置的值

二、应用场景

Semaphore可以用来做流量控制,特别少量的公用资源与大量的使用者的应用场景,比如:
1.网上最常见的银行窗口与客户、校园食堂与学生;
2.数据库连接,多线程读数据;
这里附上在一篇博文看到好玩的校园食堂学生打饭的示例Demo:

  • 模拟学校食堂的窗口打饭场景
    食堂有2个打饭窗口,有20个学生排队打饭,每个人打饭时耗费时间不一样;
    学生排队的耐心:
    1.耐心好,一直等待直到打到饭;
    2.耐心不好,等待时间超过了心里预期,不再排队,回宿舍吃泡面了;
    3.意外情况,排队时突然接到通知全班聚餐,不再排队,而是去吃大餐了;
  • 逻辑分析
    食堂有2个打饭窗口:需要定义一个permits=2的Semaphore对象。
    学生 按次序 排队打饭:此Semaphore对象是公平的。
    有20个学生:定义20个学生线程。
    打到饭的学生:调用了acquireUninterruptibly()方法,无法被中断
    吃泡面的学生:调用了tryAcquire(timeout,TimeUnit)方法,并且等待时间超时了
    吃大餐的学生:调用了acquire()方法,并且被中断了;
public SemaphoreTest() {
        Semaphore semaphore = new Semaphore(2);
        //101班的学生
        Thread[] students = new Thread[5];
        for (int i = 0; i < 20; i++) {
            //前10个同学都在耐心的等待打饭
            if (i < 10) {
                new Thread(new Student("第"+i+"个学生:"+"打饭" , semaphore, 0)).start();
            } else if (i >= 10 && i < 15) {//这5个学生没有耐心打饭,只会等1000毫秒
                new Thread(new Student("第"+i+"个学生:"+"吃泡面去啦" , semaphore, 1)).start();
            } else {//这5个学生没有耐心打饭
                students[i - 15] = new Thread(new Student("第"+i+"个学生:"+"全班聚餐啦" + i, semaphore, 2));
                students[i - 15].start();
            }
        }
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 5; i++) {
            students[i].interrupt();
        }
    }

public  class Student implements Runnable {
        //TAG
        private String name;
        //打饭许可
        private Semaphore semaphore;
        /**
         * 打饭方式
         * 0    一直等待直到打到饭
         * 1    等了一会不耐烦了,回宿舍吃泡面了
         * 2    打饭中途被其他同学叫走了,不再等待
         */
        private int type;

        public Student(String name, Semaphore semaphore, int type) {
            this.name = name;
            this.semaphore = semaphore;
            this.type = type;
        }

        /**
         * <p>打饭</p>
         *
         * @author hanchao 2018/3/31 19:49
         **/
        @Override
        public void run() {
            //根据打饭情形分别进行不同的处理
            switch (type) {
                //打饭时间
                //这个学生很有耐心,它会一直排队直到打到饭
                case 0:
                    //排队
                    semaphore.acquireUninterruptibly();
                    //进行打饭
                    try {
                        Thread.sleep(RandomUtils.nextLong(1000, 3000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //将打饭机会让后后面的同学
                    semaphore.release();
                    //打到了饭
                    System.out.println(name+"---> OVER");
                    break;

                //这个学生没有耐心,等了1000毫秒没打到饭,就回宿舍泡面了
                case 1:
                    //排队
                    try {
                        //如果等待超时,则不再等待,回宿舍吃泡面
                        if (semaphore.tryAcquire(RandomUtils.nextInt(6000, 16000), TimeUnit.MILLISECONDS)) {
                            //进行打饭
                            try {
                                Thread.sleep(RandomUtils.nextLong(1000, 3000));
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            //将打饭机会让后后面的同学
                            semaphore.release();
                            //打到了饭
                            System.out.println(name+"---> OVER");
                        } else {
                            //回宿舍吃泡面
                            System.out.println(name+"---> OVER");
                        }
                    } catch (InterruptedException e) {
                        //e.printStackTrace();
                    }
                    break;

                //这个学生也很有耐心,但是他们班突然宣布聚餐,它只能放弃打饭了
                case 2:
                    //排队
                    try {
                        semaphore.acquire();
                        //进行打饭
                        try {
                            Thread.sleep(RandomUtils.nextLong(1000, 3000));
                        } catch (InterruptedException e) {
                            //e.printStackTrace();
                        }
                        //将打饭机会让后后面的同学
                        semaphore.release();
                        //打到了饭
                        System.out.println(name+"---> OVER");
                    } catch (InterruptedException e) {
                        //e.printStackTrace();
                        //被叫去聚餐,不再打饭
                        System.out.println(name+"---> OVER");
                    }
                    break;
                default:
                    break;
            }
        }
    }

三、Semaphore和Lock区别

在看到JDK中Semaphore的源码中函数时第一反应是和ReentrantLock中的函数的功能很相识;

1.共享锁与排他锁(独占锁)

Lock中ReentrantLock、ReentrantReadWriteLock.WriteLock则是排他锁,ReentrantReadWriteLock.ReadLock也是共享锁;
Semaphore的共享锁与排他锁
Semaphore信号量在state>1时是典型的共享锁;
Semaphore信号量在state=1时,Semaphore就变成排他锁;

2.公平锁与非公平锁

在源码中很显然Semaphore和Lock都是可以实现公平锁和非公平锁;二者都是默认为非公平锁,而且都是在构造函数中传入一个boolean是否为公平锁;

3.获取锁与释放锁先后顺序

在Lock中很显然是lock与unlock是有顺序要求的,否则就抛出异常;由于Semaphore特殊的计数类似实现锁,所以Semaphore中release函数和acquire函数;这样会出现一个新的问题见上文只的注意;