线程安全问题
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
取钱模型演示
需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。
如果小明和小红同时来取钱,而且2人都要取钱10万元,可能出现什么问题呢?
线程安全问题出现的原因?
- 1、存在多线程并发
- 2、同时访问共享资源
- 3、存在修改共享资源
线程安全问题案例模拟
有安全隐患的:
/**
* 需求:模拟取钱案例
* 分析:同一个账户交给了两个线程,两个线程同时跑各自的run方法
*/
public class ThreadDemo {
public static void main(String[] args) {
//1、定义一个线程类,创建一个共享的账户对象
Account account = new Account("1234",100000);
///2、创建两个线程对象代表小明和小红进入系统同时取钱
new DrawThread(account,"小明").start();
new DrawThread(account,"小红").start();
}
}
/**
* 取钱的线程类
*/
public class DrawThread extends Thread{
//接收处理的账户对象
private Account acc;
public DrawThread(Account acc,String name){
super(name);
this.acc = acc;
}
@Override
public void run() {
//取钱的
acc.drawMoney(100000);
}
}
public class Account {
private String cardId;
private double money;//账户的余额
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public Account() {
}
/**
* 小明和小红两个人都到这来了
* @param money 代表要取的钱
*/
public void drawMoney(double money) {
//1、获取是谁来取钱(获取当前的线程,也就是说小明线程执行这个方法就是小明线程对象,也就是小明取钱)
/*线程的名字就是人命*/
String name = Thread.currentThread().getName();
//2、判断账户钱是否够
if (this.money >=money){
//3、取钱
System.out.println(name+"取钱成功,吐出:"+money);
//4、更新余额(这么做是故意出bug,如果将下边这行代码放到取钱上边可能就比现在安全)
this.money -= money;
System.out.println(name+"取钱后剩余"+this.money);
}else{
//5、余额不足
System.out.println(name+"取钱,余额不足!");
}
}
}
线程安全问题发生的原因是什么?
- 多个线程同时访问同一个共享资源且存在修改该资源。
线程同步
- 为了解决线程安全问题。
取钱案例出现问题的原因?
- 多个线程同时执行,发现账户都是够钱的。
如何才能保证线程安全呢?
- 让多个线程实现先后依次访问共享资源,这样就解决了安全问题
线程同步的核心思想
加锁
,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
线程同步解决安全问题的思想是什么?
- 加锁:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
线程同步方式一:同步代码块
同步代码块
作用:
把出现线程安全问题的核心代码给上锁。原理:
每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
锁对象要求- 理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。
锁对象用任意唯一的对象好不好呢?
- 不好,会影响其他无关线程的执行。
锁对象的规范要求
- 规范上:
建议使用共享资源作为锁对象
。 - 对于实例方法建议使用
this
作为锁对象。 - 对于静态方法建议使用
字节码(类名.class)
对象作为锁对象。
同步代码块是如何实现线程安全的?
- 对出现问题的核心代码使用
synchronized
进行加锁 - 每次只能一个线程占锁进入访问
同步代码块的同步锁对象有什么要求?
- 理论上锁对象可以使用任意的唯一的对象,但会锁住无关线程,所以对于
实例方法
建议使用this
作为锁对象。 - 对于
静态方法
建议使用字节码(类名.class)
对象作为锁对象。
public class Account {
private String cardId;
private double money;//账户的余额
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public Account() {
}
//静态
/* public static void run(){
synchronized (Account.class){
//出现问题的核心代码
}
}*/
/**
* 小明和小红两个人都到这来了
* @param money 代表要取的钱
*/
public void drawMoney(double money) {
//1、获取是谁来取钱(获取当前的线程,也就是说小明线程执行这个方法就是小明线程对象,也就是小明取钱)
/*线程的名字就是人命*/
String name = Thread.currentThread().getName();
//同步代码块(需要声明一个锁对象,这个锁对象对于这两个线程来说是唯一的就可以)
//synchronized ("") {//字符串字面量如果用双引号的形式,它在常量池中只有一个
//this == acc 共享账户
synchronized (this) {//这个this只会锁小明和小红,不会锁别的账户(卡号做锁,因为是字符串,可能会重复
// 但this代表的是一个地址,不会重复)this指代当前对象的地址!对象的地址是在这个对象new出来之后才有的。所以可以作为一把不影响别人的锁
//2、判断账户钱是否够
if (this.money >=money){
//3、取钱
System.out.println(name+"取钱成功,吐出:"+money);
//4、更新余额(这么做是故意出bug,如果将下边这行代码放到取钱上边可能就比现在安全)
this.money -= money;
System.out.println(name+"取钱后剩余"+this.money);
}else{
//5、余额不足
System.out.println(name+"取钱,余额不足!");
}
}
}
}
/**
* 取钱的线程类
*/
public class DrawThread extends Thread{
//接收处理的账户对象
private Account acc;
public DrawThread(Account acc, String name){
super(name);
this.acc = acc;
}
@Override
public void run() {
//取钱的
acc.drawMoney(100000);
}
}
/**
* 需求:模拟取钱案例
* 分析:同一个账户交给了两个线程,两个线程同时跑各自的run方法
*/
public class TestSafeDemo {
public static void main(String[] args) {
//1、定义一个线程类,创建一个共享的账户对象
Account account = new Account("1234",100000);
///2、创建两个线程对象代表小明和小红进入系统同时取钱
new DrawThread(account,"小明").start();
new DrawThread(account,"小红").start();
}
}
线程同步方式二:同步方法
同步方法
作用:
把出现线程安全问题的核心方法给上锁。原理:
每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
格式
/**
* 小明和小红两个人都到这来了
*
* @param money 代表要取的钱
*/
//方法上加synchronized
public synchronized void drawMoney(double money) {
//1、获取是谁来取钱(获取当前的线程,也就是说小明线程执行这个方法就是小明线程对象,也就是小明取钱)
/*线程的名字就是人命*/
String name = Thread.currentThread().getName();
//2、判断账户钱是否够
if (this.money >= money) {
//3、取钱
System.out.println(name + "取钱成功,吐出:" + money);
//4、更新余额(这么做是故意出bug,如果将下边这行代码放到取钱上边可能就比现在安全)
this.money -= money;
System.out.println(name + "取钱后剩余" + this.money);
} else {
//5、余额不足
System.out.println(name + "取钱,余额不足!");
}
}
同步方法底层原理
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用
this
作为的锁对象。但是代码要高度面向对象! - 如果方法是静态方法:同步方法默认用
类名.class
作为的锁对象。
是同步代码块好还是同步方法好一点?
- 同步代码块锁的范围更小,同步方法锁的范围更大。但是同步方法锁的方式还是更常用一些
同步方法是如何保证线程安全的?
- 对出现问题的核心方法使用synchronized修饰
- 每次只能一个线程占锁进入访问
同步方法的同步锁对象的原理?
- 对于实例方法默认使用this作为锁对象。
- 对于静态方法默认使用类名.class对象作为锁对象。
线程同步方式三:Lock锁
Lock锁
- 为了更清晰的表达如何加锁和释放锁,
JDK5
以后提供了一个新的锁对象Lock
,更加灵活、方便。 Lock
实现提供比使用synchronized
方法和语句可以获得更广泛的锁定操作。Lock
是接口不能直接实例化,这里采用它的实现类ReentrantLock
来构建Lock
锁对象。
Lock的API
对以下代码做了修改:
public class Account {
private String cardId;
private double money;//账户的余额
/*好处:当new一个账户对象的时候,其实内部也会new一个锁对象,因为它是实例变量与对象是一起加载的(创建一个账户对象就是创建一个锁对象)
加final(唯一不可替换)的好处是:这个所不能被别人撬,比如在取钱的方法中设置lock = null,就会报错,因为加final的变量只能赋一次值
final修饰后:锁对象是唯一不可替换的,非常专业
*/
private final Lock lock = new ReentrantLock();
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public Account() {
}
/**
* 小明和小红两个人都到这来了
*
* @param money 代表要取的钱
*/
public void drawMoney(double money) {
//1、获取是谁来取钱(获取当前的线程,也就是说小明线程执行这个方法就是小明线程对象,也就是小明取钱)
/*线程的名字就是人命*/
String name = Thread.currentThread().getName();
//上锁
lock.lock();
try {
//2、判断账户钱是否够
if (this.money >= money) {
//3、取钱
System.out.println(name + "取钱成功,吐出:" + money);
//4、更新余额(这么做是故意出bug,如果将下边这行代码放到取钱上边可能就比现在安全)
this.money -= money;
System.out.println(name + "取钱后剩余" + this.money);
} else {
//5、余额不足
System.out.println(name + "取钱,余额不足!");
}
} finally {
/*假如代码出现异常,就不会执行下边这个代码了,异常就向上抛出去了,导致锁没有解开,所以放到finally块中*/
//解锁
lock.unlock();
}
}
}