线程安全、线程同步

线程安全问题

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

取钱模型演示

需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。
如果小明和小红同时来取钱,而且2人都要取钱10万元,可能出现什么问题呢?

线程安全、线程同步_第1张图片

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

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

线程安全问题案例模拟

线程安全、线程同步_第2张图片
有安全隐患的:

/**
 * 需求:模拟取钱案例
 * 分析:同一个账户交给了两个线程,两个线程同时跑各自的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+"取钱,余额不足!");
        }
    }
}

线程安全问题发生的原因是什么?

  • 多个线程同时访问同一个共享资源且存在修改该资源。

线程同步

  • 为了解决线程安全问题。

取钱案例出现问题的原因?

  • 多个线程同时执行,发现账户都是够钱的。

如何才能保证线程安全呢?

  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题

线程同步的核心思想

  • 加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。

线程安全、线程同步_第3张图片
线程同步解决安全问题的思想是什么?

  • 加锁:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
线程同步方式一:同步代码块

同步代码块

  • 作用:把出现线程安全问题的核心代码给上锁。
  • 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
    在这里插入图片描述
    锁对象要求
  • 理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。

锁对象用任意唯一的对象好不好呢?

  • 不好,会影响其他无关线程的执行。

锁对象的规范要求

  • 规范上:建议使用共享资源作为锁对象
  • 对于实例方法建议使用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();
    }
}
线程同步方式二:同步方法

同步方法

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

格式
线程安全、线程同步_第4张图片

  /**
     * 小明和小红两个人都到这来了
     *
     * @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
线程安全、线程同步_第5张图片

对以下代码做了修改:

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

    }
}

你可能感兴趣的:(安全,java,后端)