线程之Semaphore示例
一. Semaphore基本介绍:
private final int capacity = 10;
private final Semaphore empty = new Semaphore(capacity); //仓库中空的槽的信号量
private final Semaphore full = new Semaphore(0); //仓库中被占用的槽的信号量
private final Semaphore mutex = new Semaphore(1); //互斥信号量
private int insertIndex = 0; // 仓库中当前可以放置物品的位置。
private int removeIndex = 0; // 仓库中当前可以拿走物品的位置。
信号量是多线程使用的一种方式,
计数信号量(Semaphore) 维护着一个许可集。调用acquire()获取一个许可,release()释放一个许可。
设置信号量是否采用公平模式 ? 如以公平方式执行,则线程将会按到达的顺序(FIFO)执行;如是非公平,则后请求的有可能排在队列的头部。
Semaphore在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。
同步机制信号量(Semaphore) 使用场景?
1)Semaphore可轻松完成信号量控制,可以控制多个线程同时对某个资源的访问。
用来保证两个或多个程序不能同时进入临界区,从而不能同时使用一个共享资源,达到线程互斥的作用。
Lock和synchronized是锁的互斥,一个线程如果锁定了一资源,那么其它线程只能等待资源的释放。也就是一次只有一个线程执行,等到这个线程执行完毕或者unlock。
如,厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?
同时只能有5个人能够占用,当5个人中的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。
2)单个信号量Semaphore对象可实现互斥锁的功能,且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,应用于死锁恢复的一些场合。
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。
也就是说Semaphore不一定是锁定某个资源,而是流程上的概念。
比方有A,B两个线程,B线程的操作可能要等A线程执行完毕之后才执行,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类,它们也许并不访问共享变量,只是逻辑上的先后顺序。
3)信号量也可实现线程同步:用信号量实现了生产者消费者问题:
在生产者消费者问题中,信号量full和empty用来保证某种事件顺序发生或者不发生,即对于一个物品,生产的过程始终要在消费的过程之前。
里面有三个信号量full,empty和mutex,其中full和empty用来做进程同步,mutex用来做进程互斥。
每当要生产一个物品时,我们首先检查能否获取信号量empty的一个许可,如果不可以则阻塞线程,如果可以则继续获取items数组的访问权,该访问权由互斥信号量mutex来控制,当放置完物品后,会释放信号量empty的一个许可。
同理,每当要消费一个物品时,我们首先检查能否获取信号量full的一个许可,如果不可以则阻塞线程,如果可以择继续获取items数组的访问权,当拿走物品后,会释放信号量full的一个许可。
信号量和信号区别在哪里?
如果我们单单使用信号,即使用wait和notify方法,很有可能会错过丢失某些信号通知。
比如,如果我们不对count的访问添加限制,当count为0时,调度程序知道仓库里没有物品了,便准备挂起消费者线程并启动生产者线程,
如果生产者线程的启动时间比消费者线程的挂起时间快得多,很可能会有这种情况:
当生产者线程生产了一个物品,使count由0变为1,此时会向消费者线程发送一个唤醒信号,但此时消费者线程还没有完全挂起,因此它会忽略这个信号,这样一来,生产者线程会一直生产物品,直到count为capacity时挂起,而消费者线程在完全挂起之后不会再收到唤醒信号,因此也会一直挂起,这样整个系统就全部挂起,永远不会执行下去。
当然,我们的实现方式是同一时刻只能有一个线程操作count,因此消费者线程的wait方法一定在生产者线程的notify方法之前执行,即当消费者线程完全挂起之后,生产者线程才能启动,于是不会出现错过丢失信号的问题。
而Java中Semaphore的实现也是建立在信号的基础上的,但不同的是它会保存所有的信号,不会丢弃任何信号,这样就很好地避免了前面的问题,这也是信号量和信号的最大区别所在。
信号(Signal)是一种处理异步事件的通讯方式,用于通知其他进程或者自己本身,来告知将有某种事件发生。
在Java中,信号机制通过wait(),notify()和notifyAll()来实现。其中wait()使得当前调用wait()的线程挂起,并释放已经获得的wait()所在代码块的锁;
notify()用于随即唤醒一个被wait()挂起的线程进入线程调度队列;notifyAll()用于唤醒所有被wait()挂起的线程进入线程调度队列。
二.Java并发编程 — 信号量与互斥量
信号量与互斥量的区别:
信号量用于线程同步,互斥量用户保护资源的互斥访问。
互斥量用于线程的互斥,信号线用于线程的同步。
互斥量值只能为0/1,信号量值可以为非负整数。信号量可以实现多个同类资源的多线程互斥和同步。
互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
信号量Semaphore,信号量是在多线程环境中,线程间传递信号的一种方式。
1.Java内置的Semaphore
java.util.concurrent包中有Semaphore的实现,可以设置参数,控制同时访问的个数。
下面Demo申明了一个只有5个许可的Semaphore,而有20个线程要访问这个资源,通过acquire()和release()获取和释放访问许可。
final Semaphore semp = new Semaphore(5); ExecutorService exec = Executors.newCachedThreadPool(); for (int index = 0; index < 20; index++) { final int NO = index; Runnable run = new Runnable() { public void run() { try { semp.acquire(); System.out.println("Accessing: " + NO); Thread.sleep((long) (Math.random() * 10000)); // 访问完后,释放 semp.release(); System.out.println("-----------------" + semp.availablePermits());}} }; exec.execute(run); } exec.shutdown();
2. 互斥量Mutex
互斥量:提供对资源的独占访问,只能为0/1,如果某一个资源同时只能允许一个访问者对其访问,可以使用互斥量控制线程对其访问。
互斥量实现:
public class Mutex {
private boolean isLocked = false;
public synchronized void lock() {
while(this.isLocked) //使用while可以避免线程 假唤醒
wait();
this.isLocked= true;}
}
public synchronized void unlock() throws InterruptedException{
this.isLocked= false;
this.notify();}
}
在Mutex中,我们添加了一个signal用于保存信号。
将互斥量当作锁来使用:
Mutex mutex = new Mutex();
mutex.lock();
...
//临界区
mutex.unlock();
互斥量的加锁和解锁必须由同一个线程分别对应使用。
三.信号量介绍:
原文地址 By Jakob Jenkov 转载自并发编程网 – ifeve.com 本文链接地址: 信号量
Semaphore(信号量) 是一个线程同步结构,用于在线程间传递信号,以避免出现信号丢失,或者像锁一样用于保护一个关键区域。自从5.0开始,jdk在java.util.concurrent包里提供了Semaphore 的官方实现,因此大家不需要自己去实现Semaphore。但是还是很有必要去熟悉如何使用Semaphore及其背后的原理:
本文涉及:
简单的Semaphore实现
使用Semaphore来发出信号
可计数的Semaphore
有上限的Semaphore
把Semaphore当锁来使用
1、简单的Semaphore实现
一个信号量的简单实现:
public class Semaphore {
private boolean signal = false;//使用signal可以避免信号丢失
public synchronized void take() {
this.signal = true;
this.notify();}
public synchronized void release() throws InterruptedException{
while(!this.signal) //使用while避免假唤醒
wait();
this.signal = false;}
}
Take方法发出一个被存放在Semaphore内部的信号,而Release方法则等待一个信号,当其接收到信号后,标记位signal被清空,然后该方法终止。
使用这个semaphore可以避免错失某些信号通知。用take方法来代替notify,release方法来代替wait。
如果某线程在调用release等待之前调用take方法,那么调用release方法的线程仍然知道take方法已经被某个线程调用过了,因为该Semaphore内部保存了take方法发出的信号。而wait和notify方法就没有这样的功能。当用semaphore来产生信号时,take和release这两个方法名看起来有点奇怪。
这两个名字来源于后面把semaphore当做锁的例子,后面会详细介绍这个例子,在该例子中,take和release这两个名字会变得很合理。
2、使用Semaphore来产生信号
下面的例子,两个线程通过Semaphore发出的信号来通知对方
Semaphore semaphore = new Semaphore();
SendingThread sender = new SendingThread(semaphore);
ReceivingThread receiver = new ReceivingThread(semaphore);
receiver.start();
sender.start();
public class SendingThread {
Semaphore semaphore = null;
public SendingThread(Semaphore semaphore){
this.semaphore = semaphore;
}
public void run(){
while(true){
//do something, then signal
this.semaphore.take();
}}
}
public class RecevingThread {
Semaphore semaphore = null;
public ReceivingThread(Semaphore semaphore){
this.semaphore = semaphore;
}
public void run(){
while(true){
this.semaphore.release();
//receive signal, then do something...}
}}
3、可计数的Semaphore
上面提到的Semaphore的简单实现并没有计算通过调用take方法所产生信号的数量。可以把它改造成具有计数功能的Semaphore。下面是一个可计数的Semaphore的简单实现。
public class CountingSemaphore {
private int signals = 0;
public synchronized void take() {
this.signals++;
this.notify(); }
public synchronized void release() throws InterruptedException{
while(this.signals == 0) wait();
this.signals--;}
}
4、有上限的Semaphore
上面的CountingSemaphore并没有限制信号的数量。下面的代码将CountingSemaphore改造成一个信号数量有上限的BoundedSemaphore。
public class BoundedSemaphore {
private int signals = 0;
private int bound = 0;
public BoundedSemaphore(int upperBound){
this.bound = upperBound;
}
public synchronized void take() throws InterruptedException{
while(this.signals == bound) wait();
this.signals++;
this.notify();
}
public synchronized void release() throws InterruptedException{
while(this.signals == 0) wait();
this.signals--;
this.notify();}
}
在BoundedSemaphore中,当已经产生的信号数量达到了上限,take方法将阻塞新的信号产生请求,直到某个线程调用release方法后,被阻塞于take方法的线程才能传递自己的信号。
5、把Semaphore当锁来使用
当信号量的数量上限是1时,Semaphore可以被当做锁来使用。通过take和release方法来保护关键区域。请看下面的例子:
BoundedSemaphore semaphore = new BoundedSemaphore(1);
...
semaphore.take();
try{
//critical section
} finally {
semaphore.release();
}
在前面的例子中,Semaphore被用来在多个线程之间传递信号,这种情况下,take和release分别被不同的线程调用。
但是在锁这个例子中,take和release方法将被同一线程调用,因为只允许一个线程来获取信号(允许进入关键区域的信号),其它调用take方法获取信号的线程将被阻塞,知道第一个调用take方法的线程调用release方法来释放信号。对release方法的调用永远不会被阻塞,这是因为任何一个线程都是先调用take方法,然后再调用release。
通过有上限的Semaphore可以限制进入某代码块的线程数量。设想一下,在上面例子中,如果BoundedSemaphore 上限设为5将会发生什么?意味着允许5个线程同时访问关键区域,但是你必须保证,这个5个线程不会互相冲突。否则你的应用程序将不能正常运行。
必须注意,release方法应当在finally块中被执行。这样可以保在关键区域的代码抛出异常的情况下,信号也一定会被释放。
二. 示例
为了提高接口的响应速度,可以使用ThreadPoolExecutor + Runnable 或者ThreadPoolExecutor 并发调用 技术来并行执行task。但是ThreadPoolExecutor有个特点,就是当core线程不足以应付请求的时候,会将task加入到队列中。一旦使用队列,那么就可能出现队列爆掉或者队列导致的内存溢出问题。
为了尽快提供接口响应速度,但是又不想使用队列特性的话。可以使用信号量来做到。
Semaphore信号量管理着一组许可,在执行操作时需要首先获得许可,并在使用后释放许可。如果已经没有许可了, acquire方法将一直阻塞,直到有许可。
信号量简单例子:
ThreadPoolExecutor中使用信号量:
在ThreadPoolExecutor中,我们在定义core线程参数的时候,比如定义为10个,那么使用信号量的时候,初始化参数也设置为10.
Semaphore<Integer> sem= new Semaphore<>(10);
ThreadPoolExecutor中,如果不想用到队列,就必须保证线程池中始终只有core线程在工作。那么当请求太多,core线程处理不过来的时候,用信号量进行阻塞,
保证只有当core线程的某些线程执行完后,阻塞才解开。
这里使用JAVA并发编程一书中的例子来说明信号量的基本用法。
public class BoundedHashSet<T> {
public static void main(String[] args) throws Exception {
BoundedHashSet<String> set = new BoundedHashSet<>(2);
set.add("1");
set.add("2");
set.remove("2");
set.add("3");
System.out.println(JSON.toJSONString(set));
}
private final Set<T> tempSet;
private final Semaphore sem;
public BoundedHashSet(int size) {
this.tempSet = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(size);
}
public boolean add(T o) throws Exception {
sem.acquire();
boolean isAdd = false;
try{
isAdd = tempSet.add(o);
return isAdd;
}
finally {
if (!isAdd) {
sem.release();}}
}
public boolean remove(Object o) {
boolean isRemoved = tempSet.remove(o);
if (isRemoved) {
sem.release();}
return isRemoved;}
}
这里例子实现了有界阻塞的HashSet。只允许这个HashSet存放两个元素,如果想存第三个元素,必须等到有人把HashSet中的元素remove掉。
每次add之前先申请一个许可,如果能申请到,则正常添加元素。申请不到,则acquire()方法会一直阻塞。remove操作里面,则有一个释放许可的操作。
Semaphore使用事例
21 使用信号量解决死锁问题?
public class BadLockTest { protected Object obj1 = new Object(); protected Object obj2 = new Object(); protected ExecutorService executorService = Executors.newCachedThreadPool(); protected Task1 test1=new Task1(); protected Task2 test2=new Task2(); public static void main(String[] args) { BadLockTest test = new BadLockTest(); for(int i=0;i<50;i++){ test.test1.setCount(i); test.test2.setCount(i); test.executorService.execute(test.test1); test.executorService.execute(test.test2); } } class Task1 implements Runnable { public int count; public void setCount(int count){ this.count=count; } @Override public void run() { synchronized (obj1) { System.out.println("task1得到obj1对象锁"+count); synchronized (obj2) { System.out.println("task1得到obj2对象锁"+count);}} } } class Task2 implements Runnable { public int count; public void setCount(int count){ this.count=count; } @Override public void run() { synchronized (obj2) { System.out.println("task1得到obj1对象锁"+count); synchronized (obj1) { System.out.println("task1得到obj2对象锁"+count);}} }} } 得到结果: task1得到obj1对象锁1 task1得到obj1对象锁1
可从结果就知道已经发生了死锁。
信号量可以控制资源能被多少线程访问,这里我们指定只能被一个线程访问,就做到了类似锁住。
而信号量可以指定去获取的超时时间,我们可以根据这个超时时间,去做一个额外处理。
对于无法成功获取的情况,一般就是重复尝试,或指定尝试的次数,也可以马上退出。
public class BadLockTest { protected ExecutorService executorService = Executors.newCachedThreadPool(); protected Task1 test1=new Task1(); protected Task2 test2=new Task2(); protected Semaphore s1=new Semaphore(1); protected Semaphore s2=new Semaphore(1); public static void main(String[] args) { BadLockTest test = new BadLockTest(); for(int i=0;i<50;i++){ test.test1.setCount(i); test.test2.setCount(i); test.executorService.execute(test.test1); test.executorService.execute(test.test2);} } class Task1 implements Runnable { public int count; public void setCount(int count){ this.count=count; } public void run() { try { if(s2.tryAcquire(1, TimeUnit.SECONDS)){ System.out.println("task1得到obj1对象锁"+count); if(s1.tryAcquire(1, TimeUnit.SECONDS)){ System.out.println("task1得到obj2对象锁"+count);} } s2.release(); s1.release();} }} class Task2 implements Runnable { public int count; public void setCount(int count){ this.count=count; } @Override public void run() { // synchronized (obj2) { try { if(s1.tryAcquire(1, TimeUnit.SECONDS)){ System.out.println("task2得到obj1对象锁"+count); if(s2.tryAcquire(1, TimeUnit.SECONDS)){ System.out.println("task2得到obj2对象锁"+count);} } s1.release(); s2.release();} }} } 结果: task1得到obj2对象锁49 task2得到obj2对象锁49
22. 线程之Semaphore生产消费模型
示例:假设当前有5辆车,10个司机,每个司机轮流用车。 public class Semaphore_Test { // 同时只能有5个线程访问某个资源车 private static Semaphore semaphore = new Semaphore(5); public static void main(String[] args) { // 10个司机 for (int i = 1; i <= 10; i++) { (new Driver(i)).start(); } } static class Driver extends Thread { private int i; public Driver(int i) { this.i = i; } public void run() { try { semaphore.acquire(); System.out.println("Driver " + i + " is using car"); Thread.sleep(1000); System.out.println("Driver " + i + " return back car"); semaphore.release();} }} } 可能的输出如下: Driver 1 is using car Driver 6 is using car Driver 5 is using car Driver 2 is using car Driver 3 is using car Driver 3 return back car Driver 5 return back car Driver 4 is using car Driver 2 return back car Driver 1 return back car Driver 6 return back car Driver 9 is using car Driver 8 is using car Driver 7 is using car Driver 10 is using car Driver 4 return back car Driver 10 return back car Driver 8 return back car Driver 7 return back car Driver 9 return back
3. Semaphore的使用案例(生产和消费事例)?
需求:使用2个线程,分别代表:生产者、消费者。让他们并发的去生产、消费产品,生产的总数是不能超过N的。
实现思路:
这里使用是使用信号量去控制线程的生产消费,通过释放令牌的形式去控制生产者消费者的上限。使用互斥锁保证每次最多只有一个角色去修改共享变量。
/** * 生产者消费者模型 */ public class ProducerConsumerProblem { private static final int N = 10; //初始容量 /*** * full 产品容量 * empty 空余容量 * mutex 读写锁 */ private static Semaphore full, empty, mutex; //记录当前的产品数量 private static volatile int count = 0; static { /** * empty 初始化有N个空余位置放置产品 * mutex 初始化每次最多只有一个线程可以读写 */ full = new Semaphore(0); empty = new Semaphore(N); mutex = new Semaphore(1); } public static void main(String[] args) { new Thread(new Producer()).start(); new Thread(new Consumer()).start(); } static class Producer implements Runnable { @Override public void run() { while (true) { try { empty.acquire();//等待空位 mutex.acquire();//等待读写锁 count++; out.println("生产者生产了一个,还剩:" + count); mutex.release();//释放读写锁 full.release();//放置产品 //随机休息一段时间,让生产者线程有机会抢占读写锁 Thread.sleep(((int) Math.random()) % 10); } } } } static class Consumer implements Runnable { @Override public void run() { while (true) { try { full.acquire();//等待产品 mutex.acquire();//等待读写锁 count--; out.println("消费者消费了一个,还剩:" + count); mutex.release();//释放读写锁 empty.release();//释放空位 // 随机休息一段时间,让消费者线程有机会抢占读写锁 Thread.sleep(((int) Math.random()) % 10); } } }} }
4.java信号量解决生产消费问题
信号量的基本原理: 简单点理解,信号量可看成一个整形量,表示可用资源的个数,使用资源时信号量-1,释放资源时信号量+1,以生产消费为例:
1. 假设此时缓冲区已满,sem=0,表示当前空的缓冲区个数为0
2. 生产者P1生产, sem=-1,陷入阻塞
3. 生产者P2生产,sem=-2,陷入阻塞
4. 消费者C1消费,sem=-1,唤醒P1
5. 消费者C2消费,sem=0,唤醒P2
归纳:
信号量的值可以反映有多少线程正在等待资源,信号量<0,表示有线程因无资源而陷入等待,如sem=-2,就表示有2个生产者陷入阻塞;
消费时sem++, 执行后若sem<=0, 表示之前有线程阻塞,需要唤醒,如上例中唤醒P1,P2。
信号量个数与初值的选择:
生产消费问题中,需要判断空和满 2个状态。 首先需要2个信号量, empty表示空缓冲区个数,full表示满缓冲区个数,
初值empty=缓冲区大小,full=0, 表示初始状态缓冲区全空,满的缓冲区个数为0。还需要信号量mutex=1保证互斥,初值为1为什么可保持互斥请看P,V操作。
引入P,V操作:
P:表示申请资源。 V:表示释放资源。(P,V操作都是原子性的)
P(semaphore s) {
s.value--;
if (s.value < 0)
当前线程阻塞
}
V(semaphore s) {
s.value++;
if (s.value <= 0)
唤醒等待在该资源上的线程
}
P,V解决生产消费
semaphore full = 0;
semaphore empty = BUFF_SIZE;
semaphore mutex = 1;
Producer() {
P(empty);//要生产,则耗费空资源,empty--
P(mutex);//保证互斥
生产
.....
V(mutex);
V(full);//生产完成则产生1个满资源,full++
}
Consumer() {
P(full);
P(mutex);
消费
...
V(mutex);
V(empty);
}
完整的java代码
java.util.concurrent.Semaphore,java中已实现好了Semaphore这个类,可在构造方法中设定初值,require和release方法对应P,V操作
public class SynStack {
private char[] data = new char[6];
int cnt=0;
Semaphore empty = new Semaphore(data.length);//空信号量
Semaphore full = new Semaphore(0);//满信号量
Semaphore mutex = new Semaphore(1);//互斥信号量
//生产
public void push(char ch){
try {
empty.acquire();
mutex.acquire();
data[cnt]=ch;
cnt++;
System.out.println("生产线程,生产第"+cnt+"个产品,此产品是"+ch);
}finally{
mutex.release();
full.release();
}
}
//消费
public char pop(){
char ch=' ';
try {
full.acquire();
mutex.acquire();
ch=data[cnt-1];
System.out.println("消费线程,消费第"+cnt+"个产品,此产品是"+ch);
--cnt;
}finally{
mutex.release();
empty.release();
}
return ch;
}
}
public class Producer implements Runnable{
private SynStack ss =null;
public Producer(SynStack ss){
this.ss = ss;
}
@Override
public void run() {
char ch;
for(int i=0;i<15;++i){
ch =(char)('a'+i);
ss.push(ch);
/*try {
Thread.sleep(1000);
}*/
}
}
}
public class Consumer implements Runnable{
private SynStack ss = null;
public Consumer(SynStack ss){
this.ss = ss;
}
@Override
public void run() {
for(int i=0;i<15;++i){
ss.pop();
}
}
}
public class Test {
public static void main(String[] args) {
SynStack ss = new SynStack();
Producer producer= new Producer(ss);
Consumer consumer =new Consumer(ss);
Thread threadP = new Thread(producer);
Thread threadC = new Thread(consumer);
//Thread threadC2= new Thread(consumer);
threadP.start();
threadC.start();
//threadC2.start();
}
}
定用一个整数表示信号量,然后有一个增加操作和一个减少操作,如下
class Semaphore {
private volatile int permit;
public Semaphore(int permit) {
this.permit = permit;
}
public acquire() {
if (permit <= 0) {//等待
}
//执行permit--操作
}
public release() {
//执行permit++ 操作
if(permit > 0){
//唤醒
}
}
}
这里有两点需要说明:这里先用一个二进制信号量来等效互斥操作;
由于信号量只能通过0值来进行阻塞和唤醒,所以这里必须使用两个信号量来模拟容器空和容器满两种状态
public class Cache { private int cacheSize = 0; public Semaphore mutex; public Semaphore empty; //保证了容器空的时候(empty的信号量<=0), 消费者等待 public Semaphore full; //保证了容器满的时候(full的信号量 <= 0),生产者等待 public Cache(int size) { mutex = new Semaphore(1); //二进制信号量,表示互斥锁 empty = new Semaphore(size); full = new Semaphore(0); } public int getCacheSize() throws InterruptedException { return cacheSize; } public void produce() throws InterruptedException { empty.acquire(); // 消耗一个空位 mutex.acquire(); cacheSize++; System.out.println("生产了一个产品, 当前产品数为" + cacheSize); mutex.release(); full.release(); // 增加了一个产品 } public void consume() throws InterruptedException { full.acquire(); // 消耗了一个产品 mutex.acquire(); cacheSize--; System.out.println("消费了一个产品, 当前产品数为" + cacheSize); mutex.release(); empty.release(); // 增加了一个空位 } } public class Consumer implements Runnable { private Cache cache; public Consumer(Cache cache) { this.cache = cache; } @Override public void run() { while (true) { try { cache.consume(); Thread.sleep(100); } } } } public class Producer implements Runnable { private Cache cache; public Producer(Cache cache) { this.cache = cache; } @Override public void run() { while (true) { try { cache.produce(); Thread.sleep(100); } } } } public class Main { public static void main(String[] args) { Cache cache = new Cache(10); Producer p = new Producer(cache); Consumer c = new Consumer(cache); int producerCount = 4, consumerCount = 4; for (int i = 0; i < producerCount; i++) { new Thread(p).start(); } for (int i = 0; i < consumerCount; i++) { new Thread(c).start(); } } }
看过了信号量,我们看看信号。 用Java"信号"实现的生产者和消费问题,如下:
public class TestSignal { static Monitor monitor = new Monitor(); static class Producer implements Runnable { static int num = 1; @Override public void run() { while (true) { try { monitor.insert(num); System.out.println("生产物品" + num); num++; Thread.sleep(100);} } } } static class Consumer implements Runnable { @Override public void run() { while (true) { try { System.out.println("消费物品" + monitor.remove()); Thread.sleep(500);} } } } static class Monitor {//管程,只能有一个线程占用 private final int capacity = 10; private int insertIndex = 0; //仓库中当前可以放置物品的位置 private int removeIndex = 0; //仓库中当前可以拿走物品的位置 private final Object[] items = new Object[capacity]; //仓库中的所有物品 int count = 0; //仓库中的现有物品数 //向仓库中放置物品 public synchronized void insert(Object item) throws InterruptedException { //当仓库已满时,挂起生产线程 if (count == capacity) { wait(); } items[insertIndex++] = item; if (insertIndex == capacity) { insertIndex = 0; } count++; //当仓库由空变为不空时,唤起消费线程 if (count == 1) { notify(); } } //从仓库中拿走物品 public synchronized Object remove() throws InterruptedException { //当仓库没有物品时,挂起消费线程 if (count == 0) { wait(); } Object item = items[removeIndex++]; if (removeIndex == capacity) { removeIndex = 0; } count--; //当仓库由满变为不满时,唤起生产线程 if (count == capacity - 1) { notify(); } return item; } } public static void main(String[] args) { new Thread(new Producer()).start(); new Thread(new Consumer()).start(); } }
可以看到,该例子使用一个Monitor类实现仓库中放置和拿走物品的方法,该类相当于一个管程,只能保证同一时刻只能有一个线程使用该类。
具体实现是采用静态类和synchronized方法,保证当insert()调用时拥有Monitor类的类锁,使得remove()无法获得Monitor类的类锁,
同理,保证当remove()调用时拥有Monitor类的类锁,使得remove()无法被调用。里面实现的巧妙之处在于,当count为capacity时,我们会挂起生产进程,当count从capacity变为capacity - 1时,就会唤醒生产进程加入线程调度队列。
同理,当count为0时,我们会挂起消费进程,当count从0变为1时,就会唤醒消费进程加入线程调度队列。
这种方式的执行结果,与信号量的结果类似,不再列出来。
四. Semaphore源码
1.首先分析公平模式下的获取许可方法:
protected int tryAcquireShared(int acquires) { for (;;) { if (hasQueuedPredecessors()) return -1; int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } } public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
hasQueuedPredecessors方法表示的是是否有线程在等待许可,如果已经有线程在等待了,那么直接返回-1代表获取许可失败,否则再去获取,获取许可就是通过compareAndSetState方法来更新state的值。
1.2 非公平模式下的获取许可的方法:
非公平模式和公平模式区别:公平模式会考虑是否已经有线程在等待,而非公平模式会快速去竞争,不会考虑是否有线程在前面等待,
关于多个线程是如何去竞争共享变量而获得锁语义的内容参考文章Java同步框架AbstractQueuedSynchronizer。
final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
下面来分析一下上面分析两个方法是如何被调用的,上文中提到,我们是通过使用acquire方法来获得一个许可的,下面是Semaphore的acquire():
acquire方法调用了AQS基类的acquireSharedInterruptibly(),而acquireSharedInterruptibly()调用了其子类的tryAcquireShared(),对应了公平模式和非公平模式下的tryAcquireShared方法。
public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } public final void acquireSharedInterruptibly(int arg){if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
再来分析一下归还许可方法release:获取许可是通过减少state来完成的,而归还许可则是通过增加state来完成的,AQS通过维护一个共享变量state来实现多线程同步。
public void release() { sync.releaseShared(1); } public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } 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的工作原理,下面展示一个使用示例:
class Resource { public Object getResource() { return new Object(); } } class Pool { private static final int MAX_AVAILABLE = 100; private final int availableSemaphore; private Semaphore available; public Pool() { this(MAX_AVAILABLE); } public Pool(int available) { this(available, false); } public Pool(int available, boolean fairMode) { this.availableSemaphore = available; this.available = new Semaphore(available, fairMode); items = new Resource[availableSemaphore]; for (int i = 0; i < availableSemaphore; i ++) { items[i] = new Resource(); } used = new boolean[availableSemaphore]; } public int availableSemaphore() { return this.availableSemaphore; } public Object getItem() throws InterruptedException { available.acquire(); return getNextAvailableItem(); } public void putItem(Object x) { if (markAsUnused(x)) available.release(); } // Not a particularly efficient data structure; just for demo protected Object[] items; protected boolean[] used; private synchronized Object getNextAvailableItem() { for (int i = 0; i < MAX_AVAILABLE; ++i) { if (!used[i]) { used[i] = true; return items[i]; } } return null; // not reached } private synchronized boolean markAsUnused(Object item) { for (int i = 0; i < MAX_AVAILABLE; ++i) { if (item == items[i]) { if (used[i]) { used[i] = false; return true; } else return false; } } return false; } }
Pool类使用了Semaphore来管理一组许可,获得许可的线程可以获得一个Resource,而Resource是什么可以自定义,比如是数据库连接池。
调用Pool类的getItem可以获得一个许可,而调用putItem将许可归还给Pool,下面展示了Pool的简单使用示例:
需要注意的是,任何线程在获得许可之后,使用共享资源完毕都需要执行归还操作,否则会有线程一直在等待。
public class SemaphoreDemo {
public static void main(String ... args) {
Pool pool = new Pool();
Object resource = null;
try {
Resource r = (Resource) pool.getItem();
resource = r.getResource();
//do your biz
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
pool.putItem(resource); //release here
}
}
}