参考资料:
1. http://blog.csdn.net/zmx729618/article/details/51593666
2. jdk官方文档
java提供了一个类Semaphore来实现信号量,概念上讲,一个信号量相当于持有一些许可(permits),线程可以调用Semaphore对象的acquire()方法获取一个许可,调用release()来归还一个许可
1 构造方法:
Semaphore有两个构造方法 Semaphore(int)
、Semaphore(int,boolean)
,参数中的int表示该信号量拥有的许可数量,boolean表示获取许可的时候是否是公平的,如果是公平的那么,当有多个线程要获取许可时,会按照线程来的先后顺序分配许可,否则,线程获得许可的顺序是不定的。这里在jdk中讲到 “一般而言,非公平时候的吞吐量要高于公平锁”,这是为什么呢?附上链接中的一段话:
非公平锁性能高于公平锁性能的原因:在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面:B获得锁的时刻并没有推迟,C更早的获得了锁,并且吞吐量也提高了。当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。在这些情况下,插队带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)可能不会出现。
2 获取许可
可以使用acquire()、acquire(int)、tryAcquire()
等去获取许可,其中int参数表示一次性要获取几个许可,默认为1个,acquire方法在没有许可的情况下,要获取许可的线程会阻塞,而tryAcquire()方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞,这与Lock类的lock()与tryLock()类似
3 释放许可
线程可调用 release()、release(int)来释放(归还)许可,注意一个线程调用release()之前并不要求一定要调用了acquire (There is no requirement that a thread that releases a permit must have acquired that permit by calling {@link #acquire})
4 使用场景
我们一般使用信号量来限制访问资源的线程数量,比如有一个食堂,最多允许5个人同时吃饭,则如下:
class EatThread extends Thread{
private Semaphore semaphore;
public EatThread(Semaphore semaphore){
this.semaphore=semaphore;
}
@Override
public void run(){
try {
semaphore.acquire();//获取一个许可,当然也可以调用acquire(int),这样一个线程就能拿到多个许可
long eatTime=(long) (Math.random()*10);
System.out.println(Thread.currentThread().getId()+" 正在吃饭");
TimeUnit.SECONDS.sleep(eatTime);
System.out.println(Thread.currentThread().getId()+" 已经吃完");
semaphore.release();//归还许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(5);//总共有5个许可
for(int i=0;i<7;i++){//定义七个吃的线程
new EatThread(semaphore).start();
}
}
}
结果如下:
9 正在吃饭
15 正在吃饭
13 正在吃饭
13 已经吃完
11 正在吃饭
10 正在吃饭
10 已经吃完
12 正在吃饭
14 正在吃饭
11 已经吃完
15 已经吃完
14 已经吃完
9 已经吃完
12 已经吃完
当我们在构造Semaphore对象时,如果设置的许可数量为1,这时便会达到一个互斥排他锁的效果,只有一个许可,有一个线程获取了这个许可,那么其他线程只有等待这个线程归还了许可才能获取到许可,当将Semaphore用作互斥排他锁的作用时,要注意:
A semaphore initialized to one, and which is used such that it only has at most one permit available, can serve as a mutual exclusion lock. This is more commonly known as a binary semaphore, because it only has two states: one permit available, or zero permits available. When used in this way, the binary semaphore has the property (unlike many Lock implementations), that the “lock” can be released by a thread other than the owner (as semaphores have no notion of ownership). This can be useful in some specialized contexts, such as deadlock recovery.
文档中提到,Semaphore与jdk中的Lock
的区别是
1. 使用Lock.unlock()之前,该线程必须事先持有这个锁(通过Lock.lock()获取),如下:
public class LockTest {
public static void main(String[] args) {
Lock lock=new ReentrantLock();
lock.unlock();
}
}
则会抛出异常,因为该线程事先并没有获取lock对象的锁:
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460)
at LockTest.main(LockTest.java:12)
对于Semaphore来讲,如下:
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(1);//总共有1个许可
System.out.println("可用的许可数目为:"+semaphore.availablePermits());
semaphore.release();
System.out.println("可用的许可数目为:"+semaphore.availablePermits());
}
}
结果如下:
可用的许可数目为:1
可用的许可数目为:2
i. 并没有抛出异常,也就是线程在调用release()之前并不要求先调用acquire()
ii. 我们看到可用的许可数目增加了一个,但我们的初衷是保证只有一个许可来达到互斥排他锁的目的,所以这里要注意一下
2 Semaphore(1)可以做到一个deadlock recovery,我们来看下面一个例子
class WorkThread2 extends Thread{
private Semaphore semaphore1,semaphore2;
public WorkThread2(Semaphore semaphore1,Semaphore semaphore2){
this.semaphore1=semaphore1;
this.semaphore2=semaphore2;
}
public void releaseSemaphore2(){
System.out.println(Thread.currentThread().getId()+" 释放Semaphore2");
semaphore2.release();
}
public void run() {
try {
semaphore1.acquire(); //先获取Semaphore1
System.out.println(Thread.currentThread().getId()+" 获得Semaphore1");
TimeUnit.SECONDS.sleep(5); //等待5秒让WorkThread1先获得Semaphore2
semaphore2.acquire();//获取Semaphore2
System.out.println(Thread.currentThread().getId()+" 获得Semaphore2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class WorkThread1 extends Thread{
private Semaphore semaphore1,semaphore2;
public WorkThread1(Semaphore semaphore1,Semaphore semaphore2){
this.semaphore1=semaphore1;
this.semaphore2=semaphore2;
}
public void run() {
try {
semaphore2.acquire();//先获取Semaphore2
System.out.println(Thread.currentThread().getId()+" 获得Semaphore2");
TimeUnit.SECONDS.sleep(5);//等待5秒,让WorkThread1先获得Semaphore1
semaphore1.acquire();//获取Semaphore1
System.out.println(Thread.currentThread().getId()+" 获得Semaphore1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class SemphoreTest {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore1=new Semaphore(1);
Semaphore semaphore2=new Semaphore(1);
new WorkThread1(semaphore1, semaphore2).start();
new WorkThread2(semaphore1, semaphore2).start();
//此时已经陷入了死锁,WorkThread1持有semaphore1的许可,请求semaphore2的许可
// WorkThread2持有semaphore2的许可,请求semaphore1的许可
// TimeUnit.SECONDS.sleep(10);
// //在主线程是否semaphore1,semaphore2,解决死锁
// semaphore1.release();
// semaphore2.release();
}
}
在注释最后面几行代码的情况下,结果为,陷入了一个死锁:
9 获得Semaphore2
10 获得Semaphore1
把注释删除,即在主线程释放Semaphore,这样就能解决死锁:
9 获得Semaphore2
10 获得Semaphore1
9 获得Semaphore1
10 获得Semaphore2
这即符合文档中说的,通过一个非owner的线程来实现死锁恢复,但如果你使用的是Lock则做不到,可以把代码中的两个信号量换成两个锁对象试试。很明显,前面也验证过了,要使用Lock.unlock()来释放锁,首先你得拥有这个锁对象,因此非owner线程(事先没有拥有锁)是无法去释放别的线程的锁对象