传统的线程通信
Object提供了三个方法wait(), notify(), notifyAll()在线程之间进行通信,以此来解决线程间执行顺序等问题。
- wait():释放当前线程的同步监视控制器,并让当前线程进入阻塞状态,直到别的线程发出notify将该线程唤醒。
- notify():唤醒在等待控制监视器的其中一个线程(随机)。只有当前线程释放了同步监视器锁(调用wait)之后,被唤醒的线程才有机会执行。
- notifyAll():与上面notify的区别是同时唤醒多个等待线程。
值得注意的是这三个方法是属于Object而不是属于Thread的,但是调用的时候必须用同步监视器来调用,
- 对于synchronized修饰的同步方法,由于方法所在类对象(this)就是同步监视器,因此可以直接在同步方法中调用这三个方法;
- 对于同步代码块,synchronized(obj) { ... },则需要用空号钟的obj来调用。
生产者-消费者问题模型
在经典的生产者-消费者问题中,需要使用线程通信来解决。
假设有这么一个场景,有一个线程需要存钱进一个账户,有多个线程需要从这个账户取钱,要求是每次必须先存钱之后才能取钱,而且取钱之后必须存钱,
存钱和取钱不能同时发生两次,而是要保持顺序不变,如何实现这个需求呢。
下面是用同步方法结合线程通信的方式来实现的思路,
- 首先在Account类中定义两个同步方法,deposit和draw用来确保存款和取款操作的原子性。
- 在Account类中定义用标识符flag, 由deposit和draw共用。初始值为false,表示只能存款。 如果为false,表示只能取款。
- 定义一个存款线程类,去调用Account类的同步方法deposit,在deposit中先对flag进行判断,如果不为false,则调用wait阻塞存款线程,等待取款线程发出notice。存款完成之后,将flag改为true.
- 定义一个取款线程类,去调用Account类的同步方法draw,在draw中先对flag进行判断,如果不为true,则调用wait阻塞取款线程,等待存线程发出notice。取款完成之后,将flag改为false.
- 定义测试类,同时启动一个(或多个)存款线程进行存款,同时启动多个取款线程去取款,存款(取款)线程之间不会有先后顺序,但是存款和取款直接会有严格的先后顺序,这就解决了生产者消费者问题
下面给出实现代码
在Account类中定义两个同步方法,draw和deposit
1 package threads.sync; 2 3 public class Account { 4 private String accountNo; 5 private double balance; 6 //if flag = false, means only deposit can be done 7 private boolean flag = false; 8 public Account() {} 9 10 11 public Account(String accountNo, double balance) { 12 this.accountNo = accountNo; 13 this.balance = balance; 14 } 15 16 public String getAccountNo() { 17 return accountNo; 18 } 19 20 public void setAccountNo(String accountNo) { 21 this.accountNo = accountNo; 22 } 23 24 public double getBalance() { 25 return balance; 26 } 27 28 public void setBalance(double balance) { 29 this.balance = balance; 30 } 31 32 33 public int hashCode() { 34 return accountNo.hashCode(); 35 } 36 37 public boolean equals(Object obj) { 38 if (this == obj) return true; 39 if (obj != null && obj.getClass() == Account.class) { 40 Account target = (Account)obj; 41 return target.getAccountNo().equals(accountNo); 42 } 43 return false; 44 } 45 46 public synchronized void draw(double drawAmount) { 47 try { 48 //if flag = false, means only deposit can be done, draw method will be blocked 49 if (!flag) { 50 wait(); 51 } else { 52 System.out.println(Thread.currentThread().getName() 53 + " draw money: " + drawAmount); 54 balance -= drawAmount; 55 System.out.println(" " 56 + " balance : " + balance); 57 flag = false; 58 notifyAll(); 59 } 60 } catch (InterruptedException ex) { 61 ex.printStackTrace(); 62 } 63 } 64 65 public synchronized void deposit(double depositAmount) { 66 try { 67 //if flag = false, means only draw can be done, deposit method will be blocked 68 if (flag) { 69 wait(); 70 } else { 71 System.out.println(Thread.currentThread().getName() 72 + " deposit money: " + depositAmount); 73 balance += depositAmount; 74 System.out.println(" " 75 + " balance : " + balance); 76 flag = true; 77 notifyAll(); 78 } 79 } catch (InterruptedException ex) { 80 ex.printStackTrace(); 81 } 82 } 83 84 }
定义一个存款线程类depositThread
1 package threads.sync; 2 3 public class DepositThread extends Thread { 4 private Account account; 5 private double depositAmount; 6 public DepositThread(String name, Account account, double depositAmount) { 7 super(name); 8 this.setAccount(account); 9 this.setDepositAmount(depositAmount); 10 } 11 public Account getAccount() { 12 return account; 13 } 14 public void setAccount(Account account) { 15 this.account = account; 16 } 17 18 public double getDepositAmount() { 19 return depositAmount; 20 } 21 public void setDepositAmount(double depositAmount) { 22 this.depositAmount = depositAmount; 23 } 24 25 public void run() { 26 for(int i=0 ; i<10; i++) { 27 account.deposit(depositAmount); 28 } 29 30 } 31 32 }
定义一个取款线程类depositThread
1 package threads.sync; 2 3 public class DrawThread extends Thread { 4 private Account account; 5 private double drawAmount; 6 public DrawThread(String name, Account account, double drawAmount) { 7 super(name); 8 this.setAccount(account); 9 this.setDrawAmount(drawAmount); 10 } 11 public Account getAccount() { 12 return account; 13 } 14 public void setAccount(Account account) { 15 this.account = account; 16 } 17 public double getDrawAmount() { 18 return drawAmount; 19 } 20 public void setDrawAmount(double drawAmount) { 21 this.drawAmount = drawAmount; 22 } 23 24 public void run() { 25 for(int i=0 ; i<10; i++) { 26 account.draw(drawAmount); 27 } 28 } 29 }
下面是测试类,存款线程中会有10次存款,三个取款线程中总共会有30次取款,
1 package threads.sync; 2 3 public class DrawTest { 4 public static void main(String[] args) { 5 Account acc = new Account("123456",1000); 6 new DrawThread("DrawThread", acc, 800).start(); 7 new DepositThread("DepositThread-A",acc,800).start(); 8 new DepositThread("DepositThread-B",acc,800).start(); 9 new DepositThread("DepositThread-C",acc,800).start(); 10 } 11 }
执行结果,
1 DepositThread-A deposit money: 800.0 2 balance : 1800.0 3 DrawThread draw money: 800.0 4 balance : 1000.0 5 DepositThread-B deposit money: 800.0 6 balance : 1800.0 7 DrawThread draw money: 800.0 8 balance : 1000.0 9 DepositThread-C deposit money: 800.0 10 balance : 1800.0 11 DrawThread draw money: 800.0 12 balance : 1000.0 13 DepositThread-C deposit money: 800.0 14 balance : 1800.0 15 DrawThread draw money: 800.0 16 balance : 1000.0 17 DepositThread-C deposit money: 800.0 18 balance : 1800.0 19 DrawThread draw money: 800.0 20 balance : 1000.0 21 DepositThread-C deposit money: 800.0 22 balance : 1800.0 23 DrawThread draw money: 800.0 24 balance : 1000.0 25 DepositThread-C deposit money: 800.0 26 balance : 1800.0 27 DrawThread draw money: 800.0 28 balance : 1000.0 29 DepositThread-A deposit money: 800.0 30 balance : 1800.0
从执行结果中可以看到,三个取款线程ABC执行顺序随机,但是总是在存款完成后,才会进行取款操作,而且无论存款还是取款,都不会同时进行两次。
使用condition控制线程通信
如果程序使用lock来同步线程的话,就要使用condition来进行线程通信。
在lock同步线程中,lock 对象就是一个显示的同步监视器,但是这个显示的同步监视器不直接阻塞或者通知线程,而是通过condition——lock对象通过调用newCondition方法返回一个与lock关联的condition对象,由condition对象来控制线程阻塞(await)和发出信号(single)唤醒其他线程。
与synchronized同步线程方式对应的是,conditions方式也提供了三个方法,
await:类似于synchronized隐式同步控制器对象调用的wait方法,可以阻塞当前线程,直到在别的线程中调用了condition的singal方法唤醒该线程。
signal:随机唤醒一个被await阻塞的线程。注意只有在当前线程已经释放lock同步监视器之后,被唤醒的其他线程才有机会执行。
signalAll:与上面类似,但是是唤醒所有线程。
下面用condition的方式来实现前面的银行取钱的例子,只需要修改Account类,改用lock同步线程,condition线程通信,
1 package threads.sync; 2 3 import java.util.concurrent.locks.Condition; 4 import java.util.concurrent.locks.Lock; 5 import java.util.concurrent.locks.ReentrantLock; 6 7 public class Account { 8 private String accountNo; 9 private double balance; 10 private boolean flag = false; 11 //显示定义lock对象 12 private final Lock lock = new ReentrantLock(); 13 //获取lock对象对应的condition 14 private final Condition cond = lock.newCondition(); 15 public Account() {} 16 17 18 public Account(String accountNo, double balance) { 19 this.accountNo = accountNo; 20 this.balance = balance; 21 } 22 23 public String getAccountNo() { 24 return accountNo; 25 } 26 27 public void setAccountNo(String accountNo) { 28 this.accountNo = accountNo; 29 } 30 31 public double getBalance() { 32 return balance; 33 } 34 35 public void setBalance(double balance) { 36 this.balance = balance; 37 } 38 39 40 public int hashCode() { 41 return accountNo.hashCode(); 42 } 43 44 public boolean equals(Object obj) { 45 if (this == obj) return true; 46 if (obj != null && obj.getClass() == Account.class) { 47 Account target = (Account)obj; 48 return target.getAccountNo().equals(accountNo); 49 } 50 return false; 51 } 52 53 public void draw(double drawAmount) { 54 lock.lock(); 55 try { 56 //if flag = false, means only deposit can be done, draw method will be blocked 57 if (!flag) { 58 //this.wait(); 59 cond.await(); 60 } else { 61 System.out.println(Thread.currentThread().getName() 62 + " draw money: " + drawAmount); 63 balance -= drawAmount; 64 System.out.println(" " 65 + " balance : " + balance); 66 flag = false; 67 //this.notifyAll(); 68 cond.signalAll(); 69 } 70 } catch (InterruptedException ex) { 71 ex.printStackTrace(); 72 } finally { 73 lock.unlock(); 74 } 75 } 76 77 public void deposit(double depositAmount) { 78 lock.lock(); 79 try { 80 //if flag = false, means only draw can be done, deposit method will be blocked 81 if (flag) { 82 //this.wait(); 83 cond.await(); 84 } else { 85 System.out.println(Thread.currentThread().getName() 86 + " deposit money: " + depositAmount); 87 balance += depositAmount; 88 System.out.println(" " 89 + " balance : " + balance); 90 flag = true; 91 //this.notifyAll(); 92 cond.signalAll(); 93 } 94 } catch (InterruptedException ex) { 95 ex.printStackTrace(); 96 } finally { 97 lock.unlock(); 98 } 99 } 100 101 }
对比用synchronized方式同步线程的例子,前面例子中是隐式的同步监视器(this)调用wait和notify来通信,
而本例是显示同步监视器(lock)的关联对象(condition)调用await和signal来通信,执行结果与前面一样不再给出。
使用阻塞队列(BlockingQueue)控制线程通信
BlockingQueue是JAVA5提供的一个队列接口,但这个队列并不是用作一个容器,而是作为线程的同步工具。
它可以很好地解决生产者消费者问题,而且比前面提到的两种方式更为灵活,
BlockingQueue的特征是,
当生产者线程试图向BlockingQueue存入元素时,如果队列已满,生产者线程将会阻塞,
当消费者线程试图从BlockingQueue取出元素时,如果队列为空,消费者线程将会阻塞
对比前面线程通信的例子,synchronized同步方法/代码块和lock+condition方式中,都只能控制生产者和消费者按固定顺序执行,
但BlockingQueue则是可以通过集合中的元素个数(商品数量)来控制线程执行顺序,通过调整集合容量可以控制线程切换的条件。
集合(商品)为空时,消费者阻塞,只能执行生产者线程;集合(商品)已满时,生产者阻塞,只能执行消费者线程。
BlockingQueue接口有很多实现类,下面演示最常用的实现类ArrayBlockQueue控制线程通信,
定义一个生产者线程类Producter
1 package threads.sync; 2 3 import java.util.concurrent.BlockingQueue; 4 5 public class Producter extends Thread { 6 private BlockingQueue<String> bq; 7 8 public Producter(BlockingQueue<String> bq) { 9 this.bq = bq; 10 } 11 12 public void run() { 13 String[] strArr = new String[] { 14 "Java", 15 "Struts", 16 "Spring" 17 }; 18 19 for(int i = 0; i<999999; i++) { 20 System.out.println(getName()+" 生产者准备生产集合元素"); 21 try { 22 Thread.sleep(200); 23 //如果队列已满,线程将阻塞 24 bq.put(strArr[i % 3]); 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } 28 System.out.println(getName()+" 生产完成: " + bq); 29 } 30 } 31 }
定义一个消费者类Consumer
1 package threads.sync; 2 3 import java.util.concurrent.BlockingQueue; 4 5 public class Consumer extends Thread { 6 private BlockingQueue<String> bq; 7 8 public Consumer(BlockingQueue<String> bq) { 9 this.bq = bq; 10 } 11 12 public void run() { 13 14 while (true) { 15 System.out.println(getName()+" 消费者准备消费集合元素"); 16 try { 17 Thread.sleep(200); 18 //如果队列已空,线程将阻塞 19 bq.take(); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 System.out.println(getName()+" 消费完成: " + bq); 24 } 25 } 26 }
在测试类中,定义一个容量为2的阻塞集合,
启动三个生产者线程, 每个线程都在不停生产商品,存入阻塞队列中,
启动一个消费者线程,每个线程也在不停从阻塞队列中取出商品,
1 package threads.sync; 2 3 import java.util.concurrent.ArrayBlockingQueue; 4 import java.util.concurrent.BlockingQueue; 5 6 public class BlockingQueueTest { 7 public static void main(String[] args) { 8 BlockingQueue<String> bq = new ArrayBlockingQueue<String>(2); 9 new Producter(bq).start(); 10 new Producter(bq).start(); 11 new Producter(bq).start(); 12 new Consumer(bq).start(); 13 } 14 }
执行结果,从执行结果中可以看到,只要集合中有元素且集合没有满,那么生产者和消费者线程都有机会得到执行,具体谁有机会要看谁抢到CPU执行片,
但是当集合空了的时候,例如第7行(Thread-8 消费完成: []),接着又有一个消费者线程执行,但是因此集合为空而阻塞了,此时只有生产者线程能执行,
当集合满了的时候,例如第11行(Thread-7 生产完成: [Java, Java]),接着又有一个生产者线程执行,但是因为集合已满而阻塞了,此时只有消费者线程能执行。
1 Thread-5 生产者准备生产集合元素 2 Thread-6 生产者准备生产集合元素 3 Thread-7 生产者准备生产集合元素 4 Thread-8 消费者准备消费集合元素 5 Thread-5 生产完成: [Java] 6 Thread-5 生产者准备生产集合元素 7 Thread-8 消费完成: [] 8 Thread-8 消费者准备消费集合元素 9 Thread-6 生产完成: [Java] 10 Thread-6 生产者准备生产集合元素 11 Thread-7 生产完成: [Java, Java] 12 Thread-7 生产者准备生产集合元素 13 Thread-8 消费完成: [Java] 14 Thread-8 消费者准备消费集合元素 15 Thread-5 生产完成: [Java, Struts] 16 Thread-5 生产者准备生产集合元素 17 Thread-8 消费完成: [Struts] 18 Thread-8 消费者准备消费集合元素 19 Thread-6 生产完成: [Struts, Struts] 20 Thread-6 生产者准备生产集合元素 21 Thread-8 消费完成: [Struts] 22 Thread-8 消费者准备消费集合元素 23 Thread-7 生产完成: [Struts, Struts] 24 Thread-7 生产者准备生产集合元素 25 Thread-8 消费完成: [Struts] 26 Thread-8 消费者准备消费集合元素 27 Thread-5 生产完成: [Struts, Spring] 28 Thread-5 生产者准备生产集合元素 29 Thread-8 消费完成: [Spring] 30 Thread-8 消费者准备消费集合元素 31 Thread-6 生产完成: [Spring, Spring] 32 Thread-6 生产者准备生产集合元素 33 Thread-8 消费完成: [Spring] 34 Thread-8 消费者准备消费集合元素
上面的例子来自李刚的疯狂JAVA, 但个人认为并不是太好,因为无论在生产者还是消费者线程中,打印bq操作前后的两段日志并不是原子操作,这会导致打印的日志不准确,
例如下面的运行结果,从第5行看到(Thread-6 生产完成: []),刚刚执行完一个生产者线程中的入队操作,但是打印队列却是空的,原因就在于在打印两行日志期间,消费者线做了取元素的操作。
1 Thread-6 生产者准备生产集合元素 2 Thread-5 生产者准备生产集合元素 3 Thread-7 生产者准备生产集合元素 4 Thread-8 消费者准备消费集合元素 5 Thread-6 生产完成: [] 6 Thread-6 生产者准备生产集合元素 7 Thread-5 生产完成: [Java] 8 Thread-5 生产者准备生产集合元素 9 Thread-7 生产完成: [Java, Java] 10 Thread-7 生产者准备生产集合元素 11 Thread-8 消费完成: [] 12 Thread-8 消费者准备消费集合元素 13 Thread-7 生产完成: [Java, Struts] 14 Thread-7 生产者准备生产集合元素 15 Thread-8 消费完成: [Java, Struts] 16 Thread-8 消费者准备消费集合元素 17 Thread-8 消费完成: [Struts] 18 Thread-5 生产完成: [Struts, Struts] 19 Thread-5 生产者准备生产集合元素 20 Thread-8 消费者准备消费集合元素 21 Thread-8 消费完成: [Struts] 22 Thread-8 消费者准备消费集合元素 23 Thread-6 生产完成: [Struts, Struts] 24 Thread-6 生产者准备生产集合元素 25 Thread-8 消费完成: [Struts] 26 Thread-8 消费者准备消费集合元素 27 Thread-6 生产完成: [Struts, Spring] 28 Thread-6 生产者准备生产集合元素 29 Thread-8 消费完成: [Spring] 30 Thread-8 消费者准备消费集合元素 31 Thread-5 生产完成: [Spring, Spring] 32 Thread-5 生产者准备生产集合元素 33 Thread-8 消费完成: [Spring] 34 Thread-8 消费者准备消费集合元素 35 Thread-5 生产完成: [Spring, Java] 36 Thread-5 生产者准备生产集合元素 37 Thread-8 消费完成: [Java] 38 Thread-8 消费者准备消费集合元素 39 Thread-6 生产完成: [Java, Java] 40 Thread-6 生产者准备生产集合元素 41 Thread-8 消费完成: [Java] 42 Thread-8 消费者准备消费集合元素 43 Thread-7 生产完成: [Java, Spring] 44 Thread-7 生产者准备生产集合元素 45 Thread-8 消费完成: [Spring] 46 Thread-8 消费者准备消费集合元素 47 Thread-7 生产完成: [Spring, Java] 48 Thread-7 生产者准备生产集合元素 49 Thread-8 消费完成: [Java] 50 Thread-8 消费者准备消费集合元素 51 Thread-7 生产完成: [Java, Struts] 52 Thread-7 生产者准备生产集合元素 53 Thread-8 消费完成: [Struts] 54 Thread-8 消费者准备消费集合元素 55 Thread-7 生产完成: [Struts, Spring] 56 Thread-7 生产者准备生产集合元素 57 Thread-8 消费完成: [Spring] 58 Thread-8 消费者准备消费集合元素 59 Thread-7 生产完成: [Spring, Java] 60 Thread-7 生产者准备生产集合元素 61 Thread-8 消费完成: [Java] 62 Thread-8 消费者准备消费集合元素 63 Thread-7 生产完成: [Java, Struts] 64 Thread-7 生产者准备生产集合元素 65 Thread-7 生产完成: [Struts, Spring] 66 Thread-7 生产者准备生产集合元素 67 Thread-8 消费完成: [Struts, Spring] 68 Thread-8 消费者准备消费集合元素 69 Thread-8 消费完成: [Spring] 70 Thread-8 消费者准备消费集合元素 71 Thread-5 生产完成: [Spring, Struts] 72 Thread-5 生产者准备生产集合元素 73 Thread-8 消费完成: [Struts] 74 Thread-8 消费者准备消费集合元素 75 Thread-6 生产完成: [Struts, Struts] 76 Thread-6 生产者准备生产集合元素 77 Thread-8 消费完成: [Struts, Spring] 78 Thread-8 消费者准备消费集合元素 79 Thread-6 生产完成: [Struts, Spring] 80 Thread-6 生产者准备生产集合元素