多
线程编程是有趣的事情,当常常容易突然出现“
错误的情况”,这是由于系统的线程调度具有一定的随机性。即使是程序运行期间偶尔出现的问题,那也是由于我们的编程不当所引起的。当使用多个线程来访问同一个数据时,非常容易出现
线程安全问题。
关于线程安全问题,有一个经典的问题:
银行取钱问题。银行取钱的基本流程可以分为如下几个步骤:
【1】用户输入账户,密码,系统判断用户的账户,密码是否匹配。
【2】用户输入取款金额。
【3】系统判断账户余额是否大于取款金额。
【4】如果余额大于取款金额,取款成功,否则取款失败。
这个流程看上去没有任何问题。但一旦将这个流程放在多线程并发的场景下,就有可能出现问题,但不是说一定。也许程序运行一百万次都没有出现问题,没有出现问题并不等于没有问题!这就是多线程访问同一数据的时候,数据
安全性问题,在Java中主要有
三种方法解决线程安全问题:
同步代码块,同步方法,同步锁。在这里简要介绍下同步代码块方法。
同步代码块语法格式如下:
synchronized(){
...
//此处代码就是同步代码块
}
在以上语法中synchronized后括号里的obj就是同步监视器,以上代码的含义就是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。显然模拟一个取款过程可建立三个类:用户账户,取款类,测试类
用户类Account代码:
package com.
thread.test;
/**
* 用户类
*
* @author wwb
*
*/
public class Account
{
/**
* 账户
*/
private String accountNo;
/**
* 账户余额
*/
private double balance;
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;
}
public void setBalance(double balance)
{
this.balance = balance;
}
public double getBalance()
{
return this.balance;
}
//下面两个方法根据accountNo来计算Account的
hashCode和判断equals
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if (obj != null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
由于同步监视器的目的就是:阻止两条线程对同一个共享资源进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,我们应该考虑使用账户作为同步监视器。因此以下的模拟取款程序代码如下:
取款类DrawThread代码:
package com.thread.test;
/**
* 取款类
*
* @author wwb
*
*/
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 ex) {
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
} else {
System.out.println(getName() + "取钱失败!余额不足!");
}
}
}
}
测试类TestDraw代码:
package com.thread.test;
/**
* 测试类 *
* @author wwb
*
*/
public class TestDraw {
/**
* @param args
*/
public static void main(String[] args) {
// 创建一个账户
Account acct = new Account("1234567", 1000);
// 模拟两个线程对同一个账户取钱
new DrawThread("甲", acct, 800).start();
new DrawThread("乙", acct, 800).start();
}
}