锁中使用多重状态

Java 9并发编程指南 目录

锁机制可能与一个或多个状态关联,这些状态定义在Condition接口里。目的是允许线程有权利控制锁,以及判断状态是否为true。如果是false,线程将被暂停直到其它线程唤醒它。Condition接口提供了暂停线程以及唤醒已暂停线程的解决办法。

生产者-消费者问题是并发编程中一个经典问题。如之前所述,在数据缓存区中,一个或多个生产者在缓存区保存数据,同时一个或多个消费者从缓存区里取出数据。

在本节中,通过使用锁机制和多重状态解决生产者-消费者问题。

准备工作

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

实现过程

通过如下步骤完成范例:

  1. 首先,模拟一个文本文件。创建名为FileMock的类,包含两个属性:名为content的字符串数组以及名为index的整型。分别存储文件内容和模拟文件的检索行:

    public class FileMock {
    	private String[] content;
    	private int index;
    
  2. 实现类构造函数,用随机字符初始化文件内容:

    	public FileMock(int size, int length) {
    		content = new String[size];
    		for(int i = 0; i < size ; i ++){
    			StringBuilder buffer = new StringBuilder(length);
    			for(int j = 0 ; j < length ; j ++){
    				int randomCharacter = (int)Math.random() * 255;
    				buffer.append((char)randomCharacter);
    			}
    			content[i] = buffer.toString();
    		}
    		index = 0 ;
    	}
    
  3. 实现hasMoreLines()方法,如果文件还有更多行需要处理则返回true,如果已经到达模拟文件结尾则返回false:

    	public boolean hasMoreLines(){
    		return index < content.length;
    	}
    
  4. 实现getLine()方法,返回index属性确定的行,增加属性值:

    	public String getLine(){
    		if(this.hasMoreLines()) {
    			System.out.println("Mock: " + (content.length - index));
    			return content[index++];
    		}
    		return null;
    	}
    
  5. 实现名为Buffer的类,实现生产者和消费者共享的缓存区:

  6. Buffer类包含六个属性:

    • 名为buffer的LinkedList属性,用来存储共享数据,例如:

      private final LinkedList<String> buffer;
      
    • 名为maxSize的整型,用来存储缓存区的大小,例如:

      private final int maxSize;
      
    • 名为lock的ReentrantLock对象,用来控制访问修改缓存区的代码块,例如:

      private final ReentrantLock lock;
      
    • 两个名为lines和space的Condition属性,例如:

      private final Condition lines;
      private final Condition space;
      
    • 名为pendingLines的布尔型,用来指出缓存区中是否存在线,例如:

      private boolean pendingLines;
      
  7. 实现类构造函数,初始化前面所有属性:

    	public Buffer(int maxSize){
    		this.maxSize = maxSize;
    		buffer = new LinkedList<>();
    		lock = new ReentrantLock();
    		lines = lock.newCondition();
    		space = lock.newCondition();
    		pendingLines = true;
    	}
    
  8. 实现insert()方法,用来接收字符串内容作为参数,尝试存储到缓存区中。首先,它得到锁的控制,然后检查缓存区中是否有空间。如果缓存区已满,此方法调用space参数的await()方法等待空闲空间。当其它线程调用space属性中的signal()或signalAll()方法时,此线程将被唤醒。一旦唤醒后,线程将通过lines属性将字符串存储到缓存区中并且调用signalAll()方法。过后将会看到,这个状态将唤醒所有等待缓存区内容的线程。为了使代码更加简洁,忽略了InterruptedException异常,在真正开发中,需要处理此异常:

    	public void insert(String line){
    		lock.lock();
    		try {
    			while(buffer.size() == maxSize) {
    					space.await();	
    			}
    			buffer.offer(line);
    			System.out.printf("%s : Inserted Line : %d\n", Thread.currentThread().getName(), buffer.size());
    			lines.signalAll();
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}finally{
    			lock.unlock();
    		}
    	}
    
  9. 实现get()方法,返回缓存区中存储的第一条内容。首先,它得到锁的控制,然后检查缓存区中是否有空间。如果缓存区为空,此方法调用lines属性中的await()方法等待缓存区中的内容。当其它线程调用lines属性中的signal()或signalAll()方法时,此线程将被唤醒。一旦唤醒后,此方法得到缓存区中的第一条内容,通过space属性调用signalAll()方法,返回字符串:

    	public String get(){
    		String line = null;
    		lock.lock();
    
    		try {
    			while((buffer.size() == 0) && (hasPendingLines())){
    				lines.await();
    			}
    			if(hasPendingLines()){
    				line = buffer.poll();
    				System.out.printf("%s: Line Readed: %d\n", Thread.currentThread().getName(), buffer.size());
    				space.signalAll();
    			}
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}finally{
    			lock.unlock();
    		}
    		return line;
    	}
    
  10. 实现setPendingLines()方法,设置pendingLines属性值。当不再有内容产生时,生产者将调用此方法:

    	public synchronized void setPendingLines(boolean pendingLines){
    		this.pendingLines = pendingLines;
    	}
    
  11. 实现hasPendingLines()方法,如果还有内容待处理则返回true,否则返回false:

    	public synchronized boolean hasPendingLines() {
    		return pendingLines || buffer.size()>0;
    	}
    
  12. 现在转到生产者,实现名为Producer的类并指定其实现Runnable接口:

    public class Producer implements Runnable{
    
  13. 定义两个属性,分别是FileMock类对象和Buffer类对象:

    	private FileMock mock;
    	private Buffer buffer;
    
  14. 实现类构造函数,初始化两个属性:

    	public Producer(FileMock mock, Buffer buffer){
    		this.mock = mock;
    		this.buffer = buffer;
    	}
    
  15. 实现run()方法,读取在FileMock对象中创建的所有内容,并且使用insert()方法将读取内容存储到缓存区中。一旦结束,使用setPendingLines()方法警示缓存区无法生成更多内容:

    	@Override
    	public void run() {
    		buffer.setPendingLines(true);
    		while(mock.hasMoreLines()){
    			String line = mock.getLine();
    			buffer.insert(line);
    		}
    		buffer.setPendingLines(false);
    	}
    
  16. 接着是消费者,实现名为Consumer的类并指定其实现Runnable接口:

    public class Consumer implements Runnable{
    
  17. 定义Buffer对象,实现类构造函数,初始化此对象:

    	private Buffer buffer;
    	public Consumer (Buffer buffer){
    		this.buffer = buffer;
    	}
    
  18. 实现run()方法,如果缓存区中存在内容,尝试获得一条字符串内容并处理:

    	@Override
    	public void run() {
    		while(buffer.hasPendingLines()){
    			String line = buffer.get();
    			processLine(line);
    		}
    	}
    
  19. 实现辅助方法processLine(),它只是将线程休眠10毫秒,用来模拟对字符串内容进行某种处理:

    	private void processLine(String line) {
    		try {
    			Random random = new Random();
    			Thread.sleep(random.nextInt(100));
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    
  20. 实现范例主类,创建名为Main的类,添加main()方法:

    public class Main {
    	public static void main(String[] args){
    
  21. 创建FileMock对象:

    		FileMock mock = new FileMock(100, 10);
    
  22. 创建Buffer对象:

    		Buffer buffer = new Buffer(20);
    
  23. 创建Producer对象以及运行它的线程:

    		Producer producer = new Producer(mock, buffer);
    		Thread producerThread = new Thread(producer, "Producer");
    
  24. 创建三个Consumer对象以及运行它们的线程:

    		Consumer consumers[] = new Consumer[3];
    		Thread consumersThreads[] = new Thread[3];
    		
    		for(int i = 0 ; i < 3; i ++){
    			consumers[i] = new Consumer(buffer);
    			consumersThreads[i] = new Thread(consumers[i], "Consumer " + i);
    		}
    
  25. 启动生产者和三个消费者线程:

    		producerThread.start();
    		for(int i = 0 ; i < 3; i ++){
    			consumersThreads[i].start();
    		}
    

工作原理

所有Condition对象都与锁有关,并且使用定义在Lock接口中的newCondition()方法创建。在使用一个状态进行所有操作之前,需要控制此状态关联的锁。所以具备状态的操作必须在线程内完成,这个线程调用Lock对象中的lock()方法来保持住锁,然后使用相同对象的unlock()方法释放锁。

当线程调用一个状态的await()方法,线程自动释放对锁的控制以便其它线程能够控制锁,既可以开始执行操作,也可以进入锁保护的其它临界区。

当线程调用状态的signal()或者signalAll()方法时,所有等待状态的线程将被唤醒,但这并不保证现在促使它们休眠的状态为true。所以必须在一个循环中调用await()方法,在状态为true循环无法结束。当状态为false时,必须再次调用await()方法。

务必谨慎使用await()和signal()方法,如果在状态中调用await()方法,并且在此状态中从不调用signal()方法,线程将会一直休眠下去。

调用await()方法后,正在休眠的线程会被中断,所以需要处理InterruptedException异常。

扩展学习

await()方法在Condition接口中还有其它使用形式,如下所示:

  • await(long time , TimeUnit unit):在这里,线程将会休眠直到:
    • 它被中断了
    • 状态中的其它线程调用signal()或者signalAll()方法
    • 设定的时间到了
    • TimeUnit是一个枚举类型的类,包含如下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS、和SECONDS
  • awaitUninterruptibly():不会被中断的线程将会休眠,直到其它线程调用signal()或者signalAll()方法。
  • awaitUntil(Date date):线程将会休眠直到:
    • 它被中断了
    • 状态中的其它线程调用signal()或者signalAll()方法
    • 设定的日期到了

可以通过读/写锁中的ReadLock和WriteLock使用状态。

更多关注

  • 本章中”锁同步代码块“和“读/写锁同步数据存取”小节。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值