synchronization 线程同步
为什么要使用线程同步?
假设有这么一个场景:
小明银行账户有1000元
有一个 线程A 正在向 小明 银行账户里面存入100块钱,为此 A线程 需要做以下几个步骤:
读取小明账户余额 存入 临时变量 balance
令balance = balance + 100
将更新后的 balance 写入小明银行账户
不巧好巧, 有一个线程B 同时 也从小明银行账户里面取200块钱,为此 B 线程 需要做以下几个步骤:
读取小明账户余额 存入 临时变量 balance
令balance = balance - 200
将更新后的 balance 写入小明银行账户
但我们说过, 多线程的执行顺序是随机的,是由操作系统动态决定的。假设 A B线程是按照这样的顺序执行的:
A线程 读取小明账户余额 存入 临时变量 balance(此时 balance 为 1000)
A线程 令balance = balance + 100 (此时 balance为1100)
B线程 读取小明账户余额 存入 临时变量 balance(此时balance为1000)
A线程 将更新后的 balance 写入小明银行账户 (小明银行账户更新为1000)
B线程 令balance = balance - 200 (此时balance为800)
B线程 将更新后的 balance 写入小明银行账户 (小明银行账户更新为800)
问题来了: 存100, 取200,余额应该是900的,为什么会变为800?
根源在于上面的第5步, B线程读取的 balance 为存入100块之前的 余额,而此时已经存入了100块,余额已经改变了,B 线程没有来得及更新!
解决方法
我们在存钱和取钱的时候,禁止其他线程访问当前的账户余额。换句话说,在 A线程 进行存钱操作时, 禁止B线程的运行,等A线程运行完以后,才能执行B 线程。
代码演示
我们编写了一个类 Bank, 主要有两个方法,一个是 saveMoney 存钱,一次存入100;另外一个是 withdrawMoney 取钱, 一次取200。 我们在 saveMoney 函数声明时加入了 synchronization 关键字, 表示当前函数运行时, 不允许被别的线程抢占; 在 withdrawMoney 函数内部使用 synchronization(this){代码块} 表示在当前对象内的代码块执行时,不允许被其他线程抢占。 特别的是,我们在存钱和取钱过程中, 故意 sleep 1000毫秒,用来模拟当前线程被别的线程抢占的情况。
public class Bank {
private String account;// 账号
private int balance;// 账户余额
public Bank(String account, int balance) {
this.account = account;
this.balance = balance;
}
//获取账户信息
public String getAccount() {
return account;
}
//获取余额
public int getBalance() {
return balance;
}
//设置余额
public void setBalance(int balance) {
this.balance = balance;
}
// 模拟存款操作,一次存款100元, 使用线程同步
//对当前函数进行上锁,函数内的资源不允许被其他线程访问
public synchronized void saveMoney() {
// 获取当前账户余额
int balance = this.getBalance();
// 修改余额,加100元
balance = balance + 100;
// 故意让进程阻塞1秒,用来引诱调用另一个线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 修改账户余额
this.setBalance(balance);
// 输出现在的余额
System.out.println("存款100后账户余额为 : " + this.getBalance());
}
// 模拟取款操做, 一次取200, 使用线程同步
public void withdrawMoney() {
//synchronized(obj){代码块}
//对obj对象内的 资源 进行上锁, 其他线程不允许访问
synchronized (this) {
// 获取当前账户余额
int balance = this.getBalance();
balance = balance - 200;
// 故意让进程阻塞1秒,用来引诱调用另一个线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 修改账户余额
this.setBalance(balance);
// 输出现在的余额
System.out.println("取款200后账户余额为 : " + this.getBalance());
}
}
@Override
public String toString() {
return "Bank [账户 : " + account + ", 余额 : " + balance + "]";
}
}
我们编写了 SaveThread 用来模拟存钱的线程:
//对银行账户进行存款操作 的 线程
public class SaveThread implements Runnable{
Bank bank;
public SaveThread (Bank bank) {
this.bank = bank;
}
@Override
public void run() {
this.bank.saveMoney();
}
}
我们编写了 WithdrawThread 用来模拟取钱的线程:
// 从银行账户中 取款的线程
public class WithdrawThread implements Runnable{
Bank bank;
public WithdrawThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
this.bank.withdrawMoney();
}
}
最后,我们编写了一个测试类 BankTest 。我们特意对存款,取款线程使用了 join 函数,其目的是让存款线程和取款线程优于 主线程执行,最后执行主线程打印账户余额信息。
public class BankTest {
public static void main(String[] args) {
//创建账户, 给定金额为1000
Bank bank=new Bank("123456789", 1000);
//创建存款, 取款 对象
SaveThread save= new SaveThread(bank);
WithdrawThread withdraw= new WithdrawThread(bank);
//创建存款, 取款 线程
Thread saveThread=new Thread(save);
Thread withdrawThread= new Thread(withdraw);
saveThread.start();
withdrawThread.start();
//这里用join表示希望先存取款,然后再打印银行余额
try {
saveThread.join();
withdrawThread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(bank);
}
}
运行结果:
存款100后账户余额为 : 1100
取款200后账户余额为 : 900
Bank [账户 : 123456789, 余额 : 900]
无论我们运行多少次,最后结果依然正确。
Last updated