线程安全

多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。

线程安全问题出现的原因:

  • 存在多线程并发
  • 同时访问共享资源
  • 存在修改共享资源

模拟实现:共同的账户,余额是 10 万元,模拟 2 人同时去取钱 10 万

  1. 需要提供一个账户类,创建一个账户对象代表 2 个人的共享账户。
  2. 需要定义一个线程类,线程类可以处理账户对象。
  3. 创建 2 个线程对象,传入同一个账户对象。
  4. 启动 2 个线程,去同一个账户对象中取钱 10 万。

    1. @Data
    2. @AllArgsConstructor
    3. @NoArgsConstructor
    4. public class Account {
    5. private String cardId;
    6. private double money; // 账户的余额
    7. public void drawMoney(double money) {
    8. // 0、先获取是谁来取钱,线程的名字就是人名
    9. String name = Thread.currentThread().getName();
    10. // 1、判断账户是否够钱
    11. if (this.money >= money) {
    12. // 2、取钱
    13. System.out.println(name + "来取钱成功,吐出:" + money);
    14. // 3、更新余额
    15. this.money -= money;
    16. System.out.println(name + "取钱后剩余:" + this.money);
    17. } else {
    18. // 4、余额不足
    19. System.out.println(name + "来取钱,余额不足!");
    20. }
    21. }
    22. }
    1. /**
    2. 取钱的线程类
    3. */
    4. public class DrawThread extends Thread {
    5. // 接收处理的账户对象
    6. private Account acc;
    7. public DrawThread(Account acc,String name){
    8. super(name);
    9. this.acc = acc;
    10. }
    11. @Override
    12. public void run() {
    13. // 小明 小红:取钱
    14. acc.drawMoney(100000);
    15. }
    16. }
    1. public static void main(String[] args) {
    2. // 1、定义线程类,创建一个共享的账户对象
    3. Account acc = new Account("ICBC-111", 100000);
    4. // 2、创建 2 个线程对象,代表小明和小红同时进来了。
    5. new DrawThread(acc, "小明").start();
    6. new DrawThread(acc, "小红").start();
    7. }

    线程同步

    让多个线程实现先后依次访问共享资源,这样就解决了安全问题
    线程同步的核心思想:加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来

    方式一:同步代码块

    作用:把出现线程安全问题的核心代码给上锁。
    原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

    1. public void drawMoney(double money) {
    2. // 0、先获取是谁来取钱,线程的名字就是人名
    3. String name = Thread.currentThread().getName();
    4. // 同步代码块
    5. synchronized ("halo") {
    6. // 1、判断账户是否够钱
    7. if (this.money >= money) {
    8. // 2、取钱
    9. System.out.println(name + "来取钱成功,吐出:" + money);
    10. // 3、更新余额
    11. this.money -= money;
    12. System.out.println(name + "取钱后剩余:" + this.money);
    13. } else {
    14. // 4、余额不足
    15. System.out.println(name + "来取钱,余额不足!");
    16. }
    17. }
    18. }

    IDEA 快捷创建:选中代码块 Ctrl + Alt + T 后选择 9

锁对象用任意唯一的对象好不好呢?不好,会影响其他无关线程的执行。
锁对象的规范要求:

  • 规范上:建议使用共享资源作为锁对象。
  • 对于实例方法建议使用 this 作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。

    1. public void drawMoney(double money) {
    2. // 0、先获取是谁来取钱,线程的名字就是人名
    3. String name = Thread.currentThread().getName();
    4. // 同步代码块
    5. synchronized (this) {
    6. // 1、判断账户是否够钱
    7. if (this.money >= money) {
    8. // 2、取钱
    9. System.out.println(name + "来取钱成功,吐出:" + money);
    10. // 3、更新余额
    11. this.money -= money;
    12. System.out.println(name + "取钱后剩余:" + this.money);
    13. } else {
    14. // 4、余额不足
    15. System.out.println(name + "来取钱,余额不足!");
    16. }
    17. }
    18. }
    1. public static void main(String[] args) {
    2. // 1、定义线程类,创建一个共享的账户对象
    3. Account acc = new Account("ICBC-111", 100000);
    4. // 2、创建 2 个线程对象,代表小明和小红同时进来了。
    5. new DrawThread(acc, "小明").start();
    6. new DrawThread(acc, "小红").start();
    7. // 1、定义线程类,创建一个共享的账户对象
    8. Account acc2 = new Account("ICBC-112", 100000);
    9. // 2、创建 2 个线程对象,代表小明和小红同时进来了。
    10. new DrawThread(acc2, "小明2").start();
    11. new DrawThread(acc2, "小红2").start();
    12. }

    方式二:同步方法

    作用:把出现线程安全问题的核心方法给上锁。
    原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

    1. public synchronized void drawMoney(double money) {
    2. // 0、先获取是谁来取钱,线程的名字就是人名
    3. String name = Thread.currentThread().getName();
    4. // 1、判断账户是否够钱
    5. if (this.money >= money) {
    6. // 2、取钱
    7. System.out.println(name + "来取钱成功,吐出:" + money);
    8. // 3、更新余额
    9. this.money -= money;
    10. System.out.println(name + "取钱后剩余:" + this.money);
    11. } else {
    12. // 4、余额不足
    13. System.out.println(name + "来取钱,余额不足!");
    14. }
    15. }

    同步方法底层原理:

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。

  • 如果方法是实例方法:同步方法默认用 this 作为的锁对象。但是代码要高度面向对象!
  • 如果方法是静态方法:同步方法默认用 类名.class 作为的锁对象。

同步代码块锁的范围更小,同步方法锁的范围更大。

方式三:Lock 锁

为了更清晰的表达如何加锁和释放锁,Java 1.5 以后提供了一个新的锁对象 Lock,更加灵活、方便。
Lock 实现提供比使用 synchronized 方法和语句可以获得更广泛的锁定操作。
Lock 是接口不能直接实例化,这里采用它的实现类 ReentrantLock 来构建 Lock 锁对象。

  • public ReentrantLock():获得 Lock 锁的实现类对象

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Account {
      private String cardId;
      private double money; // 账户的余额
    
      // final修饰后:锁对象是唯一和不可替换的
      private final Lock lock = new ReentrantLock();
    
      public void drawMoney(double money) {
          // 0、先获取是谁来取钱,线程的名字就是人名
          String name = Thread.currentThread().getName();
          lock.lock(); // 上锁
          try {
              if(this.money >= money){
                  // 钱够了
                  System.out.println(name+"来取钱,吐出:" + money);
                  // 更新余额
                  this.money -= money;
                  System.out.println(name+"取钱后,余额剩余:" + this.money);
              }else{
                  // 3、余额不足
                  System.out.println(name+"来取钱,余额不足!");
              }
          } finally {
              lock.unlock(); // 解锁
          }
      }
    }