Semaphore原理浅析和相关面试题分析

本文首发在个人公众号:HelloWorldEE,欢迎关注。

本篇文章的来源是这样,有一天,我一同学面试某公司回来,和我分享其被问的相关面试题。其中就有一道关于Semaphore的面试题,个人觉得比较经典,分享出来供大家参考。

具体同学和面试官的对话还原出来是这样。

面试官:现在有一个方法task,希望只能被10个线程调用,利用Java相关类,应该如何来实现?

同学:使用Java中的Semaphore类来实现,当一个线程调用方法task之前先从Semaphore申请令牌,如果申请到了,则调用task方法,调用完之后释放令牌供其他的线程使用,如果没有,则阻塞等待。

面试官:Semaphore中申请令牌、释放令牌的方法叫什么?

同学:acquire、release方法

面试官:semaphore初始化有10个令牌,11个线程同时各调用1次acquire方法,会发生什么?

同学:拿不到令牌的线程阻塞,不会继续往下运行。

面试官.semaphore初始化有10个令牌,一个线程重复调用11次acquire方法,会发生什么?

同学:稍微有点蒙,因为考虑到了锁的重入问题,不知道令牌会不会和锁一样,是可以重入的。注:笔者留给大家思考,后面会给出答案。

面试官:semaphore初始化有1个令牌,1个线程调用一次acquire方法,然后调用两次release方法,之后另外一个线程调用acquire(2)方法,此线程能够获取到足够的令牌并继续运行吗?

同学:额,比较蒙,一个线程获取一个令牌然后释放两个令牌,Semaphore会允许吗?同学回答应该不允许拿一个令牌然后释放多个令牌。注:笔者留给大家思考,后面会给出答案。

面试官.semaphore初始化有2个令牌,一个线程调用1次release方法,然后一次性获取3个令牌,会获取到吗?

同学:额,继续蒙。。。心里想:不调用acquire方法来获取令牌,直接调用release方法来释放令牌,semaphore允许吗?

到这里,关于Semaphore的面试题就结束了,一问接一问,一环接一环,确实如果对Semaphore这个类没有特别细致的研究,回答起来是比较困难的。除了这个问题,还有其他的问题也相当经典,在后续的文章中,笔者会一一分析出来给大家分享,这样一场精彩的面试,为同学和面试官点赞。

下面回到Semaphore类,关于J.C.U包下Semaphore这个类,作为Java程序员,应该或许都会听说过和使用过。

这个类的作用:常常被用来控制访问速率。

例如:对于某一种资源,我们希望其最多被N个线程同时访问。

真实的场景:库存秒杀限流。

具体代码实现如下:

public class TestSemaphore {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        final Semaphore semaphore = new Semaphore(2);
        for (int index = 0; index < 10; index++) {
            final int taskId = index;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        //1.获取执行令牌,如果获取不到,则阻塞
                        semaphore.acquire();
                        //2.执行任务
                        task();
                        //3.释放令牌
                        semaphore.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        executorService.shutdown();

    }
    public static void task() {
        try {
            System.out.println("task开始执行");
            Thread.sleep(1000);
            System.out.println("task执行结束");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

看了上面的例子,对于Semaphore的使用,是比较简单的。其大致过程如下:

1.调用acquire来获取令牌,如果获取到,则继续运行,运行完成之后,调用release方法释放令牌供后面的线程使用。

2.如果获取不到,则等待,直到有令牌空闲出来,其才会被唤醒然后获取令牌之后继续运行。

通过上面的示例,怎么使用,我们应该都清楚了。那么对于acquire、release方法的实现原理是怎么样的呢?如果有兴趣,你可以去看一眼这个类的源码,比较简单,就是借助了一个volatile的变量state,然后当调用acquire方法是,就是对state进行减一操作,由于state–操作不是原子性操作,因为为保证原子性,采用的是cas来完成。当调用release,就是对state进行加一操作,也是采用的cas来完成。

下面是acquire、release方法的部分核心代码。

  final int nonfairTryAcquireShared(int acquires) {
        for (;;) {
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }


  protected final boolean tryReleaseShared(int releases) {
        for (;;) {
            int current = getState();
            int next = current + releases;
            if (next < current) // overflow
                throw new Error("Maximum permit count exceeded");
            if (compareAndSetState(current, next))
                return true;
        }
    }

看完相关的实现原理,简单来说:semaphore就是一个计算器。更具体、详细的源码分析可以参考相应的文章。

回到文章开头的面试题

问题1.semaphore初始化有10个令牌,11个线程同时各调用1次acquire方法,会发生什么?

答案:拿不到令牌的线程阻塞,不会继续往下运行。

问题2.semaphore初始化有10个令牌,一个线程重复调用11次acquire方法,会发生什么?

答案:线程阻塞,不会继续往下运行。可能你会考虑类似于锁的重入的问题,很好,但是,令牌没有重入的概念。你只要调用一次acquire方法,就需要有一个令牌才能继续运行。

问题3.semaphore初始化有1个令牌,1个线程调用一次acquire方法,然后调用两次release方法,之后另外一个线程调用acquire(2)方法,此线程能够获取到足够的令牌并继续运行吗?

答案:能,原因是release方法会添加令牌,并不会以初始化的大小为准。

问题4.semaphore初始化有2个令牌,一个线程调用1次release方法,然后一次性获取3个令牌,会获取到吗?

答案:能,原因是release会添加令牌,并不会以初始化的大小为准。Semaphore中release方法的调用并没有限制要在acquire后调用。

具体示例如下,如果不相信的话,可以运行一下下面的demo,在做实验之前,笔者也认为应该是不允许的。。

public class TestSemaphore2 {
    public static void main(String[] args) {
        int permitsNum = 2;
        final Semaphore semaphore = new Semaphore(permitsNum);
        try {
            System.out.println("availablePermits:"+semaphore.availablePermits()+",semaphore.tryAcquire(3,1, TimeUnit.SECONDS):"+semaphore.tryAcquire(3,1, TimeUnit.SECONDS));
            semaphore.release();
            System.out.println("availablePermits:"+semaphore.availablePermits()+",semaphore.tryAcquire(3,1, TimeUnit.SECONDS):"+semaphore.tryAcquire(3,1, TimeUnit.SECONDS));
        }catch (Exception e) {

        }
    }
}

到这里,关于Semaphore的分析,以及常见面试题的分析就结束了,希望对大家有一定的收获。

你可能感兴趣的:(面试题,java)