锁机制可能与一个或多个状态关联,这些状态定义在Condition接口里。目的是允许线程有权利控制锁,以及判断状态是否为true。如果是false,线程将被暂停直到其它线程唤醒它。Condition接口提供了暂停线程以及唤醒已暂停线程的解决办法。
生产者-消费者问题是并发编程中一个经典问题。如之前所述,在数据缓存区中,一个或多个生产者在缓存区保存数据,同时一个或多个消费者从缓存区里取出数据。
在本节中,通过使用锁机制和多重状态解决生产者-消费者问题。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤完成范例:
-
首先,模拟一个文本文件。创建名为FileMock的类,包含两个属性:名为content的字符串数组以及名为index的整型。分别存储文件内容和模拟文件的检索行:
public class FileMock { private String[] content; private int index;
-
实现类构造函数,用随机字符初始化文件内容:
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 ; }
-
实现hasMoreLines()方法,如果文件还有更多行需要处理则返回true,如果已经到达模拟文件结尾则返回false:
public boolean hasMoreLines(){ return index < content.length; }
-
实现getLine()方法,返回index属性确定的行,增加属性值:
public String getLine(){ if(this.hasMoreLines()) { System.out.println("Mock: " + (content.length - index)); return content[index++]; } return null; }
-
实现名为Buffer的类,实现生产者和消费者共享的缓存区:
-
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;
-
-
实现类构造函数,初始化前面所有属性:
public Buffer(int maxSize){ this.maxSize = maxSize; buffer = new LinkedList<>(); lock = new ReentrantLock(); lines = lock.newCondition(); space = lock.newCondition(); pendingLines = true; }
-
实现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(); } }
-
实现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; }
-
实现setPendingLines()方法,设置pendingLines属性值。当不再有内容产生时,生产者将调用此方法:
public synchronized void setPendingLines(boolean pendingLines){ this.pendingLines = pendingLines; }
-
实现hasPendingLines()方法,如果还有内容待处理则返回true,否则返回false:
public synchronized boolean hasPendingLines() { return pendingLines || buffer.size()>0; }
-
现在转到生产者,实现名为Producer的类并指定其实现Runnable接口:
public class Producer implements Runnable{
-
定义两个属性,分别是FileMock类对象和Buffer类对象:
private FileMock mock; private Buffer buffer;
-
实现类构造函数,初始化两个属性:
public Producer(FileMock mock, Buffer buffer){ this.mock = mock; this.buffer = buffer; }
-
实现run()方法,读取在FileMock对象中创建的所有内容,并且使用insert()方法将读取内容存储到缓存区中。一旦结束,使用setPendingLines()方法警示缓存区无法生成更多内容:
@Override public void run() { buffer.setPendingLines(true); while(mock.hasMoreLines()){ String line = mock.getLine(); buffer.insert(line); } buffer.setPendingLines(false); }
-
接着是消费者,实现名为Consumer的类并指定其实现Runnable接口:
public class Consumer implements Runnable{
-
定义Buffer对象,实现类构造函数,初始化此对象:
private Buffer buffer; public Consumer (Buffer buffer){ this.buffer = buffer; }
-
实现run()方法,如果缓存区中存在内容,尝试获得一条字符串内容并处理:
@Override public void run() { while(buffer.hasPendingLines()){ String line = buffer.get(); processLine(line); } }
-
实现辅助方法processLine(),它只是将线程休眠10毫秒,用来模拟对字符串内容进行某种处理:
private void processLine(String line) { try { Random random = new Random(); Thread.sleep(random.nextInt(100)); } catch (InterruptedException e) { e.printStackTrace(); } }
-
实现范例主类,创建名为Main的类,添加main()方法:
public class Main { public static void main(String[] args){
-
创建FileMock对象:
FileMock mock = new FileMock(100, 10);
-
创建Buffer对象:
Buffer buffer = new Buffer(20);
-
创建Producer对象以及运行它的线程:
Producer producer = new Producer(mock, buffer); Thread producerThread = new Thread(producer, "Producer");
-
创建三个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); }
-
启动生产者和三个消费者线程:
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使用状态。
更多关注
- 本章中”锁同步代码块“和“读/写锁同步数据存取”小节。