Java 多线程共享数据引发的问题

一、多线程并发情况下,线程不安全​​

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;
    }

    @Override
    public 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;
    }

    @Override
    public 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;
    }

    @Override
    public 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 将会大大影响效率。
    @Override
    public 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) 同步块 所锁住的范围太小,锁不住,线程不安全。

猜你喜欢

转载自blog.csdn.net/hlx20080808/article/details/130947981