Java程序设计17——多线程-Part-B
5 改变线程优先级
每个线程执行都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。
Thread提供了setPriority(int new Priority)和getPriority()方法来设置和返回指定线程的优先级,其中setPriority方法的参数可以是一个整数,范围是1~10,也可以是Thread类的三个静态常量:
1.MAX_PRIORITY:其值是10
2.MIN_PRIORITY:值是1
3.NORM_PRIORITY:其值是5
package chapter16; public class PriorityTest extends Thread{ public PriorityTest(){ }; public PriorityTest(String name){ super(name); }; //线程执行体 public void run(){ for(int i = 0; i < 50; i++){ System.out.println(getName() + ",其优先级是: " + getPriority() + ",循环变量的值是" + i); } }; public static void main(String[] args){ //改变主线程的优先级 Thread.currentThread().setPriority(6); for(int i = 0; i < 27; i++){ if(i == 10){ PriorityTest low = new PriorityTest("低级"); low.start(); //输出主线程创建的子线程的优先级 System.out.println("创建之初优先级是:" + low.getPriority()); //修改其优先级为最低级 low.setPriority(Thread.MIN_PRIORITY); } if(i == 20){ PriorityTest high = new PriorityTest("高级"); high.start(); //输出主线程创建的子线程的优先级 System.out.println("创建之初的优先级是: " + high.getPriority()); //修改其优先级为最高级 high.setPriority(Thread.MAX_PRIORITY); } } } }; 输出结果: 创建之初优先级是:6 创建之初的优先级是: 6 高级,其优先级是: 10,循环变量的值是0 高级,其优先级是: 10,循环变量的值是1 高级,其优先级是: 10,循环变量的值是2 高级,其优先级是: 10,循环变量的值是3 高级,其优先级是: 10,循环变量的值是4 高级,其优先级是: 10,循环变量的值是5 高级,其优先级是: 10,循环变量的值是6 高级,其优先级是: 10,循环变量的值是7 高级,其优先级是: 10,循环变量的值是8 高级,其优先级是: 10,循环变量的值是9 高级,其优先级是: 10,循环变量的值是10 高级,其优先级是: 10,循环变量的值是11 高级,其优先级是: 10,循环变量的值是12 高级,其优先级是: 10,循环变量的值是13 高级,其优先级是: 10,循环变量的值是14 高级,其优先级是: 10,循环变量的值是15 高级,其优先级是: 10,循环变量的值是16 高级,其优先级是: 10,循环变量的值是17 高级,其优先级是: 10,循环变量的值是18 高级,其优先级是: 10,循环变量的值是19 高级,其优先级是: 10,循环变量的值是20 高级,其优先级是: 10,循环变量的值是21 高级,其优先级是: 10,循环变量的值是22 高级,其优先级是: 10,循环变量的值是23 高级,其优先级是: 10,循环变量的值是24 高级,其优先级是: 10,循环变量的值是25 高级,其优先级是: 10,循环变量的值是26 低级,其优先级是: 1,循环变量的值是0 低级,其优先级是: 1,循环变量的值是1 低级,其优先级是: 1,循环变量的值是2 低级,其优先级是: 1,循环变量的值是3 低级,其优先级是: 1,循环变量的值是4 低级,其优先级是: 1,循环变量的值是5 低级,其优先级是: 1,循环变量的值是6 低级,其优先级是: 1,循环变量的值是7 低级,其优先级是: 1,循环变量的值是8 低级,其优先级是: 1,循环变量的值是9 低级,其优先级是: 1,循环变量的值是10 低级,其优先级是: 1,循环变量的值是11 低级,其优先级是: 1,循环变量的值是12 低级,其优先级是: 1,循环变量的值是13 低级,其优先级是: 1,循环变量的值是14 低级,其优先级是: 1,循环变量的值是15 低级,其优先级是: 1,循环变量的值是16 低级,其优先级是: 1,循环变量的值是17 低级,其优先级是: 1,循环变量的值是18 低级,其优先级是: 1,循环变量的值是19 低级,其优先级是: 1,循环变量的值是20 低级,其优先级是: 1,循环变量的值是21 低级,其优先级是: 1,循环变量的值是22 低级,其优先级是: 1,循环变量的值是23 低级,其优先级是: 1,循环变量的值是24 低级,其优先级是: 1,循环变量的值是25 低级,其优先级是: 1,循环变量的值是26
程序分析:
虽然高级的线程后启动,但是它获得了先执行的机会。
需要指出的是,虽然Java提供了10个优先级别,但这些优先级别需要操作系统指出,不幸的是,不同操作系统上优先级并不相同,而且不能很好的和Java的10个优先级对应,例如windows2000提供了7个优先级,这种情况下,我们应该避免直接为线程指定优先级,应该使用三个静态常量来设置优先级,这样才可以保证程序具有最好的可移植性。
6 线程的同步
多线程编程是有趣的事情,它常常容易出现"错误情况",这是由于系统的线程调度具有随机性,即使程序在运行过程中偶尔出现问题,那也是由于我们编程不当引起的。当使用多个线程访问同一个数据时,非常容易出现线程安全问题。
线程安全问题
关于线程安全问题,有一个经典的问题:银行取钱问题。一个人取钱一般不会有问题,但是两个人同时对一个账户并发取钱可能就会出现问题。
取钱通常有如下步骤:
1.用户输入账户、密码,系统判断用户的账户、密码是否匹配
2.用户输入取款金额
3.系统判断账户余额是否大于取款金额。
4.如果余额大于取款金额,取款成功;如果余额小于取款金额,则取款失败。
我们按上面的流程去编写取款程序,而且我们使用两条模拟现场来模拟取钱操作,模拟两个人使用同一个账户并发取钱的问题。为了简化问题,不管检查账户和密码操作,仅仅模拟后面三步操作。
先定义一个账户类,该账户类封装了账户编号和余额两个属性。
1 package chapter16; 2 3 public class Account { 4 //封装账号和余额两个属性 5 private String accountNo; 6 private double balance; 7 public Account(){ 8 9 }; 10 public void setAcountNo(String acountNo) { 11 this.accountNo = accountNo; 12 } 13 public String getAcountNo() { 14 return accountNo; 15 } 16 public void setBalance(double balance) { 17 this.balance = balance; 18 } 19 public double getBalance() { 20 return balance; 21 } 22 public Account(String accountNo, double balance){ 23 this.accountNo = accountNo; 24 this.balance = balance; 25 }; 26 //下面根据accountNo来计算Account的hashCode和判断equals 27 public int hashCode(){ 28 return accountNo.hashCode(); 29 } 30 public boolean equals(Object obj){ 31 if((obj != null) && (obj.getClass() == Account.class)){ 32 Account target = (Account)obj; 33 return target.getAcountNo().equals(accountNo); 34 } 35 return false; 36 } 37 }
接下来提供一个取钱的线程类,该线程类根据执行账户、取钱数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出钞票,余额减少。
1 package chapter16; 2 3 public class DrawThread extends Thread{ 4 //模拟用户账户 5 private Account account; 6 //当前取钱线程希望取的钱数 7 private double drawAmount; 8 public DrawThread(String name, Account account, double drawAmount){ 9 super(name); 10 this.account = account; 11 this.drawAmount = drawAmount; 12 } 13 //当多条线程试图修改同一个共享数据时,将会引发安全问题 14 public void run(){ 15 //账户余额大于取钱数目 16 if(account.getBalance() >= drawAmount){ 17 //吐出钞票 18 System.out.println(getName() + "取钱成功,吐出钞票!" + drawAmount); 19 /* 20 try{ 21 Thread.sleep(1); 22 }catch(InterruptedException ex){ 23 ex.printStackTrace(); 24 } 25 */ 26 //修改余额 27 account.setBalance(account.getBalance() - drawAmount); 28 System.out.println("\t余额为:" + account.getBalance()); 29 }else{ 30 System.out.println(getName() + "余额不足,取钱失败!"); 31 } 32 } 33 }
上面就是普通的取钱操作。下面是程序的主程序。创建一个账户,并启动两个线程,从该账户中取钱。
多次运行上面的程序,可以看到如下结果:
乙取钱成功,吐出钞票!800.0 余额为:200.0 甲余额不足,取钱失败! 或者 乙取钱成功,吐出钞票!800.0 余额为:200.0 甲余额不足,取钱失败!
出现这两种情况是因为线程的不确定性。当然上面的结果也是我们期望的结果,这表明程序逻辑没有问题。现在我们假设系统线程调度器在程序的try...catch处暂停,让另一条线程执行————为了强制暂停,我们只要取消上面程序中的粗体字代码的注释即可。取消注释后再次编译DrawThread.java,并再次运行TestDraw类将看到如下结果
乙取钱成功,吐出钞票!800.0 甲取钱成功,吐出钞票!800.0 余额为:200.0 余额为:-600.0
6.1 同步代码块
之所以出现上面的结果,其原因是因为run方法的方法体不具有同步安全性:程序中有两条并发线程在修改Account对象。而且系统恰好在try...catch处执行线程切换,切换给另一条修改Account对象的线程,所以就出现问题。
为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块,同步代码块的语法格式如下:
synchronized(obj){ .... //此处的代码就是同步代码块 }
上述格式的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
任何时刻只能有一条线程可以获得同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视器的锁定。
虽然Java程序允许使用任何对象来作为同步监视器,但想一下同步监视器的目的:阻止两条线程对同一个共享资源的并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,我们应该考虑使用账户account作为同步监视器。只要修改程序如下即可:
1 package chapter16; 2 3 public class DrawThread extends Thread{ 4 //模拟用户账户 5 private Account account; 6 //当前取钱线程希望取的钱数 7 private double drawAmount; 8 public DrawThread(String name, Account account, double drawAmount){ 9 super(name); 10 this.account = account; 11 this.drawAmount = drawAmount; 12 } 13 //当多条线程试图修改同一个共享数据时,将会引发安全问题 14 public void run(){ 15 //使用account作为同步监视器,任何线程进入下面同步代码块之前 16 //必须先获得对account账户锁定————其他线程无法获得锁,自然也就无法修改它 17 //这种做法符合加锁——>修改完成——>释放锁 逻辑 18 synchronized(account){ 19 //账户余额大于取钱数目 20 if(account.getBalance() >= drawAmount){ 21 //吐出钞票 22 System.out.println(getName() + "取钱成功,吐出钞票!" + drawAmount); 23 24 try{ 25 Thread.sleep(1); 26 }catch(InterruptedException ex){ 27 ex.printStackTrace(); 28 } 29 30 //修改余额 31 account.setBalance(account.getBalance() - drawAmount); 32 System.out.println("\t余额为:" + account.getBalance()); 33 }else{ 34 System.out.println(getName() + "余额不足,取钱失败!"); 35 } 36 } 37 38 } 39 }
上面程序使用synchronized将run方法i方法体修改成同步代码块,该同步代码块的同步监视器是account对象,这样的做法符合加锁——>修改完成——>释放锁 逻辑。任何线程想修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一条线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻最多只有一条线程处于临界区内,从而保证了线程的安全性。
6.2 同步方法
与同步代码块对应的,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于同步方法而言,无须显式指定同步监视器,同步方法的同步监视器是this,也就是该对象本身。
通过使用同步方法可以非常方便地将某类变成线程安全类,线程安全的类具有如下特征
1.该类的对象可以被多个线程安全防卫
2.每个线程调用该对象的任意方法之后都将得到正确的结果
3.每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
7 线程的同步
前面我们介绍了可变类和不可变类,其中不可变类总是线程安全的,因为它的对象的状态不可改变。但可变对象需要额外的方法来保证其线程安全。例如上面的Account就是一个可变类,它的account和balance两个属性都可变,当两个线程同时修改Account对象的balance属性时,程序就出现了异常。可以将修改balance的方法修改成同步方法,使用synchronized关键字即可。
1 public class Account{ 2 // 封装账户编号、账户余额两个Field 3 private String accountNo; 4 private double balance; 5 public Account(){} 6 // 构造器 7 public Account(String accountNo , double balance){ 8 this.accountNo = accountNo; 9 this.balance = balance; 10 } 11 12 // accountNo的setter和getter方法 13 public void setAccountNo(String accountNo){ 14 this.accountNo = accountNo; 15 } 16 public String getAccountNo(){ 17 return this.accountNo; 18 } 19 // 因此账户余额不允许随便修改,所以只为balance提供getter方法, 20 public double getBalance(){ 21 return this.balance; 22 } 23 24 // 提供一个线程安全draw()方法来完成取钱操作 25 public synchronized void draw(double drawAmount){ 26 // 账户余额大于取钱数目 27 if (balance >= drawAmount){ 28 // 吐出钞票 29 System.out.println(Thread.currentThread().getName() 30 + "取钱成功!吐出钞票:" + drawAmount); 31 try{ 32 Thread.sleep(1); 33 } 34 catch (InterruptedException ex){ 35 ex.printStackTrace(); 36 } 37 // 修改余额 38 balance -= drawAmount; 39 System.out.println("\t余额为: " + balance); 40 }else{ 41 System.out.println(Thread.currentThread().getName() 42 + "取钱失败!余额不足!"); 43 } 44 } 45 46 // 下面两个方法根据accountNo来重写hashCode()和equals()方法 47 public int hashCode(){ 48 return accountNo.hashCode(); 49 } 50 public boolean equals(Object obj){ 51 if(this == obj) 52 return true; 53 if (obj !=null 54 && obj.getClass() == Account.class){ 55 Account target = (Account)obj; 56 return target.getAccountNo().equals(accountNo); 57 } 58 return false; 59 } 60 }
上面程序中增加了一个代表取钱的draw方法,并使用了synchronized关键字修饰该方法,把该方法变成同步方法,同步方法的同步监视器是this,也就是谁调用该方法,this代表哪个对象,因此对于同一个Account账户而言,任意时刻只能有一条线程获得对Account对象的锁定,然后进入draw()方法执行取钱操作————这样也可以保证多条线程并发取钱的线程安全。 synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。
因为Account类中已经提供了draw方法,并且取消了setBalance()方法,DrawThread线程类需要改写,该类只要调用Account对象的draw方法来执行取钱操作。
1 public class DrawThread extends Thread{ 2 // 模拟用户账户 3 private Account account; 4 // 当前取钱线程所希望取的钱数 5 private double drawAmount; 6 public DrawThread(String name , Account account, double drawAmount){ 7 super(name); 8 this.account = account; 9 this.drawAmount = drawAmount; 10 } 11 // 重复100次执行取钱操作 12 public void run(){ 13 for (int i = 0 ; i < 100 ; i++ ){ 14 account.draw(drawAmount); 15 } 16 } 17 }
上面的DrawThread类无须自己实现取钱操作,而是直接调用account的draw方法来执行取钱,该方法是线程安全的。
上面把draw方法定义在Account里,而不是直接在run方法中实现取钱逻辑,这种做法更符合面向对象规则。在面向对象里有一种流行的设计方式:Domain Driven Design(即领域驱动设计,简称DDD),这种方式认为每个类都应该是完备的领域对象,也就是说对于它的每一个属性都应该提供完整的操作方法。例如Account它代表用户账户,它应该提供用户账户的相关方法,例如通过draw()方法来执行取钱操作,转账操作等等,而不是直接将setBalance()方法暴露出来任人操作,这样才可以更换地保证Account对象的完整性和一致性。
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略: 1.不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。不能对属性、构造器同步 2.如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本:线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。
8 释放同步监视器的锁定
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定.
线程会在如下几种情况下释放对同步监视器的锁定:
1.当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器
2.当线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。
3.当线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时将会释放同步监视器。
4.当线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
下面情况下,线程不会释放同步监视器:
1.线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
2.线程执行同步代码块时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。当然,我们应该尽量避免使用suspend和resume方法来控制线程
9 同步锁(Lock)
从JDK1.5之后,Java提供了另外一种线程同步机制:它通过显式定义同步锁对象来实现同步,在这种机制下,同步锁应该使用Lock对象充当。
通常认为:Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且可以支持多个相关的Condition对象。
Lock是控制多线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。不过,某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁)。当然,在实现线程安全的控制中,通常喜欢使用ReentrantLock(可重入锁)。使用该Lock对象可以显示加锁、释放锁,通常使用Lock对象的代码格式如下:
1 package chapter16; 2 3 import java.util.*; 4 import java.util.concurrent.locks.ReentrantLock; 5 6 public class X { 7 //定义锁对象 8 private final ReentrantLock lock = new ReentrantLock(); 9 //定义需要保证线程安全的方法 10 public void method(){ 11 //加锁 12 lock.lock(); 13 try{ 14 //需要保证线程安全的代码 15 }finally{ 16 //释放锁 17 lock.unlock(); 18 } 19 } 20 }
使用Lock对象来进行同步时,锁定和释放锁出现在不同作用范围中时,通常建议使用finally块来确保在必要时释放锁。通常使用Lock对象我们可以把Account类改为如下形式,它依然是线程安全的。
1 import java.util.concurrent.locks.*; 2 3 public class Account{ 4 // 定义锁对象 5 private final ReentrantLock lock = new ReentrantLock(); 6 // 封装账户编号、账户余额两个Field 7 private String accountNo; 8 private double balance; 9 public Account(){} 10 // 构造器 11 public Account(String accountNo , double balance){ 12 this.accountNo = accountNo; 13 this.balance = balance; 14 } 15 16 // accountNo的setter和getter方法 17 public void setAccountNo(String accountNo){ 18 this.accountNo = accountNo; 19 } 20 public String getAccountNo(){ 21 return this.accountNo; 22 } 23 // 因此账户余额不允许随便修改,所以只为balance提供getter方法, 24 public double getBalance(){ 25 return this.balance; 26 } 27 28 // 提供一个线程安全draw()方法来完成取钱操作 29 public void draw(double drawAmount){ 30 // 加锁 31 lock.lock(); 32 try{ 33 // 账户余额大于取钱数目 34 if (balance >= drawAmount){ 35 // 吐出钞票 36 System.out.println(Thread.currentThread().getName() 37 + "取钱成功!吐出钞票:" + drawAmount); 38 try{ 39 Thread.sleep(1); 40 }catch (InterruptedException ex){ 41 ex.printStackTrace(); 42 } 43 // 修改余额 44 balance -= drawAmount; 45 System.out.println("\t余额为: " + balance); 46 }else{ 47 System.out.println(Thread.currentThread().getName() 48 + "取钱失败!余额不足!"); 49 } 50 } 51 finally{ 52 // 修改完成,释放锁 53 lock.unlock(); 54 } 55 } 56 57 // 下面两个方法根据accountNo来重写hashCode()和equals()方法 58 public int hashCode(){ 59 return accountNo.hashCode(); 60 } 61 public boolean equals(Object obj){ 62 if(this == obj) 63 return true; 64 if (obj !=null 65 && obj.getClass() == Account.class){ 66 Account target = (Account)obj; 67 return target.getAccountNo().equals(accountNo); 68 } 69 return false; 70 } 71 }
上面程序定义了一个可重入锁对象,程序中实现draw方法时,进入方法开始执行后立即请求对lock对象进行加锁,接着程序完全实现了draw方法的取钱逻辑之后,程序使用finally块来确保释放锁。
同步方法或同步代码块使用竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且当释放了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。
虽然同步方法和同步代码块的范围机制使得多线程安全编程非常方便,而且还可以避免了很多涉及锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。Lock提供了同步方法和同步代码块没有的其他功能,包括用于非块结构的tryLock方法,以及试图获取可中断锁lockInterruptibly()方法,还有获取超时失效锁tryLock(long,TimeUnit)方法 ReentrantLock锁具有重入性,也就是说线程可以对它已经加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用,线程在每次调用lock加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
10 死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采用措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁的出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有现存处于阻塞状态,无法继续。
死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下,如下程序将会出现死锁。
1 package chapter6; 2 3 class A{ 4 public synchronized void foo(B b){ 5 System.out.println("当前线程名:" + 6 Thread.currentThread().getName()+ "进入了A的实例foo方法");//① 7 try{ 8 Thread.sleep(200); 9 }catch(InterruptedException ex){ 10 ex.printStackTrace(); 11 } 12 System.out.println("当前线程名:" + 13 Thread.currentThread().getName()+ "企图调用B实例的last方法");//③ 14 b.last(); 15 } 16 public synchronized void last(){ 17 System.out.println("进入了A实例last方法的内部"); 18 } 19 }; 20 class B{ 21 public synchronized void bar(A a){ 22 System.out.println("当前线程名:" + 23 Thread.currentThread().getName()+ "进入了B的实例bar方法");//② 24 try{ 25 Thread.sleep(200); 26 }catch(InterruptedException ex){ 27 ex.printStackTrace(); 28 } 29 System.out.println("当前线程名:" + 30 Thread.currentThread().getName()+ "企图调用A实例的last方法");//④ 31 a.last(); 32 } 33 public synchronized void last(){ 34 System.out.println("进入了B实例last方法的内部"); 35 } 36 }; 37 public class DeadLock implements Runnable { 38 A a = new A(); 39 B b = new B(); 40 public void init(){ 41 Thread.currentThread().setName("主线程"); 42 //调用a对象的foo方法 43 a.foo(b); 44 System.out.println("进入主线程之后"); 45 } 46 public void run(){ 47 Thread.currentThread().setName("副线程"); 48 //调用b对象的bar方法 49 b.bar(a); 50 System.out.println("进入副线程之后"); 51 } 52 public static void main(String[] args){ 53 DeadLock dl = new DeadLock(); 54 //以dl为target启动线程 55 new Thread(dl).start(); 56 //执行init方法作为新线程 57 dl.init(); 58 } 59 };
输出结果:
当前线程名:主线程进入了A的实例foo方法
当前线程名:副线程进入了B的实例bar方法
当前线程名:主线程企图调用B实例的last方法
当前线程名:副线程企图调用A实例的last方法
从上面结果可以看出,程序即无法向下执行,也不会抛出任何异常,将一直僵持着无法向下进行。原因是因为:上面程序中A对象和B对象的方法都是同步方法,也就是A对象和B对象都是同步锁。程序中两条线程执行,一条线程的线程执行体DeadLock类的run方法,另一条线程的线程执行体是DeadLock的init方法(主线程调用了init方法)。其中run方法中让B对象调用bar方法,而init方法让A对象调用foo方法。由于线程分配执行的随机性,从结果来看,本次执行时,init方法执行,调用了A对象的foo方法,进入foo方法之前,该线程对A对象加锁————当程序执行到①号代码时,主线程暂停200ms;CPU切换到执行另一条线程,让B对象执行bar方法,所以在结果中看到副线程开始执行B实例的bar方法,进入bar方法之前,该线程对B对象加锁————当程序执行到②号代码时,副线程也暂停200ms;接下来主线程会先醒过来,继续向下执行,直到③号代码处希望调用B对象的last方法————执行该方法之前必须先对B对象加锁,但此时副线程正保持着B对象的锁,所以主线程被阻塞;接下来副线程应该也醒过来了,继续向下执行,直到④号代码处希望调用A对象的last方法————执行该方法之前必须先对A对象加锁,但此时主线程正保持着A对象的锁————至此就出现了主线程保持着A对象的锁,等待对B对象的加锁,而副线程保持着B对象的锁,等待着对A对象的加锁,两条线程互相等待对方先释放,这就被称为死锁现象。
由于Thread类的suspend也很容易导致死锁,所以Java不再推荐使用该方法来暂停线程的执行。
11 线程通信
当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但我们可以通过一些机制来保证线程协调运行。
11.1 线程的协调运行
假设现在系统中有两条线程,这两条线程分别代表存款者和取钱者———现在假设系统有一种特殊的要求,系统要求存款者和取钱者不断地重复取款、取钱动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取出该笔钱。不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。
为了实现这种功能,可以借助于Object类提供的wait()、notify()、notifyAll()三个方法,这三个方法并不属于Thread类,而是属于Object类,但这三个方法必须由同步监视器对象来调用,这可分成两种情况。
1.对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用者三个方法
2.对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用者三个方法
关于这三个方法的解释如下:
1.wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。该wait()方法又三种形式:无时间参数的wait(一直等待,知道其他线程通知),带毫秒参数的wait和带毫秒、微秒参数的wait(这两种方法都是等待指定时间后自动苏醒)。调用wait()方法的当前线程会释放对该同步监视器的锁定。
2.notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
3.notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
程序中可以通过一个旗标来标识账户中是否已有存款,当旗标为false时,表明账户中没有存款,存款者线程可以向下执行,当存款者把钱存入账户后,将旗标设为true,并调用notify()或notifyAll()方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为true就调用wait()方法来阻塞该线程。
当旗标为true时,表明账户中已经存入存款,则取钱者线程可以向下执行,当取钱者把钱从账户中取出后,将旗标设为false,并调用notify()或notifyAll()方法来唤醒其他线程;当取钱者线程进入线程体后,如果旗标为false就调用wait方法来阻塞该线程。
本程序通过Account类提供draw和deposit两个方法,分别对应该账户的取钱、存款等操作,因为这两个方法可能需要并发修改Account类的balance属性,所以这两个方法都使用synchronized修饰成同步方法。除此之外,这两个方法还使用了wait()、notifyAll()来控制线程的协作。
1 package thread; 2 3 public class Account { 4 private String accountNo; 5 private double balance; 6 //标识账户是否已有存款的旗标 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 public void setAccountNo(String accountNo){ 16 this.accountNo = accountNo; 17 } 18 public double getBalance(){ 19 return this.balance; 20 }; 21 22 public String getAcountNo() { 23 return accountNo; 24 } 25 public synchronized void draw(double drawAmount){ 26 try{ 27 //如果flag为假,表明还没有人存钱进去,则取钱方法阻塞 28 if(!flag){ 29 wait(); 30 }else{ 31 //执行取钱 32 System.out.println(Thread.currentThread().getName() + "取钱" + drawAmount); 33 balance -= drawAmount; 34 System.out.println("账户余额为:" + balance); 35 //将标识账户是否已有存款的旗标设为false 36 flag = false; 37 //唤醒其他线程 38 notifyAll(); 39 } 40 }catch(InterruptedException ex){ 41 ex.printStackTrace(); 42 } 43 }; 44 public synchronized void deposit(double depositAmount){ 45 try{ 46 //如果flag为真,表明账户中有人存钱进去,则存钱方法阻塞 47 if(flag){ 48 wait(); 49 }else{ 50 //执行存款 51 System.out.println(Thread.currentThread().getName() 52 + " 存款" + depositAmount); 53 balance += depositAmount; 54 System.out.println("账户余额为: " + balance); 55 //将账户是否有存款设为true 56 flag = true; 57 notifyAll(); 58 } 59 }catch(InterruptedException ex){ 60 ex.printStackTrace(); 61 } 62 } 63 //下面根据accountNo来计算Account的hashCode和判断equals 64 public int hashCode(){ 65 return accountNo.hashCode(); 66 } 67 public boolean equals(Object obj){ 68 if((obj != null) && (obj.getClass() == Account.class)){ 69 Account target = (Account)obj; 70 return target.getAcountNo().equals(accountNo); 71 } 72 return false; 73 } 74 }
上面程序中使用了wait和notifyAll进行了控制,对存款者线程而言,当程序进入deposit方法后,如果flag为true,表明账户中已有存款,程序调用wait方法阻塞;否则程序向下执行存款操作,当存款操作执行完成后,系统将flag设为true,然后调用notifyAll来唤醒其他被阻塞的线程————如果系统中有存款者线程,存款者线程也会被唤醒,但该存款者线程执行到①号代码处再次进入阻塞,只有执行draw方法的取款者线程才可以向下执行。同理,取款者线程的运行流程也是如此。
程序中的存款者线程循环100次重复循环,而取钱者线程则循环100次重复取钱,存款者线程和取钱者线程分别调用Account对象deposit、draw方法来实现。
1 package thread; 2 3 public class DrawThread extends Thread{ 4 //模拟用户账户 5 private Account account; 6 //当前取钱线程所希望的取钱数 7 private double drawAmount; 8 public DrawThread(String name, Account account, double drawAmount){ 9 super(name); 10 this.account = account; 11 this.drawAmount = drawAmount; 12 }; 13 //重复一百次取钱操作 14 public void run(){ 15 for(int i = 0; i < 100; i++){ 16 account.draw(drawAmount); 17 } 18 } 19 }
主程序可以启动任意多条存款线程和取钱线程,可以看到所有取钱线程必须等存款线程存钱后才可以向下执行,而取款线程也必须等取钱线程取钱后才可以向下执行。主程序代码如下:
1 package thread; 2 3 public class TestDraw { 4 public static void main(String[] args){ 5 //创建一个账户 6 Account acct = new Account("1234567",0); 7 new DrawThread("取钱者",acct,800).start(); 8 new DepositThread("存款者甲",acct,800).start(); 9 new DepositThread("存款者乙",acct,800).start(); 10 new DepositThread("存款者丙",acct,800).start(); 11 } 12 }
运行该程序将可看到存款者线程、取钱者交替执行的情形,每当存款者向账户存入800元之后,取钱者线程立即从账户中取出这笔钱。存款完成后账户余额总是800元,取钱结束后账户余额总是0元。运行该程序结果如下:
存款者甲 存款800.0 账户余额为: 800.0 取钱者取钱800.0 账户余额为:0.0 存款者丙 存款800.0 账户余额为: 800.0 取钱者取钱800.0 账户余额为:0.0 存款者丙 存款800.0 账户余额为: 800.0 取钱者取钱800.0 账户余额为:0.0 存款者甲 存款800.0 账户余额为: 800.0 取钱者取钱800.0 账户余额为:0.0 存款者乙 存款800.0 账户余额为: 800.0 取钱者取钱800.0 账户余额为:0.0
从上图可以看出,三个存款者线程随机地向账户中存钱,只有一个取钱者线程执行取钱。只有当取钱者取钱后,存款者才可以存款;同样只有等存款者存款后,取钱者线程才可以取钱。
程序最后被阻塞无法继续向下执行,这是因为3个存款者线程,共有300次存款操作,但1个取钱者操作只有100次取钱操作,所以程序最后被阻塞。
注意:上面这种情况并不是死锁,这种情况的取钱者线程已经执行结束,而存款者线程只是在等待其他线程来取钱而已,并不是等待其他线程释放同步监视器,不要把死锁和程序阻塞等同起来。
10.2 使用条件变量控制协调
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器对象,也就不能使用wait、notify、notifyAll方法来协调进程的运行。
当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程是否Lock对象,Condition对象也可以唤醒其他处于等待的线程。
Condition将同步监视器方法(wait、notify和notifyAll)分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。
实例实质上被绑定到一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象newCondition()方法即可。Condition类提供了如下三个方法:
await():类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。该await方法有更多变体:long awaitNanos(long nanosTimeout)等,可以完成更丰富的等待操作。
signal():唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程。
signalAll():唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
1 import java.util.concurrent.locks.*; 2 3 public class Account{ 4 //显示定义Lock对象 5 private final Lock lock = new ReentrantLock(); 6 //获得指定Lock对象对应的条件变量 7 private final Condition cond = lock.newCondition(); 8 9 private String accountNo; 10 private double balance; 11 12 //标识账户中是否已经存款的旗标 13 private boolean flag = false; 14 15 public Account(){ 16 17 } 18 19 public Account(String accountNo , double balance){ 20 this.accountNo = accountNo; 21 this.balance = balance; 22 } 23 24 public void setAccountNo(String accountNo){ 25 this.accountNo = accountNo; 26 } 27 public String getAccountNo(){ 28 return this.accountNo; 29 } 30 31 public double getBalance(){ 32 return this.balance; 33 } 34 public void draw(double drawAmount){ 35 //加锁 36 lock.lock(); 37 try{ 38 //如果账户中还没有存入存款,该线程等待 39 if (!flag){ 40 cond.await(); 41 }else{ 42 //执行取钱操作 43 System.out.println(Thread.currentThread().getName() + 44 " 取钱:" + drawAmount); 45 balance -= drawAmount; 46 System.out.println("账户余额为:" + balance); 47 //将标识是否成功存入存款的旗标设为false 48 flag = false; 49 //唤醒该Lock对象对应的其他线程 50 cond.signalAll(); 51 } 52 }catch (InterruptedException ex){ 53 ex.printStackTrace(); 54 } 55 //使用finally块来确保释放锁 56 finally{ 57 lock.unlock(); 58 } 59 } 60 public void deposit(double depositAmount){ 61 lock.lock(); 62 try{ 63 //如果账户中已经存入了存款,该线程等待 64 if(flag){ 65 cond.await(); 66 }else{ 67 //执行存款操作 68 System.out.println(Thread.currentThread().getName() + 69 " 存款:" + depositAmount); 70 balance += depositAmount; 71 System.out.println("账户余额为:" + balance); 72 //将标识是否成功存入存款的旗标设为true 73 flag = true; 74 //唤醒该Lock对象对应的其他线程 75 cond.signalAll(); 76 } 77 }catch (InterruptedException ex){ 78 ex.printStackTrace(); 79 } 80 //使用finally块来确保释放锁 81 finally{ 82 lock.unlock(); 83 } 84 } 85 86 public int hashCode(){ 87 return accountNo.hashCode(); 88 } 89 public boolean equals(Object obj){ 90 if (obj != null && obj.getClass() == Account.class){ 91 Account target = (Account)obj; 92 return target.getAccountNo().equals(accountNo); 93 } 94 return false; 95 } 96 }
用该程序和前面的进行对比,两个逻辑基本是类似的,现在显式使用Lock对象来充当同步监视器,使用Condition对象来暂停指定线程,唤醒指定线程。
其他的类与之前的完全一样。