简介
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。很多年以来,我都觉得从字面上很难理解Semaphore所表达的含义,只能把它比作是控制流量的红绿灯,比如XX马路要限制流量,只允许同时有一百辆车在这条路上行使,其他的都必须在路口等待,所以前一百辆车会看到绿灯,可以开进这条马路,后面的车会看到红灯,不能驶入XX马路,但是如果前一百辆中有五辆车已经离开了XX马路,那么后面就允许有5辆车驶入马路,这个例子里说的车就是线程,驶入马路就表示线程在执行,离开马路就表示线程执行完成,看见红灯就表示线程被阻塞,不能执行。
示例
下面用一个示例演示,假设有N个并发线程都要打印文件,但是打印机只有1台,先来一个打印队列类:
import java.util.concurrent.Semaphore;
public class PrintQueue {
private final Semaphore semaphore;
public PrintQueue() {
semaphore = new Semaphore(1);//限定了共享资源只能有1个(相当于只有一把钥匙)
}
public void printJob(Object document) {
try {
semaphore.acquire();//取得对共享资源的访问权(即拿到了钥匙))
long duration = (long) (1 + Math.random() * 10);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), duration);
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();//钥匙用完了,要还回去,这样其它线程才能继续有序的拿到钥匙,访问资源
}
}
}
由于是在多线程环境中,真正运行的作业处理,得继承自Runnable(或Callable)
public class Job implements Runnable {
private PrintQueue printQueue;
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
public void run() {
System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());
printQueue.printJob(new Object());
System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
}
}
好了,测试一把:
public class Main {
public static void main(String args[]) {
PrintQueue printQueue = new PrintQueue();
int threadCount = 3;
Thread thread[] = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
thread[i] = new Thread(new Job(printQueue), "Thread" + i);
}
for (int i = 0; i < threadCount; i++) {
thread[i].start();
}
}
}
输出:
Thread0: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread0: PrintQueue: Printing a Job during 7 seconds
Thread0: The document has been printed
Thread2: PrintQueue: Printing a Job during 5 seconds
Thread2: The document has been printed
Thread1: PrintQueue: Printing a Job during 1 seconds
Thread1: The document has been printed
从输出上看,线程0打印完成后,线程2才开始打印,然后才是线程1,没有出现一哄而上,抢占打印机的情况。这样可能没啥感觉,我们把PrintQueue如果去掉Semaphore的部分,变成下面这样:
public class PrintQueue {
//private final Semaphore semaphore;
public PrintQueue() {
//semaphore = new Semaphore(1);//限定了共享资源只能有1个(相当于只有一把钥匙)
}
public void printJob(Object document) {
try {
//semaphore.acquire();//取得对共享资源的访问权(即拿到了钥匙))
long duration = (long) (1 + Math.random() * 10);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), duration);
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//semaphore.release();//钥匙用完了,要还回去,这样其它线程才能继续有序的拿到钥匙,访问资源
}
}
}
这回的输出:
Thread0: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread2: PrintQueue: Printing a Job during 4 seconds
Thread1: PrintQueue: Printing a Job during 8 seconds
Thread0: PrintQueue: Printing a Job during 1 seconds
Thread0: The document has been printed
Thread2: The document has been printed
Thread1: The document has been printed
可以发现,3个线程全都一拥而上,同时开始打印,也不管打印机是否空闲,实际应用中,这样必然出问题。
好的,继续,突然有一天,公司有钱了,又买了2台打印机,这样就有3台打印机了,这时候怎么办呢?简单的把PrintQueue构造器中的
public PrintQueue() {
semaphore = new Semaphore(3);
}
就行了吗?仔细想想,就会发现问题,代码中并没有哪里能告诉线程哪个打印机正在打印,哪个打印机当前空闲,所以仍然有可能出现N个线程(N<=3)同时抢一台打印机的情况(即:如果把控制权当成钥匙的话,相当于有可能3个人各领取到了1把钥匙,但是这3把钥匙是相同的,3个人都看中了同一个箱子,都要用手中的钥匙去抢着开箱)。
所以得改进一下:
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class PrintQueue {
private boolean freePrinters[];//用来存放打印机的状态,true表示空闲,false表示正在打印
private Lock lockPrinters;//增加了锁,保证多个线程,只能获取得锁,才能查询哪台打印机空闲的
private final Semaphore semaphore;
public PrintQueue() {
int printerNum = 3;//假设有3台打印机
semaphore = new Semaphore(printerNum);
freePrinters = new boolean[printerNum];
for (int i = 0; i < printerNum; i++) {
freePrinters[i] = true;//初始化时,默认所有打印机都空闲
}
lockPrinters = new ReentrantLock();
}
private int getPrinter() {
int ret = -1;
try {
lockPrinters.lock();//先加锁,保证1次只能有1个线程来获取空闲的打印机
for (int i = 0; i < freePrinters.length; i++) {
//遍历所有打印机的状态,发现有第1个空闲的打印机后,领取号码,
// 并设置该打印机为繁忙状态(因为马上就要用它)
if (freePrinters[i]) {
ret = i;
freePrinters[i] = false;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//最后别忘记了解锁,这样后面的线程才能上来领号
lockPrinters.unlock();
}
return ret;
}
public void printJob(Object document) {
try {
semaphore.acquire();
int assignedPrinter = getPrinter();//领号
long duration = (long) (1 + Math.random() * 10);
System.out.printf("%s: PrintQueue: Printing a Job in Printer%d during %d seconds\n", Thread.currentThread().getName(),
assignedPrinter, duration);
Thread.sleep(duration);
freePrinters[assignedPrinter] = true;//打印完以后,将该打印机重新恢复为空闲状态
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
}
测试一下,这回把线程数增加到5,输出结果类似下面这样:
Thread0: Going to print a job
Thread4: Going to print a job
Thread3: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread4: PrintQueue: Printing a Job in Printer1 during 7 seconds
Thread0: PrintQueue: Printing a Job in Printer0 during 4 seconds
Thread3: PrintQueue: Printing a Job in Printer2 during 8 seconds
Thread0: The document has been printed
Thread2: PrintQueue: Printing a Job in Printer0 during 1 seconds
Thread2: The document has been printed
Thread4: The document has been printed
Thread1: PrintQueue: Printing a Job in Printer0 during 1 seconds
Thread3: The document has been printed
Thread1: The document has been printed
从输出结果可以看出,一次最多只能有3个线程使用这3台打印机,而且每个线程使用的打印机互不冲突,打印完成后,空闲的打印机会给其它线程继续使用,继续折腾,如果把getPrinter()中加锁的部分去掉,即:
private int getPrinter() {
int ret = -1;
try {
//lockPrinters.lock();//先加锁,保证1次只能有1个线程来获取空闲的打印机
for (int i = 0; i < freePrinters.length; i++) {
//遍历所有打印机的状态,发现有第1个空闲的打印机后,领取号码,
// 并设置该打印机为繁忙状态(因为马上就要用它)
if (freePrinters[i]) {
ret = i;
freePrinters[i] = false;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//最后别忘记了解锁,这样后面的线程才能上来领号
//lockPrinters.unlock();
}
return ret;
}
再跑一下,结果如何,为了放大冲突,这回开到15个线程来抢3台打印机,输出如下:
Thread0: Going to print a job
Thread14: Going to print a job
Thread13: Going to print a job
Thread12: Going to print a job
Thread11: Going to print a job
Thread10: Going to print a job
Thread9: Going to print a job
Thread8: Going to print a job
Thread7: Going to print a job
Thread6: Going to print a job
Thread5: Going to print a job
Thread4: Going to print a job
Thread3: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread0: PrintQueue: Printing a Job in Printer0 during 29 seconds
Thread14: PrintQueue: Printing a Job in Printer0 during 92 seconds
Thread13: PrintQueue: Printing a Job in Printer1 during 66 seconds
Thread0: The document has been printed
Thread12: PrintQueue: Printing a Job in Printer0 during 86 seconds
Thread13: The document has been printed
Thread11: PrintQueue: Printing a Job in Printer1 during 1 seconds
Thread11: The document has been printed
Thread10: PrintQueue: Printing a Job in Printer1 during 58 seconds
Thread14: The document has been printed
Thread9: PrintQueue: Printing a Job in Printer0 during 92 seconds
Thread12: The document has been printed
Thread8: PrintQueue: Printing a Job in Printer0 during 59 seconds
Thread10: The document has been printed
Thread7: PrintQueue: Printing a Job in Printer1 during 51 seconds
Thread8: The document has been printed
Thread6: PrintQueue: Printing a Job in Printer0 during 33 seconds
Thread7: The document has been printed
Thread5: PrintQueue: Printing a Job in Printer1 during 2 seconds
Thread9: The document has been printed
Thread3: PrintQueue: Printing a Job in Printer1 during 85 seconds
Thread4: PrintQueue: Printing a Job in Printer0 during 61 seconds
Thread5: The document has been printed
Thread6: The document has been printed
Thread2: PrintQueue: Printing a Job in Printer0 during 66 seconds
Thread4: The document has been printed
Thread1: PrintQueue: Printing a Job in Printer0 during 9 seconds
Thread1: The document has been printed
Thread3: The document has been printed
Thread2: The document has been printed
注意标粗的部分:Thread0与Thread14同时分配到了Printer0上了,出现了多个线程同时抢一个资源的情况。