在多线程情况下,资源被多个线程访问,就会出现一些问题
下面看一个例子,写一个模拟银行操作,就只完成取钱操作,先看代码。
class Account {
private String name;
private int balance;
public Account(String name, int balance) {
this.name = name;
this.balance = balance;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
这是一个账户类,只有简单的属性:账户名和余额
class Bank {
private Account account;
public Bank() {
account = new Account("card",100);
}
public void drawMoney(int money) {
if(money <= account.getBalance()) {
System.out.println("取钱成功 :"+ money);
account.setBalance(account.getBalance()-money);
System.out.println("当前账户余额 :" + account.getBalance());
} else {
System.out.println("取钱失败");
}
}
}
这是一个银行类,这里只写了一个银行账户,只是为了说明问题
public class ChangeAccount {
public static void main(String[] args) {
Bank bank = new Bank();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bank.drawMoney(100);
}).start();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bank.drawMoney(100);
}).start();
}
}
现在我们起了两个线程对银行的这个账户进行取钱操作,虽然我们在取钱业务逻辑上写了判断账户余额和要取钱数之间的关系,如果要取的钱大于余额,则会提示取钱失败。
但是当我们是两个线程进行访问时来看看运行结果
很清楚,账户只有100块,但是却取出来了200元,这就是多线程所带来的并发安全问题。
java针对这种情况提供了同步监视器,先来看看解决办法1:
同步代码块
语法:
synchronize (obj) {
//需要同步的代码
}
obj就是同步监视器,在该代码块中就会实现线程同步功能,当很多线程访问时,只会有一个线程进入,对同步监视器进行加锁,然后执行代码后,释放锁。这时其他线程才可以进行访问。也就是说这段代码同一时间只会有一个线程执行。
同步监视器可以是任何对象,但是我们的同步代码块就是为了完成资源的同步,所以我们要把可能被并发访问的共享资源充当为同步监视器。
我们对代码进行如下修改
public void drawMoney(int money) {
synchronized (account) {
if(money <= account.getBalance()) {
System.out.println("取钱成功 :"+ money);
account.setBalance(account.getBalance()-money);
System.out.println("当前账户余额 :" + account.getBalance());
} else {
System.out.println("取钱失败");
}
}
}
这样我们再去运行就会得到正确结果了
除了同步代码块,我们还可以有同步方法
同步方法
被synchroized修饰的方法就叫做同步方法,同步方法可以被多个线程安全的进行访问,同步方法无需指定同步监视器,默认为this,也就是调用该方法的对象,我们这里也就是bank,对代码这样修改也是可以的
public synchronized void drawMoney(int money) {
if(money <= account.getBalance()) {
System.out.println("取钱成功 :"+ money);
account.setBalance(account.getBalance()-money);
System.out.println("当前账户余额 :" + account.getBalance());
} else {
System.out.println("取钱失败");
}
}
运行结果也是正确的。
需要注意的是:线程安全是降低效率来换取安全,所以我们并不需要把所有的方法都写为同步方法,只需要对那些对共享资源修改的方法设置为同步方法。
如果有两种运行需求,也就是有单线程和多线程需求,最好做两套版本,以保证性能。
同步代码块最好不要阻塞的方法,eg:InputStream.read()这种阻塞方法。
同步代码块最好不要调用其他对象的同步方法:容易出现死锁。
Lock
java同时提供更为灵活的同步机制,那就是锁
比较常用的锁就是ReentrantLock(可重入锁),可以显式的对Lock对象加锁,释放锁,使用起来更加灵活方便。
我们用锁来改进我们的代码
class Bank {
private Account account;
//new一个锁对象
private final ReentrantLock reentrantLock = new ReentrantLock();
public Bank() {
account = new Account("card",100);
}
public void drawMoney(int money) {
//加锁
reentrantLock.lock();
try{
if(money <= account.getBalance()) {
System.out.println("取钱成功 :"+ money);
account.setBalance(account.getBalance()-money);
System.out.println("当前账户余额 :" + account.getBalance());
} else {
System.out.println("取钱失败");
}
} finally {
//释放锁
reentrantLock.unlock();
}
}
}
我们必须通过unlock来释放锁,否则就会出现死锁,为了保证关闭,我们最好用finally语句去关闭锁。