一、使用信号量进行资源的并发访问控制
本实例使用一种二进制信号量。是一种比较特殊的信号量,用来保护对唯一共享资源的访问,因而内部计数器只有0 和1两个值。
本实例创建10个线程竞争一个打印机使用一个打印队列实现。
看代码
1、打印队列类
/**
*
* @author fcs
* @date 2015-4-19
* 描述:资源的并发访问控制
* 说明:信号量机制
*/
public class PrintQueue {
private final Semaphore semaphore;
//**对信号量进行初始化**
public PrintQueue(){
semaphore = new Semaphore(1,true); //true表示使用非公平模式竞争信号量保护的资源,默认使用公平模式
}
/**
*
* 作者:fcs
* 描述:打印方法,使用信号量进行资源的并发访问的控制
* 说明:
* 返回:
* 参数: document 传入打印的文档
* 时间:2015-4-19
*/
public void printJob(Object document){
try {
semaphore.acquire(); //获得信号量,该方法会抛出异常
long duration = (long)(Math.random()*10);
System.out.printf("%sL PrintQueue: print a Job during %d seconds",Thread.currentThread().getName(),duration);
Thread.sleep(duration); //随机睡眠duration时间
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
semaphore.release(); //释放信号量,当该代码被注释后,运行会出现死锁,导致线程饥饿,所以获取信号量后,完成任务一定要释放信号量
}
}
}
2.打印工作类
/**
*
* @author fcs
* @date 2015-4-19
* 描述:工作打印类
* 说明:将打印文档发送到打印机
*/
public class Job implements Runnable{
private PrintQueue printQueue;
//使用构造方法初始化打印队列
public Job(PrintQueue printQueue){
this.printQueue = printQueue;
}
//调用打印方法,执行打印任务,将打印信息输出到控制台
@Override
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());
}
}
3.测试类
/**
*
* @author fcs
* @date 2015-4-19
* 描述:创建十个线程执行打印任务,但是共享一个打印机,
* 并且使用信号量机制实现资源的并发访问控制
*
* 这里使用的是多线程并发竞争的公平模式,所以JVM会按照等待时间最长的线程
* 来选择执行,可以信号量的构造函数中传入参数进行修改为非公平模式。
*
* 说明:
*/
public class Main {
public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread [] thread = new Thread[10];
for(int i =0 ;i< 10;i++){
thread[i] = new Thread(new Job(printQueue),"Thread"+i);
}
for(int i = 0;i<10;i++){
thread[i].start();
}
}
}
4.运行效果:
5.当没有只有获取信号量的时候没有释放信号量会出现什么状况呢?
在printJob(Object document)该方法中将信号量释放的代码注释重新运行Main测试类就会发现下面的现象:
分析:在非公平模式下第一个获取信号量的线程在完成打印工作后没有释放信号量,导致信号量从1—>0之后没有变化,让其他线程以为信号量一直没有释放,所以其他线程会一直等待,导致线程没有获得资源而处于线程饥饿状态。当然这是一种线程失败的状态。
二、资源的多副本的并发访问控制
1.本范例演示通过信号量用来保护一个资源的多个副本,或者被多个线程同时执行的临界区。相当于保护某一类资源。
本例中使用信号量保护三个打印机。
打印机队列类
package cn.fans.chapter3.two;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
*
* @author fcs
* @date 2015-4-19
* 描述:资源的多副本的并发访问控制
*
* 说明:修改one包中的部分代码即可
* 这里使用信号量的机制实现对多个资源的并发访问控制
*/
public class PrintQueue {
private final Semaphore semaphore; //声明信号量
private boolean freePrinters[]; //存放打印机的状态,即空闲或者正在打印。
@SuppressWarnings("unused")
private Lock lockPrinters; //用锁的方式保护对上面数组的同步访问
public PrintQueue(){
semaphore = new Semaphore(3); //信号量初始化为3,表示有三个共享资源
freePrinters = new boolean[3];
for(int i =0 ;i< 3;i++){
freePrinters[i] = true;
}
lockPrinters = new ReentrantLock();
}
//执行打印
public void printJob(Object object){
try {
semaphore.acquire(); //获得信号量,抛出异常,要捕获处理
int assingedPrinter = getPrinter();
long duration = (int)(Math.random()*10);
System.out.printf("%s: printQueue: Printing a Job in Printer %d during %d seconds\n",Thread.currentThread().getName(),assingedPrinter,duration);
Thread.sleep(duration); //随机等待一段时间
freePrinters[assingedPrinter] = true; //打印完毕后将该编号的打印机状态设置为可用
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
semaphore.release(); //注意没有释放信号量的话则会死锁,只有其中3个线程执行打印任务后其他线程就无法打印了
}
}
//该方法查找在指定时间内可用的打印机,如果有则返回打印机序号,否则返回-1
private int getPrinter(){
int ret = -1;
try {
lockPrinters.lock(); //获取锁
for(int i =0;i<freePrinters.length;i++){
if(freePrinters[i]){
ret = i;
freePrinters[i] = false;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}finally{
lockPrinters.unlock(); //释放锁,返回获得的打印机编号
}
return ret;
}
}
打印Job类和Main测试类使用第一个,不用修改。
这里使用了公平模式进行测试的。
三、对信号量使用的总结
1.信号量(Semaphore): 一种计数器,用来保护一个或者多个共享资源的访问,通常用于限制可以访问某些资源的线程数目。是并发编程的一种基础工具。
主要方法:
acquire():通过该方法获得信号量,如果没有可用资源的话,或者没有获得许可的话,就会阻塞。
release():通过该方法释放信号量,也叫释放一个许可,相当于释放一个可用资源,
如果任意线程试图获得许可,则选中一个线程并将刚刚释放的许可给予该线程,然后针对线程安排目的启动(或者再启动)该线程
acquiredUninterruptibly(): 内部实现原理与acquire()方法一样,当信号量的内部计数器变成0的时候,信号量将阻塞线程直到其被释放。
线程在被阻塞的这段时间里,可能会被中断,从而导致acquire方法抛出被中断异常.而该方法会忽略线程的中断并且不会抛出任何异常。
tryacquire(): 这个方法试图获得信号量,如果能获得就返回true,如果不能就返回false,从而避开线程的阻塞和等待信号量的释放,我们可以根据返回值是true,还是
false,来做出恰当的处理。
信号量机制也是有公平性的。通过该构造参数的第二个参数可以设置公平性,如果是false表示创建的信号量是非公平模式的,如果是true就是公平模式,默认是非公平模式的
案例演示说明:
1.资源的并发访问
java语句中的信号量,如果线程要访问一个共享资源,必须先获得信号量,如果信号量的内部计数器大于0,信号量将减1,然后允许访问这个共享资源。
计数器大于0则意味着有可以使用的资源,因此线程将被允许使用其中一个资源。
如果信号量的计数器等于0,信号量将会把线程置入休眠直到计数器大于0.线程要使用共享资源就要等待。信号量也可以理解为当前可用资源的数目。
当线程使用完某个共享资源的时候,信号量必须被释放,以便其他线程能够访问共享资源。释放操作将使信号量的内部计数器加1.
这个案例使用的是一种特殊的信号量机制,也就是二进制信号量,用来保护对唯一共享资源的访问。,内部计数器只有0和1两个值。
信号量的实现与锁的实现有些不同,信号量可以由线程释放“锁”,而不是由所有者释放锁,因为信号量没有所有权的概念,
在某些专门的上下文(如死锁恢复中会很有用)。信号量也具有锁的公平模式和非公平模式,使用一个布尔值表示是否使用非公平模式
内存一致性效果:线程中调用“释放”方法(比如 release())之前的操作 happen-before 另一线程中紧跟在成功的“获取”方法(比如 acquire())之后的操作。
2.资源的多副本的并发访问控制
1.信号量可以用来保护一个资源的多个副本,或者被多个线程同时执行的临界区,这个多个副本的意思就是某一类资源的总称。
这里是一个范例:一个打印队列,将被三个不同的打印机使用。实际上就是3个打印机被10个线程使用。
acquire(),acquireUniterruptibly(),tryAcquire(),release()方法都有另外一种实现形式,提供了一个int类型的参数,这个参数声明了线程试图获取或者释放的
资源数目,也就是这个线程想要在信号量内部计数器上删除或者增加的数目。
对acquire的三个形式的方法来说,如果计数器的值少于参数对应的值,那么线程将被阻塞直到计数器重新累加到或者超过该这个值。