多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。
如果小明和小红同时来取钱,而且2人都要取钱10万元,可能出现什么问题呢?
/**
* 需求:模拟取钱案例
* 分析:同一个账户交给了两个线程,两个线程同时跑各自的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
作为的锁对象。是同步代码块好还是同步方法好一点?
同步方法是如何保证线程安全的?
同步方法的同步锁对象的原理?
Lock锁
JDK5
以后提供了一个新的锁对象Lock
,更加灵活、方便。Lock
实现提供比使用synchronized
方法和语句可以获得更广泛的锁定操作。Lock
是接口不能直接实例化,这里采用它的实现类ReentrantLock
来构建Lock
锁对象。对以下代码做了修改:
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();
}
}
}