控制并发访问资源副本
在本节中,学习如何使用Java语言提供的信号量机制。信号量是一个计数器,用来保护访问共享资源。
1965年,Edsger Dijkstray引入了信号量的概念,首次在THEOS操作系统中使用。
当线程想要访问共享资源时,首先需要识别信号量。如果信号量的内置计数器值大于0,则信号量减少计数器值,并且允许线程访问共享资源。值大于0的计数器表明有可以使用的空闲资源,所以线程能够访问使用这些资源。
否则如果计数器值等于0,信号量将让线程休眠直到计数大于0。值为0的计数器表明所有共享资源被其它线程使用着,所以线程想要使用其中一个资源,就必须等到它是空闲的。
当线程已经用完共享资源时,它必须释放信号量以便让其它线程能够访问资源。这个操作将增加信号量的内置计数器值。
在本节中,学习如何使用Semaphore类保护资源副本。在范例中,实现三个不同打印机的打印文件队列。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤完成范例:
-
创建名为PrintQueue的类实现打印队列:
public class PrintQueue {
-
这个类有三个私有属性。名为semaphore的Semaphore类型,名为freePrinters的布尔型队列,名为lockPrinters的Lock类型,如下代码所示:
private final Semaphore semaphore; private final boolean freePrinters[]; private final Lock lockPrinters;
-
实现类构造函数,初始化类的三个属性,如下代码所示:
public PrintQueue() { semaphore = new Semaphore(3); freePrinters = new boolean[3]; for ( int i = 0 ; i < 3 ; i ++){ freePrinters[i] = true; } lockPrinters = new ReentrantLock(); }
-
实现printJob()方法来模拟打印文件操作,接收名为document的对象作为参数:
public void printJob(Object document) {
-
首先printJob()方法调用acquire()方法来得到信号量的访问权。因为此方法会抛出InterruptedException异常,需要代码进行处理:
try { semaphore.acquire();
-
然后,得到分配打印任务的打印机的编号,使用getPrinter()私有函数:
int assignedPrinter = getPrinter();
-
接着,随机等待一段时间,模拟正在打印文件,输出打印过程:
long duration = (long)(Math.random() * 10); System.out.printf("%s - %s : PrintQueue : Printing a Job in Printer %d during %d seconds\n", new Date(), Thread.currentThread().getName(), assignedPrinter, duration); TimeUnit.SECONDS.sleep(duration);
-
最后通过调用release()方法释放信号量,设置打印机为空闲状态,并且在freePrinters队列中相应的索引赋值true:
freePrinters[assignedPrinter] = true; } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); }
-
接下来,实现getPrinter()方法,是返回整型且没有参数的私有方法:
private int getPrinter() {
-
首先,定义整型变量存储打印机索引:
int ret = -1;
-
然后,获得lockPrinters对象的访问权:
try{ lockPrinters.lock();
-
在freePrinters队列中找到第一个true值,将其索引保存为变量。然修改此索引值为false因为此打印机将被占用:
for(int i = 0 ; i < freePrinters.length ; i++){ if(freePrinters[i]){ ret = i; freePrinters[i] = false; break; } }
-
最后,释放lockPrinters对象,返回为true值的索引:
} catch(Exception e) { e.printStackTrace(); } finally { lockPrinters.unlock(); } return ret; }
-
接下来,创建名为Job的类,并指定其实现Runnable接口。此类实现给打印机传送文件的任务:
public class Job implements Runnable {
-
定义名为printQueue的PrintQueue对象:
private PrintQueue printQueue;
-
实现类构造函数,初始化类中定义的PrintQueue对象:
public Job(PrintQueue printQueue) { this.printQueue = printQueue; }
-
实现run()方法:
@Override public void run() {
-
首先,在控制台输出一条表示打印任务开始的信息:
System.out.printf("%s : Going to print a job\n", Thread.currentThread().getName());
-
然后,调用PrintQueue对象的printJob()方法:
printQueue.printJob(new Object());
-
最后,在控制台输出一条表示结束打印任务的信息:
System.out.printf("%s : The document has been printed\n", Thread.currentThread().getName()); }
-
接下来,实现主方法。创建一个包含main()方法的Main类:
public class Main { public static void main(String[] args) {
-
创建名为printQueue的PrintQueue对象:
PrintQueue printQueue = new PrintQueue();
-
创建12个线程,每个线程执行一个Job对象,向打印队列发送文件:
Thread[] threads = new Thread[12]; for (int i = 0; i < threads.length ; i ++){ threads[i] = new Thread(new Job(printQueue), "Thread" + i); }
-
最后,执行这些线程:
for (int i = 0; i < threads.length ; i ++){ threads[i].start(); }
工作原理
PrintQueue类的printJob()对象是范例的关键之处。当使用信号量来实现临界区并且保护访问共享资源时,必须使用此方法的三个步骤来实现:
- 首先,使用acquire()方法得到信息量。
- 然后,使用共享资源进行必要操作。
- 最后,使用release()方法释放信号量。
PrintQueue类的构造函数和Semaphore对象的初始化也是范例中的重点。在构造函数中传递参数值为3,说明正在创建保护三个资源的信号量。调用acquire()方法前三个线程会得到范例中临界区的访问权,而其它线程则被阻塞。当一个线程用完临界区并且释放信号量,另一个线程将得到信号量。
下图显示本范例在控制台输出的执行信息:
可以看到前三个打印任务在同一时间开始,然后当一个打印任务结束后,另一个才开始。
扩展学习
Semaphore类中的acquire()方法还有三种附加形式:
- acquireUninterruptibly():在acquire()方法中,当信号量的内置计数器值为0时,在信号量被释放前阻塞线程。在这期间线程可能会被中断,如果发生的话,方法会抛出InterruptedException异常。此方法的acquire操作将忽略线程中断,且不会抛出任何异常。
- tryAcquire():此方法尝试获得信号量。如果可以,返回true值。但是如果不能的话, 返回false值,而不是被阻塞并且等待信号量的释放。基于返回结果,有责任采取正确行动。
- tryAcquire(long timeout, TimeUnit unit):此方法与前一个方法功能相同,但是传递一个等待信号量释放的特定时间周期参数。如果时间段结束后,方法还没有的到信号量,则返回false。
acquire()、acquireUninterruptibly()、tryAcquire(),和realease()方法均有一种包含整型参数的附加形式。这个参数表示线程获得或者释放信号量的允许次数,换句话说,就是线程想要删除或者添加到信号量内部计数器的数字。
在不使用acquire()、acquireUninterruptibly()、tryAcquire()方法时,如果计数器值小于传参值时,线程将被阻塞直到计数器值不小于传参值。
信号量公允
公允概念是指能够让各种线程阻塞并且等待同步资源(例如,信号量)的释放,通过Java语言在所有类中使用。默认模式称为非公允模式。在此模式下,当同步资源被释放时,选择等待的一个线程并给予此资源,但是这种选择没有任何条件。另一方面,公允模式改变其行为并且选择等待时间最长的线程。
在其它类中,Semaphore类允许在构造函数中再传一个参数,此参数必须是布尔型。如果传递值为false,即创建将要在非公允模式下工作的信号量,与不使用此参数效果相同。如果传递值为true,则是创建公允模式下工作的信号量。
更多关注
- 第九章“测试并发应用”中的“监控Lock接口”小节。
- 第二章“基础线程同步”中的“锁同步代码块”小节。