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();
new Thread(() -> demo.method1(0)).start();
}
}
现象:
正常我们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.获取可用令牌数区分availablePermits和drainPermits的区别
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)