Java多线程之线程的通信
在总结多线程通信前先介绍一个概念:锁池。线程因为未拿到锁标记而发生的阻塞不同于前面五个基本状态中的阻塞,称为锁池。每个对象都有自己的锁池的空间,用于放置等待运行的线程。这些线程中哪个线程拿到锁标记由系统决定。前面我们也有T到死锁的概念,线程互相等待其他线程释放锁标记,而又不释放自己的;造成无休止地等待。当出现死锁的时候,我们应该如何解决呢?通过线程间的通信解决。
- 线程间通信:
多线程之间的通信有2种方式,第一种是使用object类的几个方法,第二种是使用条件变了来控制协调。这里重点介绍第一种
1,线程间通信机制实际上也就是协调机制。线程间通信使用的空间称之为对象的等待队列,这个队列也属于对象的空间。值得 注意的是,一个对象除了由属性和方法之外还有互斥锁标记、锁池空间和等待队列空间。
2,wait()方法:导致当前线程等待
3,Notify()方法:唤醒在此同步监视器上面的一个线程。
4,NotifyAll()方法:唤醒在此同步监视器上面的所有线程。
- 值得注意的是:
1,只能对加锁的资源(Synchronized方法里)进行wait()和notify()。
2,我们应该用notifyall取代notify,因为我们用notify释放出的一个线程是不确定的,由OS决定。
3,释放锁标记只有在Synchronized 代码结束或者调用wait()。
4,锁标记是自己不会自动释放,必须有通知。
5,在程序中判定一个条件是否成立时要注意使用while要比使用if更严密。
下面代码模拟了一个存款和取款的逻辑,具体代码如下:
/** * * @version 1L * @author LinkinPark * @since 2015-2-5 * @motto 梦似烟花心似水,同学少年不言情 * @desc ^ 以后在编码中养成良好的习惯,要是一个类的某些属性不能随便更改的话,就不要提供set方法 */ public class Account { private String accountNo;//账户编号 private double balance;//账户余额 private boolean flag = false;//标识账户中是否已有存款的旗标 public Account() { } public Account(String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public String getAccountNo() { return this.accountNo; } // 因此账户余额不允许随便修改,所以只为balance提供getter方法,不要提供set方法 public double getBalance() { return this.balance; } public synchronized void draw(double drawAmount) { try { // 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞 if (!flag) { wait(); } else { // 执行取钱 System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount); balance -= drawAmount; System.out.println("账户余额为:" + balance); // 将标识账户是否已有存款的旗标设为false。 flag = false; // 唤醒其他线程 notifyAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } } public synchronized void deposit(double depositAmount) { try { // 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞 if (flag) { wait(); } else { // 执行存款 System.out.println(Thread.currentThread().getName() + " 存款:" + depositAmount); balance += depositAmount; System.out.println("账户余额为:" + balance); // 将表示账户是否已有存款的旗标设为true flag = true; // 唤醒其他线程 notifyAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } } // 下面两个方法根据accountNo来重写hashCode()和equals()方法 public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if (this == obj) return true; if (obj != null && obj.getClass() == Account.class) { Account target = (Account) obj; return target.getAccountNo().equals(accountNo); } return false; } }
/** * * @version 1L * @author LinkinPark * @since 2015-2-5 * @motto 梦似烟花心似水,同学少年不言情 * @desc ^ 取钱的线程 */ public class DrawThread extends Thread { private Account account;// 模拟用户账户 private double drawAmount;// 当前取钱线程所希望取的钱数 public DrawThread(String name, Account account, double drawAmount) { super(name); this.account = account; this.drawAmount = drawAmount; } // 重复100次执行取钱操作 public void run() { for (int i = 0; i < 100; i++) { account.draw(drawAmount); } } }
/** * @version 1L * @author LinkinPark * @since 2015-2-5 * @motto 梦似烟花心似水,同学少年不言情 * @desc ^存钱的线程 */ public class DepositThread extends Thread { private Account account;// 模拟用户账户 private double depositAmount;// 当前取钱线程所希望存款的钱数 public DepositThread(String name, Account account, double depositAmount) { super(name); this.account = account; this.depositAmount = depositAmount; } // 重复100次执行存款操作 public void run() { for (int i = 0; i < 100; i++) { account.deposit(depositAmount); } } }
public class DrawTest { public static void main(String[] args) { // 创建一个账户 Account acct = new Account("NightWish", 0); new DrawThread("取钱者", acct, 800).start(); new DepositThread("存款者甲", acct, 800).start(); new DepositThread("存款者乙", acct, 800).start(); new DepositThread("存款者丙", acct, 800).start(); } }
关于第2种方式,了解下就好了。我翻了下API,上面是这样子介绍Condition的。Condition将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。 这种方式和前面讲的lock锁相互使用,lock一般使用ReentrantLock做实现类,有lock和unlock2个加锁和释放锁的方法,这里Condition由lock的newCondition()方法获得,然后有await,signal,signalAll3个方法比较简单的,这里不做赘述了。