1线程同步
多线程编程是有趣的事情,它很容易突然出现“错误情况”,这是有系统的线程调度具有一定的随机性造成的,不过即使程序偶然出现问题,那也是由于编程不当引起的。当使用多个线程来访问同一个数据时,很容易“偶然”出现线程安全问题。
1.1线程安全问题
关于线程安全问题,有一个经典的问题——银行取钱的问题。银行取钱的基本流程基本上可以分为如下几个步骤。
- 用户数据账户、密码,系统判断用户的账户、密码是否匹配。
- 用户输入取款金额。
- 系统判断账户金额是否大于取款金额。
- 如果金额大于取款金额,则取款成功;如果金额小于取款金额,则取款失败。
乍一看上去,这个流程确实就是我们日常生活中的取款流程,这个流程没有任何问题。但一旦将这个流程放在多线程并发的场景下,就有可能出现问题。注意此处说的是有可能,并不是说一定。也许你的程序运行了一百万次都没有出现问题,但没有出现问题并不等于没有问题!
按上面的流程去编写取款程序,而且我么使用两个线程来模拟取钱操作,模拟两个人使用同一个账户并发取钱的问题。我们不管检查账户和密码的操作,仅仅模拟后面三步操作。下面先定义一个账户类,该账户类封装了账户编号和余额两个属性。
package Account;
public class Account {
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
public Account() { }
// 构造器
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo(){
return this.accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance(){
return this.balance;
}
public void getBalance(double balance) {
this.balance = balance;
}
// 下面方法根据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;
}
}
接下来提供一个取钱的线程类,该线程类根据执行账户、取钱数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统突出钞票,余额减少。
package Account;
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;
}
// 当多个线程修改用一个共享数据时,将涉及数据安全问题
public void run() {
// 账户金额大于取钱数目
if (account.getBalance() >= drawAmount) {
// 突出钞票
System.out.println(getName() + "取钱成功!取出钞票" + drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为:" + account.getBalance());
} else {
System.out.println(getName()+"取钱失败!余额不足!");
}
}
}
读者先不要管程序中那段被注释掉的粗体字代码,上面程序是一个非常简单的取钱逻辑,这个取钱逻辑与实际的取钱操作也很相似。程序的主程序非常简单,仅仅是创建一个账户,并启动两个线程从该账户中取钱,程序如下:
package Account;
public class DrawTest {
public static void main(String[] args) {
// 创建一个账户
Account acct = new Account("1234567", 1000);
// 模拟两个线程对用一个账号取钱
new DrawThread("ffzs", acct, 800).start();
new DrawThread("sleepycat", acct, 800).start();
}
}
多次运行上面程序,有可能会出现如下结果:
ffzs取钱成功!取出钞票800.0
sleepycat取钱成功!取出钞票800.0
余额为:200.0
余额为:-600.0
运行结果并不是我们所期望的结果(不过也有可能看到运行正确的效果),这正是多线程编程突然出现的“偶然”错误——因为线程调度的不确定性。假设系统线程调度器在粗体字代码处暂停,让另一个线程执行——为了强制暂停,只要取消上面程序中粗体字代码的注释即可。取消注释后再次编译DrawThread.java,并再次运行DrawTest类,将总可以看到上面的结果。
问题出现了:账户余额只有1000时取出了1600,而且账户余额出现了负值,这不是银行希望的结果。虽然上面程序是人为地使用Thread.sleep(1)来强制线程调度切换,但这种切换也是完全可能发生的——100000次操作只要有1次出现了错误,那就是编程错误引起的。
1.2同步代码块
出现取款问题的原因是run()方法的方法体不具备同步安全性——程序中有两个并发线程在修改Account对象;而且系统恰好在粗体字代码出执行线程切换,切换给另一个修改Account对象的线程,所以就出现了问题。
为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:
synchronized(obj)
{
...
//此处的代码就是同步代码块
}
上面语法格式中synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
虽然Java程序允许使用任何对象作为同步监视器,但想一下同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,我们应该考虑使用账户(account)作为同步监视器。我们把程序修改成如下形式:
package Account;
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;
}
// 当多个线程修改用一个共享数据时,将涉及数据安全问题
public void run() {
// 使用account作为同步监视器,任何线程进入下面同步代码块之前
// 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
// 这种做法符合:“加锁->修改->释放锁”的逻辑
synchronized (account) {
// 账户金额大于取钱数目
if (account.getBalance() >= drawAmount) {
// 突出钞票
System.out.println(getName() + "取钱成功!取出钞票" + drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为:" + account.getBalance());
} else {
System.out.println(getName() + "取钱失败!余额不足!");
}
}
}
}
通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性,运行之后结果如下:
ffzs取钱成功!取出钞票800.0
余额为:200.0
sleepycat取钱失败!余额不足!
1.3同步方法
与同步代码块对应,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于同步方法而言,无须显式指定同步监视器,同步方法的同步监视器是this,也就是该对象本身。
通过使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征。
- 该类的对象可以被多个线程安全地访问。
- 每个线程调用该对象的任意方法之后都将得到正确结果。
- 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
前面我们介绍了可变类和不可变类,其中不可变类总是线程安全的,因为它的对象状态不可改变;但可变对象需要额外的方法来保证其线程安全。例如上面的Account就是一个可变类,它的account和balance两个Field都可变,当两个线程同时修改Account对象的balance Field时,程序就出现了异常。下面我们将Account类对balance的访问设置成点成安全的,那么只要把balance的方法修改成同步方法:
package synchronizedMethod;
public class Account
{
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo=accountNo;
this.balance=balance;
}
public String getAccountNo(){
return this.accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
// 因为账户余额不允许随便修改,所以只为balance提供getter方法
public double getBalance()
{
return this.balance;
}
// 提供一个线程安全的draw()方法来完成取钱操作
public synchronized void draw(double drawAmount)
{
// 账户余额大于取钱数目
if (balance >=drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -=drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}
// 省略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;
}
}
上面程序中增加了一个代表取钱的draw()方法,并使用了synchronized关键字修饰方法,把该方法变成同步方法。同步方法的同步监视器是this,因此对于同一个Account账户而言,任意时刻只能有一个线程获得对Account对象的锁定,然后进入draw ()方法执行取钱操作——这样也可以保证多个线程并发取钱的线程安全。
因为Account类中已经提供了draw()方法,而且取消了setBalance()方法,DrawThread线程类需要改写,该线程类的run()方法只要调用Account对象的draw()方法即可执行取钱操作。run()方法代码片段如下:
package synchronizedMethod;
public class DrawThread extends Thread{
public Account account;
public double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 直接调用account对象的draw()方法来执行取钱操作
// 同步方法的同步监视器是this,this代表调用draw()方法的对象
// 也就是说,线程进入draw()方法之前,必须先对account对象
public void run() {
account.draw(drawAmount);
}
}
上面的DrawThread类无须自己实现取钱操作,而是直接调用account的draw()方法来执行取钱操作。由于已经使用synchronized关键字修饰了draw()方法,同步方法的同步监视器是this,而this总代表调用该方法的对象——在上面示例中,调用draw()方法的对象是account,因此多个线程并发修改同一份account之前,必须先对account对象加锁。这也符合了“加锁 → 修改→ 释放锁”的逻辑。
在Account里定义draw()方法,而不是直接在run()方法中实现取钱逻辑,这种做法更符合面向对象规则。在面向对象里有一种流行的设计方式:Domain Driven Design(领域驱动设计,DDD),这种方式认为每个类都应该是完备的领域对象,例如Account代表用户账户,应该提供用户账户的相关方法;通过draw()方法来执行取钱操作(实际上还应该提供transfer()等方法来完成转账等操作),而不是直接将setBalance()方法暴露出来任人操作,这样才可以更好地保证Account对象的完整性和一致性。
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略:
- 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如上面Account类中的accountNo属性就无须同步,所以程序只对draw()方法进行了同步控制。
- 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。
1.4释放同步监视器的锁定
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定。
- 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。
- 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
在如下所示的情况下,线程不会释放同步监视器。
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。当然,我们应该尽量避免使用suspend()和resume()方法来控制线程。
1.5同步锁(Lock)
从Java 5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁使用Lock对象充当。
Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock、ReadWriteLock
是Java5新提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类;为ReadWriteLock提供了ReentrantReadWriteLock实现类。
在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁,通常使用ReentrantLock的代码格式如下:
class X {
// 定义锁对象
private final ReentrantLock lock=new ReentrantLock();
// ...
// 定义需要保证线程安全的方法
public void m() {
// 加锁
lock.lock();
try {
// 需要保证线程安全的代码
// ... method body
}
// 使用finally块来保证释放锁
finally {
lock.unlock();
}
}
}
使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。通过使用ReentrantLock对象,我们可以把Account类改为如下形式,它依然是线程安全的:
package Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
public Account() {
}
// 构造器
public Account(String accountNo, double balance){
this.accountNo=accountNo;
this.balance=balance;
}
public String getAccountNo(){
return this.accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
// 因为账户余额不允许随便修改,所以只为balance提供getter方法
public double getBalance() {
return this.balance;
}
// 提供一个线程安全的draw()方法来完成取钱操作
public void draw(double drawAmount) {
lock.lock();
try {
// 账户余额大于取钱数目
if (balance >= drawAmount) {
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
} else {
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}finally {
lock.unlock();
}
}
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;
}
}
使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样都符合“加锁→修改→释放锁”的操作模式,而且使用Lock对象时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同一时刻只能有一个线程能进入临界区。
同步方法或同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。
虽然同步方法和同步代码块的范围机制使得多线程安全编程非常方便,而且还可以避免很多涉及锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。Lock提供了同步方法和同步代码块所没有的其他功能,包括用于非块结构的tryLock()方法,以及试图获取可中断锁的lockInterruptibly()方法,还有获取超时失效锁的tryLock(long, TimeUnit)方法。
ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁, ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
1.6死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下,如下程序将会出现死锁:
class A {
public synchronized void foo( B b ) {
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法" ); //①
try {
Thread.sleep(200);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); //③
b.last();
}
public synchronized void last() {
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar( A a ) {
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法" ); //②
try {
Thread.sleep(200);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); //④
a.last();
}
public synchronized void last() {
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo()方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar()方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
// 以dl为target启动新线程
new Thread(dl).start();
// 调用init()方法
dl.init();
}
}
运行上面程序,会看到如下结果:
当前线程名: 主线程 进入了A实例的foo方法
当前线程名: 副线程 进入了B实例的bar方法
当前线程名: 副线程 企图调用A实例的last方法
当前线程名: 主线程 企图调用B实例的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不再推荐使用该方法来暂停线程的执行。
2 Java实例练习
2.1模拟淘宝网购物买卖双方交易
淘宝网是亚太地区最大的网络零售商圈,其现在业务跨越C2C(个人对个人)、B2C(商家对个人)两大部分。在淘宝网上,消费者也可以通过注册并开店而成为卖家。淘宝网的交易方式大多使用的是支付宝功能,对于网上交易,线程的同步是非常重要的。本例我们使用同步机制,实现支付宝存入和支出交易账号的一致性。
2.1.1
新建项目TaobaoStore在该类的主方法中创建买方和卖方两种对象,然后创建Alipay支付宝类,定义相应的成员字段和成员方法,以及线程的开启和等待方法。核心代码如下所示:
package TaobaoStore;
class Alipay {
private final String[] goods; // 标识卖家商品,数组的长度是多少则标识商品的数量是多少
private int n; // 存入支付宝交易数量
private int m; // 取出数量
private int count; //缓冲内的交易数量
public Alipay(int count) {
// 构造方法初始化
this.goods = new String[count]; // 创建字符串数组
this.m = 0;
this.n = 0;
this.count = 0;
}
public synchronized void storage(String alipay) {
System.out.println("淘宝用户ID:" + Thread.currentThread().getName()
+ "\t支付宝存入" + alipay);
try {
while (count >= goods.length) {
wait(); // 线程等待
}
goods[n] = alipay; // 放置支付宝账号于数组
n = (n + 1) % goods.length;
count++;
notifyAll();
} catch (Exception e) { // 捕获异常
System.out.println("支付宝存入功能出现错误:" + e.getMessage());
}
}
public synchronized String outlay() { // 从支付宝中支出
String alipay = null;
try {
while (count <= 0) {
wait(); // 线程等待
}
alipay = goods[m]; // 取出指定的支付宝账号
m = (m + 1) % goods.length;
count--; // 数组个数减1
notifyAll();
} catch (Exception e) { // 捕获异常
System.out.println("支付宝支付功能出现错误:" + e.getMessage());
}
System.out.println("淘宝用户ID:" + Thread.currentThread().getName()
+ "\t支付宝支出" + alipay);
return alipay;
}
}
2.1.2
定义买家线程类,实现相应Thread类的方法,核心代码如下所示:
package TaobaoStore;
import java.util.Random;
class Buyer extends Thread { // 买家线程类
private final Random random;
private final Alipay alipay;
private static int id = 0; // 交易的流水号
public Buyer(String name, Alipay alipay, long seed) {// 构造方法进行初始化
super(name);
this.alipay = alipay;
this.random = new Random(seed);
}
public void run() { // 实现Thread类的方法,启动线程
try {
while (true) {
Thread.sleep(random.nextInt(1000));// 随机休眠
String flowerID = "交易流水账号:" + nextId();
alipay.storage(flowerID); // 存入支付宝中
}
} catch (Exception e) { // 捕获异常
}
}
private static synchronized int nextId() {
return id++;
}
}
2.1.3
package TaobaoStore;
import java.util.Random;
class Seller extends Thread { // 卖家线程类
private final Random random;
private final Alipay alipay;
// 构造方法进行初始化
public Seller(String name, Alipay alipay, long seed) {
super(name);
this.alipay = alipay;
this.random = new Random(seed); // 创建随机对象
}
public void run() { // 实现Thread类的方法,启动线程
try {
while (true) {
String alipay = this.alipay.outlay();
Thread.sleep(random.nextInt(1000));
}
} catch (Exception e) { // 捕获异常
System.out.println("买家支付预付款出错:" + e.getMessage());
}
}
}
2.1.4
创建TaobaoStore类进行操作:
package TaobaoStore;
public class TaobaoStore {
public static void main(String[] args) {
Alipay alipay = new Alipay(2); // 创建两个支付宝
// 创建实例并启动线程
new Seller("ffzs(seller)", alipay, 5).start();
new Seller("sleepycat(seller)", alipay, 7).start();
new Buyer("泛泛之素(buyer)", alipay, 101).start();
new Buyer("懒猫(buyer)", alipay, 102).start();
}
}
notify()方法是唤醒一个调用了wait()方法的等待线程。如果多个线程都调用了wait()方法处于等待状态,那么,程序无法控制哪个线程会得到通知调用,而是由JVM决定到底哪一个线程会被调用和执行。如果没有任何一个线程在等待,那么这个方法将不做任何事情。
notifyAll()方法会唤醒调用了wait()方法等待的所有线程,这些线程会重新排队竞争CPU的使用资源,线程会从上次因调用wait()方法而中断的地方开始继续运行同步方法。但是,程序同样并不能决定哪一个线程会最终被调用并执行。
2.2房门终于被打开了(解决死锁的方法)
在使用多线程的时候,很可能会发生这样一种情况,由于使用synchronized关键字锁定了某一个资源,而使得多个线程之间出现相互之间连续循环等待的现象,使得线程彼此之间的通信中断,任何一个线程都无法继续执行下去,这种情况被称之为“死锁”。在实际的应用中,使用多线程时需要采取一些方法尽量避免死锁的产生。我们可以通过一个使用两把钥匙打开房门的实例,来说明解决死锁的方法。
2.2.1
新建项目DoorOpen,并在其中创建一个DoorOpen.java文件。在该类的主方法中创建一个防盗门类,使其只能在两把钥匙都存在的情况下才能打开门,这就需要两个线程按相同的顺序获取锁。核心代码如下所示:
package DoorOpen;
public class DoorOpen {
static String[] keys = new String[] { "第1把钥匙", "第2把钥匙" };
static class DoorKey1 extends Thread { // 静态内部类
public void run() {
synchronized (keys[0]) { // 在同一时间只能有一个类访问
System.out.println("我拿起了" + keys[0] + ",在等着朋友用" + keys[1]
+ "开防盗门");
try {
Thread.sleep(100); // 线程休眠
} catch (Exception e) { // 捕获异常
System.out.println("线程休眠出错:" + e.getMessage());
}
synchronized (keys[1]) {
System.out.println("我又拿出来" + keys[1] + "打开了防盗门");
}
}
}
}
static class DoorKey2 extends Thread { // 静态内部类
public void run() {
synchronized (keys[0]) {
System.out.println("\n朋友拿出了" + keys[0] + ",在等待我用" + keys[1]
+ "开防盗门");
try {
Thread.sleep(100); // 线程休眠
} catch (Exception e) { // 捕获异常
System.out.println("线程休眠出错:" + e.getMessage());
}
synchronized (keys[1]) {
System.out.println("朋友又拿出了" + keys[1] + "打开了防盗门");
}
}
}
}
static class Gowrong extends Thread { // 静态守护线程类
public Gowrong() {
this.setDaemon(true); // 线程设置守护
}
public void run() {
while (true) {
try {
Thread.sleep(1000); // 线程休眠
} catch (Exception e) { // 捕获异常
System.out.println("线程休眠出错:" + e.getMessage());
}
System.out.println("守护线程:程序正在运行...");
}
}
}
public static void main(String[] args) { // Java程序主入口处
DoorKey1 one = new DoorKey1(); // 实例化对象
DoorKey2 two = new DoorKey2();
Gowrong daemon = new Gowrong();
one.start();
two.start();
daemon.start();
}
}
运行程序,结果如下:
我拿起了第1把钥匙,在等着朋友用第2把钥匙开防盗门
我又拿出来第2把钥匙打开了防盗门
朋友拿出了第1把钥匙,在等待我用第2把钥匙开防盗门
朋友又拿出了第2把钥匙打开了防盗门
Java语言本身并没有提供防止死锁的具体方法,但是在具体程序设计时必须要谨慎,以防止出现死锁现象。通常在程序设计中注意不用使用stop()、suspend()、resume()以及destroy()方法。
stop()方法不安全,它会解除由该线程获得的所有对象锁,而且可能使对象处于不连贯状态,如果其他线程此时访问对象,而导致的错误很难被检查出来。suspend()和resume()方法也极不安全,调用suspend()方法时,线程会停下来,但是该线程并没有放弃对象的锁,导致其他线程并不能获得对象锁。调用destroy()会强制终止线程,但是该线程也不会释放对象锁。