线程安全(synchronized)
项目最后是发布运行在服务器上的,服务器已经实现了多线程,所以我们在编写程序时需要关注的其实是项目中的数据在多线程并发环境下的安全问题。
先来看一个例子:(只是例子,忽略生活)
A和B同时去操作一个余额为1w的银行账户,A先去ATM机中取1w,然后这个时候发生了网络延迟,银行账户没有更新现在的余额为0,然后这个时候B去人工服务窗口中查询余额,发现有1w,然后B又取了1w。
这种情况肯定是不允许发生的,此时就是因为多线程并发而出现的安全问题。
1.线程不安全的条件
- 多线程并发;
- 有共享数据;
- 修改了共享数据。
模拟银行取款案例出现的不安全问题:
BankAcount类:
package com.dh.threadsafe;
public class BankAccount {
private String AccountNo;
private double balance;
public BankAccount(String accountNo, double balance) {
AccountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return AccountNo;
}
public void setAccountNo(String accountNo) {
AccountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款方法
public void withDrawMoney(double money){
//账户余额
double beforeBalance = this.getBalance();
//取款
double afterBalance = balance - money;
//在这里睡眠1s,保证t2操作的时候,账户余额还是10000
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改账户余额
this.setBalance(afterBalance);
System.out.println(Thread.currentThread().getName()+"取款"+money+",账户余额为:"+afterBalance);
}
}
线程类:
package com.dh.threadsafe;
public class MyThread extends Thread {
private BankAccount bankAccount;
public MyThread(BankAccount bankAccount) {
this.bankAccount = bankAccount;
}
@Override
public void run() {
//调用取款方法
bankAccount.withDrawMoney(5000);
}
}
测试类:
package com.dh.threadsafe;
public class Test {
public static void main(String[] args) {
BankAccount account = new BankAccount("001", 10000);
MyThread t1 = new MyThread(account);
MyThread t2 = new MyThread(account);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
结果:
t1取款5000.0,账户余额为:5000.0
t2取款5000.0,账户余额为:5000.0
所以就乱套了,t1和t2共取了10000元,但是账户余额却还显示5000元。
2.如何解决线程安全问题?
分析上述发生线程不安全问题的原因可以很轻易的解决:
只要在确保A取完钱,然后银行账户中的余额发生了改变之后,再允许B操作该账户余额即可。
线程排队执行,即线程同步机制。
这里涉及到同步和异步的概念:
- 异步:线程t1和线程t2各自执行,t1不管t2的执行,t2也不管t1的执行。(即多线程并发,效率高,安全性低);
- 同步:线程t2执行时,必须先等待线程t1执行结束。(即线程排队执行,效率低,安全性高)。
(以安全为主要考虑)
3.实现线程安全
(1)synchronized代码块
//语法
synchronized(){ //()中填写需要实现线程同步的线程共享的资源
//线程同步代码块
}
修改代码为:
//取款方法
public void withDrawMoney(double money) {
synchronized (this) { //不一定是this,只要是排队线程共享的资源即可
//账户余额
double beforeBalance = this.getBalance();
//取款
double afterBalance = balance - money;
//在这里睡眠1s,保证B操作的时候,账户余额还是10000
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改账户余额
this.setBalance(afterBalance);
System.out.println(Thread.currentThread().getName() + "取款" + money + ",账户余额为:" + afterBalance);
}
}
结果:
t1取款5000.0,账户余额为:5000.0
t2取款5000.0,账户余额为:0.0
代码执行原理:
前提:Java中的每一个对象都有一把对象锁,实际上就是一个标识。
- 假设t1线程和t2线程并发,肯定会有一个线程先执行代码,一个线程后执行代码;
- 假设现在是t1线程先执行代码,那么t1线程遇到synchronized关键字时,会自动的去找()中共享对象的锁,找到之后就会占有这把锁,然后执行同步代码块中的同步代码(在程序执行过程中会一直占有这把锁),直到执行同步代码块结束,就会释放这把锁。
- 假设t1还在运行同步代码块时,t2线程也遇到了synchronized关键字,则也会自动的去找()中共享对象的锁,但是此时锁被t1线程占有了,t2线程就只能在同步代码块外面等待这把锁,等到锁之后,t2也会占有这把锁,执行同步代码块,执行完毕归还锁。
其实也很像排队上卫生间的思想.......
这里涉及到锁池的概念:
(2)同步实例方法
用synchronized修饰实例方法:(只适用于共享数据为this时,不灵活)
//取款方法
public synchronized void withDrawMoney(double money) {
//账户余额
double beforeBalance = this.getBalance();
//取款
double afterBalance = balance - money;
//在这里睡眠1s,保证B操作的时候,账户余额还是10000
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改账户余额
this.setBalance(afterBalance);
System.out.println(Thread.currentThread().getName() + "取款" + money + ",账户余额为:" + afterBalance);
}
结果:
t1取款5000.0,账户余额为:5000.0
t2取款5000.0,账户余额为:0.0
扩大同步范围:
同步代码块的范围比同步实例方法的范围要小;同步范围越小,效率越高。
但是当共享数据为this,并且整个方法体都需要同步时,还是建议使用同步实例方法。优点是代码少。
(3)同步静态方法
同步静态方法时,找的是类锁:100个对象,也只有一把类锁。100个对象,有100把对象锁。
用synchronized修饰静态方法。
(此处不举例子,在下一篇synchronized面试题中会举例)
4.哪些变量有线程安全问题?
java中有三大变量:
- 局部变量,存储在栈中,线程不共享;
- 实例变量,存储在堆中,线程共享;
- 静态变量,存储在方法区中,线程共享。
只有实例变量和静态变量(都是成员变量)才会存在线程安全问题;局部变量不存在线程安全问题。
常量也不存在线程安全问题。
5.synchronized缺点
synchronized会让程序的执行效率低,系统的吞吐量低,用户体验差(特别是一些秒杀、抢票等功能),不得己的情况下才使用synchronized。
- 如果可以使用局部变量,就尽量使用局部变量代替实例变量和静态变量;
- 如果必须使用实例变量,就尽量多使用几个对象,最好一个线程一个对象;
- 如果既不能使用局部变量又不能多个对象的话,就只能使用synchronized了。