• java 线程安全问题 三种线程同步方案 线程通信(了解)


    线程安全问题

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

    请添加图片描述

    下面代码演示上述问题,先定义一个共享的账户类:

    public class Account {
        private String cardId; // 卡号
        private double money; // 余额。
    
        public Account() {
        }
    
        public Account(String cardId, double money) {
            this.cardId = cardId;
            this.money = money;
        }
    
        // 小明 小红同时过来的
        public void drawMoney(double money) {
            // 先搞清楚是谁来取钱?
            String name = Thread.currentThread().getName();
            // 1、判断余额是否足够
            if(this.money >= money){
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "来取钱后,余额剩余:" + this.money);
            }else {
                System.out.println(name + "来取钱:余额不足~");
            }
        }
    
        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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    在定义一个取钱的线程类

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    最后,再写一个测试类,在测试类中创建两个线程对象

    public class ThreadTest {
        public static void main(String[] args) {
             // 1、创建一个账户对象,代表两个人的共享账户。
            Account acc = new Account("ICBC-110", 100000);
            // 2、创建两个线程,分别代表小明 小红,再去同一个账户对象中取钱10万。
            new DrawThread(acc, "小明").start(); // 小明
            new DrawThread(acc, "小红").start(); // 小红
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    某个执行结果:

    小明来取钱100000.0成功!
    小红来取钱100000.0成功!
    小红来取钱后,余额剩余:-100000.0
    小明来取钱后,余额剩余:0.0
    
    • 1
    • 2
    • 3
    • 4


    三种线程同步方案

    为了解决前面的线程安全问题,我们可以使用线程同步思想。同步最常见的方案就是加锁,意思是每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动释放锁,然后其他线程才能再加锁进来。
    采用加锁的方案,就可以解决前面两个线程都取10万块钱的问题。怎么加锁呢?Java提供了三种方案:

    1.同步代码块
    2.同步方法
    3.Lock锁
    
    • 1
    • 2
    • 3

    线程同步方案1 同步代码块 synchronized

    我们先来学习同步代码块。它的作用就是把访问共享数据的代码锁起来,以此保证线程安全。

    请添加图片描述

    //锁对象:必须是一个唯一的对象(同一个地址)
    synchronized(锁对象){
        //...访问共享数据的代码...
    }
    
    • 1
    • 2
    • 3
    • 4

    在共享账户类里使用同步代码块,来解决前面代码里面的线程安全问题。我们只需要修改Account类中的代码即可。

    // 小明 小红线程同时过来的
    public void drawMoney(double money) {
        // 先搞清楚是谁来取钱?
        String name = Thread.currentThread().getName();
        // 1、判断余额是否足够
        // this表示该账户对象,正好代表共享资源,因为两个线程共享同一个账户对象。
        synchronized (this) {
            if(this.money >= money){
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "来取钱后,余额剩余:" + this.money);
            }else {
                System.out.println(name + "来取钱:余额不足~");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    
    小明来取钱100000.0成功!
    小明来取钱后,余额剩余:0.0
    小红来取钱:余额不足~
    
    • 1
    • 2
    • 3
    • 4

    最后,再说一下锁对象如何选择的问题

    1.建议把共享资源作为锁对象, 不要将随便无关的对象当做锁对象
    //比如上面的代码 我们把锁改为"锁" 这样一个字符串也行 因为这个资源在内存中永远只有一份
    //所以各个线程需要去竞争 但是这样好不好? 明显不行 万一有另外俩个人再创了一个账户 那就变成了四个人竞争一把锁了
    2.对于实例方法,建议使用this作为锁对象
    3.对于静态方法,建议把类的字节码(类名.class)当做锁对象//这里是Account.class
    
    • 1
    • 2
    • 3
    • 4
    • 5

    线程同步方案2 同步方法 synchronized

    同步方法,就是把整个方法给锁住,一个线程调用这个方法,另一个线程调用的时候就执行不了,只有等上一个线程调用结束,下一个线程调用才能继续执行,同样是修改Account类中的代码即可。

    请添加图片描述

    // 同步方法
    public synchronized void drawMoney(double money) {
        // 先搞清楚是谁来取钱?
        String name = Thread.currentThread().getName();
        // 1、判断余额是否足够
        if(this.money >= money){
            System.out.println(name + "来取钱" + money + "成功!");
            this.money -= money;
            System.out.println(name + "来取钱后,余额剩余:" + this.money);
        }else {
            System.out.println(name + "来取钱:余额不足~");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    同步方法有没有锁对象?锁对象是谁?

    同步方法也是有锁对象,只不过这个锁对象没有显式的写出来而已。
    	1.对于实例方法,锁对象其实是this(也就是方法的调用者)
    	2.对于静态方法,锁对象时类的字节码对象(类名.class)
    
    • 1
    • 2
    • 3

    最终,总结一下同步代码块和同步方法有什么区别?

    1.不存在哪个好与不好,只是一个锁住的范围大,一个范围小
    //锁的范围小一点 性能稍微好一丁点 可以提前加载那些公共区域的代码 但是提升的性能对于现在的计算机来说可以忽略不计
    //反而同步方法的可读性要好一些
    2.同步方法是将方法中所有的代码锁住
    3.同步代码块是将方法中的部分代码锁住
    
    • 1
    • 2
    • 3
    • 4
    • 5


    线程同步方案3 Lock锁

    请添加图片描述

    Lock锁是JDK5版本专门提供的一种锁对象,通过这个锁对象的方法来达到加锁,和释放锁的目的,使用起来更加灵活。格式如下

    1.首先在成员变量位子,需要创建一个Lock接口的实现类对象(这个对象就是锁对象)
    	private final Lock lk = new ReentrantLock();
    2.在需要上锁的地方加入下面的代码
    	 lk.lock(); // 加锁
    	 //...中间是被锁住的代码...
    	 lk.unlock(); // 解锁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用Lock锁改写前面DrawThread中取钱的方法,代码如下

    // 创建了一个锁对象
    //因为俩个线程公用一个账户 所以建立一个实例变量作为锁是可以的
    //用final修饰更专业 防止二次赋值
    private final Lock lk = new ReentrantLock();
    
    public void drawMoney(double money) {
            // 先搞清楚是谁来取钱?
            String name = Thread.currentThread().getName();
            try {//用try cath finally写更专业 因为你不能保证被锁的代码没有bug 有bug也要及时解锁
                lk.lock(); // 加锁
                // 1、判断余额是否足够
                if(this.money >= money){
                    System.out.println(name + "来取钱" + money + "成功!");
                    this.money -= money;
                    System.out.println(name + "来取钱后,余额剩余:" + this.money);
                }else {
                    System.out.println(name + "来取钱:余额不足~");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lk.unlock(); // 解锁
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    运行程序结果,观察是否有线程安全问题。到此三种解决线程安全问题的办法我们就学习完了。





    线程通信(了解)

    什么是线程通信呢?线程通信是当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,以相互协调,避免无效的资源挣抢。
    线程通信的常见模式:是生产者与消费者模型,生产者线程负责生成数据,消费者线程负责消费生产者生成的数据。
    注意:生产者生产完数据后应该让自己等待,通知其他消费者消费;消费者消费完数据之后应该让自己等待,同时通知生产者生成。
    比如下面案例中,有3个厨师(生产者线程),两个顾客(消费者线程)。

    请添加图片描述

    先分析一下完成这个案例的思路

    1.先确定在这个案例中,什么是共享数据?
    	答:这里案例中桌子是共享数据,因为厨师和顾客都需要对桌子上的包子进行操作。
    
    2.再确定有那几条线程?哪个是生产者,哪个是消费者?
    	答:厨师是生产者线程,3条生产者线程; 
    	   顾客是消费者线程,2条消费者线程
    	   
    3.什么时候将哪一个线程设置为什么状态
    	生产者线程(厨师)放包子:
    		 1)先判断是否有包子
    		 2)没有包子时,厨师开始做包子, 做完之后把别人唤醒,然后让自己等待
    		 3)有包子时,不做包子了,直接唤醒别人、然后让自己等待
    		 	
    	消费者线程(顾客)吃包子:
    		 1)先判断是否有包子
    		 2)有包子时,顾客开始吃包子, 吃完之后把别人唤醒,然后让自己等待
    		 3)没有包子时,不吃包子了,直接唤醒别人、然后让自己等待
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    请添加图片描述

    按照上面分析的思路和java Object提供的api写代码。先写桌子类,代码如下

    public class Desk {
        private List list = new ArrayList<>();
    
        // 放1个包子的方法
        // 厨师1 厨师2 厨师3
        //实例方法默认用this作为锁 所以可以保证锁住5个线程 它们公用一个桌子对象
        //锁也是可以跨方法的
        public synchronized void put() {
            try {
                String name = Thread.currentThread().getName();
                // 判断是否有包子。
                if(list.size() == 0){
                    list.add(name + "做的肉包子");
                    System.out.println(name + "做了一个肉包子~~");
                    Thread.sleep(2000);//让程序跑慢点容易观察
    
                    // 唤醒别人, 等待自己
                    this.notifyAll();//必须用当前同步锁对象进行调用 否则会出bug
                    this.wait();//因为只有锁对象知道当前谁占据着它 谁需要等待
                }else {
                    // 有包子了,不做了。
                    // 唤醒别人, 等待自己
                    this.notifyAll();//注意!!!notifyAll()和wait()位置不能调换
                    this.wait();//你如果先wait了 你让自己等待了 那你还怎么唤醒别人
                }
            } catch (Exception e) {//拦截sleep异常
                e.printStackTrace();
            }
        }
    
        // 吃货1 吃货2
        public synchronized void get() {
            try {
                String name = Thread.currentThread().getName();
                if(list.size() == 1){
                    // 有包子,吃了
                    System.out.println(name  + "吃了:" + list.get(0));
                    list.clear();
                    Thread.sleep(1000);
                    this.notifyAll();
                    this.wait();
                }else {
                    // 没有包子
                    this.notifyAll();
                    this.wait();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    再写测试类,在测试类中,创建3个厨师线程对象,再创建2个顾客对象,并启动所有线程

    public class ThreadTest {
        public static void main(String[] args) {
            //   需求:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上
            //      2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。
            Desk desk  = new Desk();
    
            // 创建3个生产者线程(3个厨师)
            new Thread(() -> {//匿名内部类写法
                while (true) {
                    desk.put();
                }
            }, "厨师1").start();
    
            new Thread(() -> {
                while (true) {
                    desk.put();
                }
            }, "厨师2").start();
    
            new Thread(() -> {
                while (true) {
                    desk.put();
                }
            }, "厨师3").start();
    
            // 创建2个消费者线程(2个吃货)
            new Thread(() -> {
                while (true) {
                    desk.get();
                }
            }, "吃货1").start();
    
            new Thread(() -> {
                while (true) {
                    desk.get();
                }
            }, "吃货2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    执行结果如下:

    厨师1做了一个肉包子~~
    吃货2吃了:厨师1做的肉包子
    厨师3做了一个肉包子~~
    吃货1吃了:厨师3做的肉包子
    厨师1做了一个肉包子~~
    吃货1吃了:厨师1做的肉包子
    厨师3做了一个肉包子~~
    吃货1吃了:厨师3做的肉包子
    厨师1做了一个肉包子~~
    吃货2吃了:厨师1做的肉包子
    //不终止则一直运行下去 可以发现没有出现线程安全问题 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    如何使用Jenkins持续集成构建接口自动化测试--配置邮件通知
    java线程池
    2.2.5 SQL注入(盲注)
    已有Win10基础上安装Ubuntu双系统(含BIOS操作)
    Xamarin体验:使用C#开发iOS/Android应用
    基于改进人工蜂群算法的 K 均值聚类算法(Matlab代码实现)
    redis内存碎片整理
    MySQL-Linux安装、卸载:
    通过ORPO技术微调 llama3大模型(Fine-tune Llama 3 with ORPO)
    Apache HTTPD 换行解析漏洞(CVE-2017-15715)
  • 原文地址:https://blog.csdn.net/weixin_43739821/article/details/132779165