Semaphore的注意点


seamphore大家玩的都比较多,使用起来也很简单,获取令牌和释放,但是其中坑却不少,而且会让人很难发现,希望能通俗易懂的小例子讲明白其中的几个道理。


一、线程都被阻塞了?

public class demo2 {
    static Semaphore semaphore = new Semaphore(1);

    public static void method1() {
        try {
            System.out.println(Thread.currentThread().getName()+" ,当前等待队列的线程数" + semaphore.getQueueLength());
            semaphore.acquire(1);
            for (; ; ) {
                //模拟长业务
            }
        } catch (InterruptedException e) {
            System.out.println("get semaphore interrupted...");
        } finally {
            semaphore.release(1);
            System.out.println(Thread.currentThread().getName() + "线程走了,可用数量 " + semaphore.availablePermits());
        }
    }

    public static void main(String[] args) {
        new Thread(demo2::method1).start();
        new Thread(demo2::method1).start();
        new Thread(demo2::method1).start();
        new Thread(demo2::method1).start();
        new Thread(demo2::method1).start();
        new Thread(demo2::method1).start();
    }
}

在这里插入图片描述
现象:
执行完上面的代码,我们可以发现,程序没停止且大量的线程都被阻塞在队列中了。

原因:
因为acquire具有阻塞性,会将获取不到令牌的线程阻塞在队列中,而在生产中,我们的业务如果有大量的任务要跑,很可以产生大量的任务挤压在队列,最后导致oom;解决方案也很简单,就是用tryAcquire来代替,获取不到立刻(或者执行时间内返回)

二、谁动了我的令牌?

public class demo {
    static Semaphore semaphore = new Semaphore(1);

    public static void method1(int i) {
        try {
             boolean b = semaphore.tryAcquire(1);
            System.out.println(Thread.currentThread().getName() + "线程尝试获取 ..结果为:" + b);
            if (b) {
                System.out.println(Thread.currentThread().getName() + "线程进来了,当前可用数量 " + semaphore.availablePermits());
                TimeUnit.SECONDS.sleep(i);
            }
        } catch (InterruptedException e) {
            System.out.println("get semaphore interrupted...");
        } finally {
            semaphore.release(1);
            System.out.println(Thread.currentThread().getName() + "线程走了,可用数量 " + semaphore.availablePermits());
        }
    }
    public static void main(String[] args) {
        new Thread(() -> demo.method1(1)).start(); // 线程1,sleep 1s
        new Thread(() -> demo.method1(0)).start();  // 线程2, no sleep
    }
}

在这里插入图片描述
现象:
正常我们release操作都会在finally里,但是执行完上面的代码,我们可以发现,令牌数量惊奇的变成了2,比我们的初始值1还多。

原因:
线程1虽然没有获取锁,但是还是会执行了finally里的release操作,而release操作只会将state(AQS的同步值)+1,即不会和线程绑定,也不会去判断state的值有没有超过初始值的大小,所以令牌数量被无情的增加了。

方案1: 普通的if判断

在这里插入图片描述

方案2: Seamphore类进行增强(增强的方法,根据需要)

原理:存储获取令牌的线程,释放的时候判断线程有没有获取过令牌

public class SafeSemaphore extends Semaphore {

    private static final Object object = new Object();

    private ConcurrentHashMap<Thread, Object> threadSet;

    public SafeSemaphore(int permits) {
        super(permits);
        threadSet = new ConcurrentHashMap<>(permits);
    }

    @Override
    public boolean tryAcquire(int permits)  {
        if (super.tryAcquire(permits)) {
            threadSet.put(Thread.currentThread(), object);
            return true;
        }
        return false;
    }

    @Override
    public void release(int permits) {
        final Thread thread = Thread.currentThread();
        if (threadSet.containsKey(thread)) {
            super.release(permits);
            threadSet.remove(thread);
        }
    }

    public SafeSemaphore(int permits, boolean fair) {
        super(permits, fair);
        threadSet.put(Thread.currentThread(), object);
    }

    @Override
    public void acquire() throws InterruptedException {
        super.acquire();
        threadSet.put(Thread.currentThread(), object);
    }

    @Override
    public void release() {
        final Thread thread = Thread.currentThread();
        if (threadSet.containsKey(thread)) {
            super.release();
            threadSet.remove(thread);
        }
    }


    @Override
    public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
        if (super.tryAcquire(timeout, unit)) {
            threadSet.put(Thread.currentThread(), object);
            return true;
        }
        return false;
    }


}

在这里插入图片描述

三、怎么永远到轮不到我?

public class demo4 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(2);
        new Thread(new MyRunnable(1, semaphore), "thread-A").start();
        new Thread(new MyRunnable(2, semaphore), "thread-C").start();
    }

    static class MyRunnable implements Runnable {
        private int n;
        private Semaphore semaphore;
        public MyRunnable(int n, Semaphore semaphore) {
            this.n = n;
            this.semaphore = semaphore;
        }
        @Override
        public void run() {
            try {
                semaphore.acquire(n);
                System.out.println("剩余可用许可证: " + semaphore.drainPermits());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release(n);
                System.out.println(Thread.currentThread().getName() + "释放。。。。");
            }
        }}}

在这里插入图片描述现象:
我们发现线程C永远得不到执行,于是开始了思考。。。。

我们尝试把C线程改成如下代码,也就是只获取一个令牌,却正常执行了,难道是令牌数量在作怪?
在这里插入图片描述

恍然大悟:
线程A虽然获取了1个释放了1个,但是注意drainPermits这个方法的作用是获取剩余令牌并且清空剩余令牌,因此获取剩余1个可用令牌后,可用令牌为0了,如果线程C需要一个令牌那么等A执行完了释放了就可以执行,看起来一切正常,但是当线程c需要大于等二个令牌的时候,即使A释放了也满足不了C,(因为原来的令牌被清空了)导致线程C一直无法执行,而阻塞,所以我们应该使用availablePermit获取剩余可用令牌,而不是drainPermits。

验证

在这里插入图片描述

四、总结

1.尽量使用tryAcquire 避免阻塞
2.释放操作放在finally中,一定要判断是否获取过信号量
3.获取可用令牌数区分availablePermitsdrainPermits的区别

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值