控制并发访问资源副本

Java 9并发编程指南 目录

在本节中,学习如何使用Java语言提供的信号量机制。信号量是一个计数器,用来保护访问共享资源。

1965年,Edsger Dijkstray引入了信号量的概念,首次在THEOS操作系统中使用。

当线程想要访问共享资源时,首先需要识别信号量。如果信号量的内置计数器值大于0,则信号量减少计数器值,并且允许线程访问共享资源。值大于0的计数器表明有可以使用的空闲资源,所以线程能够访问使用这些资源。

否则如果计数器值等于0,信号量将让线程休眠直到计数大于0。值为0的计数器表明所有共享资源被其它线程使用着,所以线程想要使用其中一个资源,就必须等到它是空闲的。

当线程已经用完共享资源时,它必须释放信号量以便让其它线程能够访问资源。这个操作将增加信号量的内置计数器值。

在本节中,学习如何使用Semaphore类保护资源副本。在范例中,实现三个不同打印机的打印文件队列。

准备工作

本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。

实现过程

通过如下步骤完成范例:

  1. 创建名为PrintQueue的类实现打印队列:

    public class PrintQueue {
    
  2. 这个类有三个私有属性。名为semaphore的Semaphore类型,名为freePrinters的布尔型队列,名为lockPrinters的Lock类型,如下代码所示:

    	private final Semaphore semaphore;
    	private final boolean freePrinters[];
    	private final Lock lockPrinters;
    
  3. 实现类构造函数,初始化类的三个属性,如下代码所示:

    	public PrintQueue() {
    		semaphore = new Semaphore(3);
    		freePrinters = new boolean[3];
    		for ( int i = 0 ; i < 3 ; i ++){
    			freePrinters[i] = true;
    		}
    		lockPrinters = new ReentrantLock();
    	}
    
  4. 实现printJob()方法来模拟打印文件操作,接收名为document的对象作为参数:

    	public void printJob(Object document) {
    
  5. 首先printJob()方法调用acquire()方法来得到信号量的访问权。因为此方法会抛出InterruptedException异常,需要代码进行处理:

    		try {
    			semaphore.acquire();
    
  6. 然后,得到分配打印任务的打印机的编号,使用getPrinter()私有函数:

    			int assignedPrinter = getPrinter();
    
  7. 接着,随机等待一段时间,模拟正在打印文件,输出打印过程:

    			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);
    
  8. 最后通过调用release()方法释放信号量,设置打印机为空闲状态,并且在freePrinters队列中相应的索引赋值true:

    			freePrinters[assignedPrinter] = true;
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		} finally {
    			semaphore.release();
    		}
    
  9. 接下来,实现getPrinter()方法,是返回整型且没有参数的私有方法:

    	private int getPrinter() {
    
  10. 首先,定义整型变量存储打印机索引:

    		int ret = -1;
    
  11. 然后,获得lockPrinters对象的访问权:

    		try{
    			lockPrinters.lock();
    
  12. 在freePrinters队列中找到第一个true值,将其索引保存为变量。然修改此索引值为false因为此打印机将被占用:

                for(int i = 0 ; i < freePrinters.length ; i++){
                    if(freePrinters[i]){
                        ret = i;
                        freePrinters[i] = false;
                        break;
                    }
                }
    
  13. 最后,释放lockPrinters对象,返回为true值的索引:

    			} catch(Exception e) {
    			e.printStackTrace();
    		} finally {
    			lockPrinters.unlock();
    		}
    		return ret;
    	}
    
  14. 接下来,创建名为Job的类,并指定其实现Runnable接口。此类实现给打印机传送文件的任务:

    public class Job implements Runnable {
    
  15. 定义名为printQueue的PrintQueue对象:

    	private PrintQueue printQueue;
    
  16. 实现类构造函数,初始化类中定义的PrintQueue对象:

    	public Job(PrintQueue printQueue) {
    		this.printQueue = printQueue;
    	}
    
  17. 实现run()方法:

    	@Override
    	public void run() {
    
  18. 首先,在控制台输出一条表示打印任务开始的信息:

    		System.out.printf("%s : Going to print a job\n", Thread.currentThread().getName());
    
  19. 然后,调用PrintQueue对象的printJob()方法:

    		printQueue.printJob(new Object());
    
  20. 最后,在控制台输出一条表示结束打印任务的信息:

    		System.out.printf("%s : The document has been printed\n", Thread.currentThread().getName());
    	}
    
  21. 接下来,实现主方法。创建一个包含main()方法的Main类:

    public class Main {
    	public static void main(String[] args) {
    
  22. 创建名为printQueue的PrintQueue对象:

    		PrintQueue printQueue = new PrintQueue();
    
  23. 创建12个线程,每个线程执行一个Job对象,向打印队列发送文件:

    		Thread[] threads = new Thread[12];
    		for (int i = 0; i < threads.length ; i ++){
    			threads[i] = new Thread(new Job(printQueue), "Thread" + i);
    		}
    
  24. 最后,执行这些线程:

    		for (int i = 0; i < threads.length ; i ++){
    			threads[i].start();
    		}
    

工作原理

PrintQueue类的printJob()对象是范例的关键之处。当使用信号量来实现临界区并且保护访问共享资源时,必须使用此方法的三个步骤来实现:

  1. 首先,使用acquire()方法得到信息量。
  2. 然后,使用共享资源进行必要操作。
  3. 最后,使用release()方法释放信号量。

PrintQueue类的构造函数和Semaphore对象的初始化也是范例中的重点。在构造函数中传递参数值为3,说明正在创建保护三个资源的信号量。调用acquire()方法前三个线程会得到范例中临界区的访问权,而其它线程则被阻塞。当一个线程用完临界区并且释放信号量,另一个线程将得到信号量。

下图显示本范例在控制台输出的执行信息:
pics/03_01.jpg

可以看到前三个打印任务在同一时间开始,然后当一个打印任务结束后,另一个才开始。

扩展学习

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接口”小节。
  • 第二章“基础线程同步”中的“锁同步代码块”小节。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值