线程同步
线程安全问题(银行取钱案例)
银行取钱的基本流程基本上可以分为以下步骤
- 用户输入账户,密码,判断用户账户,密码是否匹配(此处省略这一步)
- 用户输入取款金额
- 系统判断账户余额是否大于取款金额
- 如果余额大于存款金额,则取款成功;如果余额小于存款金额,则取款失败
package thread;
/**
* @author Mike
* @use 账户类
*/
public class Account {
private String accountNo;
private double balance;
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public Account(){}
public Account(String accountNo,double balance){
this.accountNo=accountNo;
this.balance=balance;
}
public int hashCode(){
return accountNo.hashCode();
}
public boolean equals(Object object){
if(this == object){
return true;
}
if (object!=null && object.getClass() == Account.class){
Account target = (Account)object;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
package thread;
/**
* @author Mike
* @use 提供一个取钱的线程类,该线程类根据执行账户、取钱数量进行取钱操作
* ,取钱的逻辑是当余额不足时,提示余额不足,当余额大于取的钱数时,余额减少
*/
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(){
//synchronized (account){
if (account.getBalance() >= drawAmount){
System.out.println(getName()+"取钱成功,取出"+drawAmount);
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为:"+account.getBalance());
}else {
System.out.println(getName() + "取钱失败!余额不足!");
}
//注释内容为同步代码块
// }
}
}
测试类:
package thread;
/**
* @author Mike
* @use 银行取钱测试类
*/
public class DrawTest {
public static void main(String[] args) {
Account account = new Account("1234567" , 1000);
new DrawThread("甲" ,account ,800).start();
new DrawThread("乙",account,600).start();
}
}
加入两个人同时从这个账户里取钱,可能会出现余额小于0的情况(可以这样理解:甲取出钱,在余额还没有来得及更新的时候,乙也来取钱了),显然这样的情况我们不想让它发生,所以我们用同步代码块来解决共享数据的安全性问题
同步代码块
synchronized(obj){
...
//此处的代码就是同步代码块
}
对Account类进行修改
把18行注释打开就好了,当然别忘了打开尾括号的注释
同步锁lock()
比较常用的是ReentrantLock(可重入锁)(反正书上说这个常用,我还没做过关于线程的项目,也不清楚)
使用时的代码格式如下
class X{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//定义需要保证线程安全的方法
public void m(){
//加锁
lock.lock();
try{
//需要保证线程安全的代码
}
//使用finally块来保证释放锁
finally{
lock.unlock();
}
}
}
下面是银行问题用lock改写的代码
Account类
package thread._16_6;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Mike
* @use 使用ReentrantLock重新完成锁内容
*/
public class Account {
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
private String accountNo;
private double balance;
public Account(){}
public Account(String accountNo,double balance){
this.accountNo=accountNo;
this.balance=balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
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 e) {
e.printStackTrace();
}
balance -=drawAmount;
System.out.println("余额为:"+balance);
}else{
System.out.println(Thread.currentThread().getName()+"取钱失败,余额不足");
}
}finally {
//修改完成,释放锁
lock.unlock();
}
}
}
线程类
package thread._16_6;
/**
* @author Mike
* @use 提供一个取钱的线程类,该线程类根据执行账户、取钱数量进行取钱操作
* ,取钱的逻辑是当余额不足时,提示余额不足,当余额大于取的钱数时,余额减少
*/
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;
}
//当多个线程修改同一个共享数据时,将涉及数据安全问题
@Override
public void run() {
account.draw(this.drawAmount);
}
}
测试类
package thread._16_6;
/**
* @author Mike
* @use 银行取钱测试类
*/
public class DrawTest {
public static void main(String[] args) {
Account account = new Account("1234567" , 1000);
new DrawThread("甲" ,account ,800).start();
new DrawThread("乙",account,600).start();
}
}
其实到这里我们又发现了一个问题
同步代码块相当于是厕所(恶臭的好例子),一个人进去,另外一个人只有等他出来才能进去。
假如有这样一种情况,手纸是另一个同步代码块,甲拿了纸,想进厕所,乙在厕所,没带纸想用纸。哦吼,这就是个死锁了。关于死锁的问题下一次复习。