阅读的书籍:《java疯狂讲义》
关键词:线程安全问题,同步代码块,同步方法,释放同步监视器的锁定,同步锁,死锁
线程安全问题:当使用多个线程来访问同一个数据时,会导致一些错误情况的发生
到底什么是线程安全问题呢,先看一个经典的案例:银行取钱的问题
模拟步骤:
1.匹配用户账户的正确性(这里就简化了)
2.用户输入取款金额
3.系统判断账户余额是否大于取款金额
4.返回取款成功或者失败
案例中一共就三个类,首先是Account:
/**
* 银行账户
*/
public class Account {
private String accountNo;//账户名
private double balance;//账户余额
public Account(String accountNo,double balance){
this.accountNo = accountNo;
this.balance = balance;
}
@Override
public int hashCode() {
return accountNo.hashCode();
}
@Override
public boolean equals(Object obj) {
if(obj == this){
return true;
}
if (obj != null && obj.getClass() == Account.class){
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
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;
}
}
然后是DrawThread:
/**
* 模拟取钱过程的线程
*/
public class DrawThread extends Thread {
private Account mAccount;//账户信息
private double mDrawAmount;//要取的金额
public DrawThread(String name, Account account, double drawAmount) {
super(name);
mAccount = account;
mDrawAmount = drawAmount;
}
@Override
public void run() {
if (mAccount.getBalance() >= mDrawAmount) {//如果 余额 >= 要取的数额,则进入取钱流程
System.out.println(getName() + "取钱成功,取出金额:" + mDrawAmount);
mAccount.setBalance(mAccount.getBalance() - mDrawAmount);//更新账户余额
System.out.println("账户余额:" + mAccount.getBalance());
} else {
System.out.println(getName() + "取钱失败,余额不足");
}
}
}
最后用DrawThread进行测试:
public class DrawTest {
public static void main(String[] args){
Account account = new Account("王尼玛",1000);
new DrawThread("小偷甲",account,800).start();
new DrawThread("小偷乙",account,800).start();
}
}
如果只是按上面的代码来运行的话,运行结果是这样的:
可以看到,这里就存在着一个问题:一开始账户中有1000块钱,小偷甲取走800后剩下200,若小偷乙继续取的话余额就不够了,可是这里却没有返回 “取钱失败,余额不足”,而是输出了一个负数:-600。两个并发线程同时在修改Account对象,出现了问题。
这就是线程安全问题的一个体现。那么如何解决呢?——使用同步监视器
同步代码块:使用同步监视器的通用方法就是同步代码块
语法格式:
synchronized(obj){
//需要同步的代码块
}
线程在开始执行同步代码块之前,必须先获得对同步监视器的锁定。
任何时刻只能由一个线程可以获得对同步监视器的锁定,当同步代码块执行完成之后,该线程会释放对该同步监视器的锁定
明白了这个原理之后,对DrawThread中的run()方法小改一下:
@Override
public void run() {
synchronized (mAccount) {//看这里
if (mAccount.getBalance() >= mDrawAmount) {//如果 余额 >= 要取的数额,则进入取钱流程
System.out.println(getName() + "取钱成功,取出金额:" + mDrawAmount);
mAccount.setBalance(mAccount.getBalance() - mDrawAmount);//更新账户余额
System.out.println("账户余额:" + mAccount.getBalance());
} else {
System.out.println(getName() + "取钱失败,余额不足");
}
}
}
可以看到,此时取钱的这块逻辑就是同步代码块,mAccount就是同步监视器了,取钱就是对mAccount进行操作,而经过这么一包裹之后,对于同一个账户来说,同一时间就只能有一个人对它进行取钱的操作了。再看看此时输出的结果:
嗯,小偷乙这会儿就取钱失败了,问题终于得到了解决。
同步方法:使用synchronized修饰某个方法来达到多线程安全
同步方法的同步监视器是调用该方法的对象,通过使用同步方法可以很方便地实现线程安全的类
线程安全的类的特点:
- 该类的对象可以被多个线程安全地访问
- 每个线程调用该对象的任意方法之后都将得到正确的结果,并且该对象状态依然保持合理状态
用同步方法对案例代码也进行小改:
//在Account类中加入这个方法
public synchronized void draw(double drawAmount){
if (getBalance() >= drawAmount) {//如果 余额 >= 要取的数额,则进入取钱流程
System.out.println(Thread.currentThread().getName() + "取钱成功,取出金额:" + drawAmount);
setBalance(getBalance() - drawAmount);//更新账户余额
System.out.println("账户余额:" + getBalance());
} else {
System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足");
}
}
然后回到DrawThread中调用:
@Override
public void run() {
mAccount.draw(mDrawAmount);
}
此时输出结果也是正确的,就不再重复贴图了
释放同步监视器的锁定:线程会在如下几种情况释放对同步监视器的锁定
- 当前线程的同步代码块,同步方法正常执行结束
- 遇到break,return终止了该同步代码块,同步方法的执行
- 出现未处理的Error或Exception,该方法异常结束
- 程序执行了同步监视器对象的wait()方法
与释放对应的是,当线程执行同步代码块,同步方法时,如下几种情况线程是不会释放同步监视器的:
- 程序调用Thread.sleep(),Thread.yield()
- 其他程序调用了该线程的suspend()方法
同步锁:通过显示定义同步锁对象(Lock)来实现同步
Lock是控制多个线程对共享资源进行访问的工具,通常每次只能由一个线程对Lock对象加锁,线程开始访问共享资源之前先要获得Lock对象
之所以说通常,是因为某些锁可能允许对共享资源并发访问,如ReadWriteLock
在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)
死锁:当两个线程互相等待对方释放同步监视器时就会发生死锁,导致所有线程处于阻塞状态,无法继续
由于篇幅有限,关于死锁就不展开了,后面会专门写一篇关于死锁的笔记或文章
注意事项:
- synchronized关键字可以修饰方法和代码块,但不能修饰构造器和成员变量等
- Thread类的suspend()方法很容易导致死锁,故Java不推荐使用这个方法