一、多线程并发情况下,线程不安全
1、使用多线程实现银行取钱
package theads;/*** @ClassName: TestBank* @Description: TODO* @Author: HLX* @date: 2023/5/29 14:53* @Version: V1.0*//*** 线程不安全: 取钱* <p>* 逻辑:* 连取两次 则为负数 -70;* 各自的口袋的钱是没问题的。*/
public class TestBank {public static void main(String[] args) {//账户上有100元生活费用Account account = new Account(100, "生活费用");System.out.println(account);//线程MyBank t1 = new MyBank("张三", account, 80);MyBank t2 = new MyBank("刘芳", account, 90);MyBank t3 = new MyBank("汪峰", account, 50);t1.start();t2.start();t3.start();}
}//银行账户
class Account {private int money; //金额private String name; //备注public Account(int money, String name) {this.money = money;this.name = name;}@Overridepublic String toString() {return name + "入账" + money + "元";}public Account() {}public int getMoney() {return money;}public void setMoney(int money) {this.money = money;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}//线程
class MyBank extends Thread {private Account account; //账户private int takeMoney;//取钱//构造方法public MyBank(String name, Account account, int takeMoney) {super(name);this.account = account;this.takeMoney = takeMoney;}@Overridepublic void run() {try {Thread.sleep(1000); // 模拟延迟网络时间} catch (InterruptedException e) {e.printStackTrace();}//从账户上取取钱if (account.getMoney() - takeMoney < 0) {return;}try {Thread.sleep(1000); // 如果不堵塞的话,速度太快。看不到问题} catch (InterruptedException e) {e.printStackTrace();}
// 设置账户上剩余的钱account.setMoney(account.getMoney() - takeMoney);System.out.println(this.getName() + "取钱:" + takeMoney + "元,余额为:" + account.getMoney() + "元");}
}
从运行的效果来看: 余额竟然会出现-40的情况,原因是有100现金, 汪峰取50,剩-40 刘芳取90,剩-40 张三取80,剩-40 也就相当于那个判断没起作用。这就是线程不安全。原因是:3个进程同时操作这个唯一的资源,就会出现线程不安全的情况。
二、线程不安全解密:内存可见性
1、 java内存模型
(1)Java的内存模型分为主内存和工作内存(线程的)。
(2)Java内存模型规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。
(3)线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。有一个关键字是 volatile, volatile可以保证任何情况下变量的可见性,是不是就是直接读主内存呢,事实上,volatile变量依然有工作内存的拷贝,但是它的操作顺序比较特殊,会每次都从主内存重新加载,所以你会看到每次volatile读取到的都是最新的值。
(4)不同的线程也无法访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
2、线程不安全说明:
当多个线程操作同一个对象或者同一个资源时(如取钱),每个线程会把资源从java主内存中 读取一份到自己的工作内存中,进行操作,操作后,在将结果写到java主内存中。但是,因为有多个线程在同时操作,就要多个工作内存去读和写,假如有A,B 两个进程,都读取了一份资源到自己的工作内存中, B内存可能没被更新到主内存去。导致A线程或者其他内存 从主内存拷贝数据到自己的工作区时,拷贝的不是最新的数据。这就是 内存可见性问题。从而导致线程不安全问题。
3、线程安全与不安全
线程不安全:是不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
线程安全:指多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的,不存在执行程序时出现意外结果。
三、解决线程安全性问题
1、 线程同步概念
当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线 程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
2、使用线程同步方法和同步块
(1) 同步方法语法
访问修饰符 synchronized 返回类型 方法名(参数列表){……}
or
synchronized 访问修饰符 返回类型 方法名(参数列表){……}
(2) 同步块语法
synchronized (obj){ }, // obj可以是任何对象,但是推荐使用共享资源作为同步监视器
使用多线程同步块实现银行取钱
package theads;/*** @ClassName: TestBank* @Description: TODO* @Author: HLX* @date: 2023/5/29 14:53* @Version: V1.0*/public class TestBank2 {public static void main(String[] args) {//账户上有200元生活费用Account2 account2 = new Account2(200, "生活费用");System.out.println(account2);MyBank2 t1 = new MyBank2("张三", account2, 80);MyBank2 t2 = new MyBank2("刘芳", account2, 90);MyBank2 t3 = new MyBank2("汪峰", account2, 10);t1.start();t2.start();t3.start();}
}//银行账户
class Account2 {private int money; //金额private String name; //备注public Account2(int money, String name) {this.money = money;this.name = name;}@Overridepublic String toString() {return name + "入账" + money + "元";}public Account2() {}public int getMoney() {return money;}public void setMoney(int money) {this.money = money;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}//线程
class MyBank2 extends Thread {private Account2 account; //账户private int takeMoney;//取钱//构造方法public MyBank2(String name, Account2 account, int takeMoney) {super(name);this.account = account;this.takeMoney = takeMoney;}//缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。@Overridepublic void run() {/*** 提高性能*/if (account.getMoney() <= 0) {return;}//目标锁定account账户//同步块synchronized (account) {//从账户上取取钱if (account.getMoney() - takeMoney < 0) {return;}try {Thread.sleep(1000); // 模拟网络延时} catch (InterruptedException e) {e.printStackTrace();}
// 设置账户上剩余的钱account.setMoney(account.getMoney() - takeMoney);System.out.println(this.getName() + "取钱:" + takeMoney + "元,|__余额为:" + account.getMoney() + "元");}}
}
- 同一时刻只能有一个线程进入synchronized(this)同步代码块
- 当一个线程访问一个synchronized(this)同步代码块时,其他synchronized(this)同步代码块同样被锁定
- 当一个线程访问一个synchronized(this)同步代码块时,其他线程可以访问该资源的非synchronized(this)同步代码
四、synchronized 的范围问题,优化效率
(1) 同步方法 所锁住的范围太大,线程安全,影响效率。
(2) 同步块 所锁住的范围太小,锁不住,线程不安全。