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

这篇具有很好参考价值的文章主要介绍了java 线程安全问题 三种线程同步方案 线程通信(了解)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

线程安全问题

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

java 线程安全问题 三种线程同步方案 线程通信(了解),java,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;
    }
}

在定义一个取钱的线程类

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

某个执行结果:

小明来取钱100000.0成功!
小红来取钱100000.0成功!
小红来取钱后,余额剩余:-100000.0
小明来取钱后,余额剩余:0.0


三种线程同步方案

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

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

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

java 线程安全问题 三种线程同步方案 线程通信(了解),java,java,安全,开发语言

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

在共享账户类里使用同步代码块,来解决前面代码里面的线程安全问题。我们只需要修改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 + "来取钱:余额不足~");
        }
    }
}

小明来取钱100000.0成功!
小明来取钱后,余额剩余:0.0
小红来取钱:余额不足~

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

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

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

java 线程安全问题 三种线程同步方案 线程通信(了解),java,java,安全,开发语言

// 同步方法
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.对于实例方法,锁对象其实是this(也就是方法的调用者)
	2.对于静态方法,锁对象时类的字节码对象(类名.class)

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

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


线程同步方案3 Lock锁

java 线程安全问题 三种线程同步方案 线程通信(了解),java,java,安全,开发语言

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

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

使用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(); // 解锁
        }
    }
}

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





线程通信(了解)

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

java 线程安全问题 三种线程同步方案 线程通信(了解),java,java,安全,开发语言

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

1.先确定在这个案例中,什么是共享数据?
	答:这里案例中桌子是共享数据,因为厨师和顾客都需要对桌子上的包子进行操作。

2.再确定有那几条线程?哪个是生产者,哪个是消费者?
	答:厨师是生产者线程,3条生产者线程; 
	   顾客是消费者线程,2条消费者线程
	   
3.什么时候将哪一个线程设置为什么状态
	生产者线程(厨师)放包子:
		 1)先判断是否有包子
		 2)没有包子时,厨师开始做包子, 做完之后把别人唤醒,然后让自己等待
		 3)有包子时,不做包子了,直接唤醒别人、然后让自己等待
		 	
	消费者线程(顾客)吃包子:
		 1)先判断是否有包子
		 2)有包子时,顾客开始吃包子, 吃完之后把别人唤醒,然后让自己等待
		 3)没有包子时,不吃包子了,直接唤醒别人、然后让自己等待

java 线程安全问题 三种线程同步方案 线程通信(了解),java,java,安全,开发语言

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

public class Desk {
    private List<String> 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();
        }
    }
}

再写测试类,在测试类中,创建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();
    }
}

执行结果如下:文章来源地址https://www.toymoban.com/news/detail-703737.html

厨师1做了一个肉包子~~
吃货2吃了:厨师1做的肉包子
厨师3做了一个肉包子~~
吃货1吃了:厨师3做的肉包子
厨师1做了一个肉包子~~
吃货1吃了:厨师1做的肉包子
厨师3做了一个肉包子~~
吃货1吃了:厨师3做的肉包子
厨师1做了一个肉包子~~
吃货2吃了:厨师1做的肉包子
//不终止则一直运行下去 可以发现没有出现线程安全问题 

到了这里,关于java 线程安全问题 三种线程同步方案 线程通信(了解)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【线程安全】死锁问题及解决方案

    比如上一次讲到 synchronized 的时候,一个线程,对同一个对象连续加锁两次,如果出现阻塞等待,代表这个锁是不可重入锁,这样的线程,也就称为死锁! 一旦程序进入死锁了就会导致线程僵住了,无法继续执行后续的工作了,程序也就出现了严重的 BUG! 而死锁这样的情况

    2024年02月06日
    浏览(47)
  • 线程安全问题的原因和解决方案

    如果某个代码,在单线程执行下没有问题,在多线程执行下执行也没有问题,则称“线程安全”,反之称“线程不安全”。 目录 前言 一、简述线程不安全案例 二、线程安全问题的原因 (一)(根本问题)线程调度是随机的 (二)代码的结构问题 (三)代码执行不是原子的

    2024年02月14日
    浏览(42)
  • JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制)

    🔥博客主页: 【 小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录         1.0 指令重排序概述         1.1 指令重排序主要分为两种类型         1.2 指令重排序所引发的问题         2.0 内存可见性概述         2.1 导致内存可见性问题主要涉及两个方面      

    2024年04月15日
    浏览(48)
  • 【JavaEE面试题(九)线程安全问题的原因和解决方案】

    大家观察下是否适用多线程的现象是否一致?同时尝试思考下为什么会有这样的现象发生呢? 原因是 1.load 2. add 3. save 注意:可能会导致 小于5w 想给出一个线程安全的确切定义是复杂的,但我们可以这样认为: 如果多线程环境下代码运行的结果是符合我们预期的,即在单线

    2024年02月16日
    浏览(47)
  • Java多线程之线程安全问题

    我们知道操作系统中线程程的调度是抢占式执行的, 宏观上上的感知是随机的, 这就导致了多线程在进行线程调度时线程的执行顺序是不确定的, 因此多线程情况下的代码的执行顺序可能就会有无数种, 我们需要保证这无数种线程调度顺序的情况下, 代码的执行结果都是正确的

    2023年04月17日
    浏览(46)
  • java基础之线程安全问题以及线程安全集合类

    当多个线程同时访问同一个临界资源时,原子操作可能被破坏,会导致数据丢失, 就会触发线程安全问题 临界资源: 被多个线程同时访问的对象 原子操作: 线程访问临界资源的过程中不可更改和缺失的操作 互斥锁 每个对象都默认拥有互斥锁, 该锁默认不开启. 当开启互斥锁之后

    2024年01月18日
    浏览(59)
  • Java中的多线程——线程安全问题

    作者:~小明学编程   文章专栏:JavaEE 格言:热爱编程的,终将被编程所厚爱。 目录 多线程所带来的不安全问题 什么是线程安全 线程不安全的原因 修改共享数据 修改操作不是原子的 内存可见性对线程的影响 指令重排序 解决线程不安全的问题 synchronized 互斥 刷新内

    2024年02月03日
    浏览(81)
  • Java中的单点登录原理与实现方案探究:深入了解安全与便捷的用户认证解决方案

    目录 1、什么是单点登录 2、单点登录的优势和应用场景 3、单点登录的原理和实现方式 3.1 传统的Cookie和Session实现方式 3.2 基于Token的实现方式 3.3 基于OAuth2的实现方式 4、单点登录的技术要点和关键问题 4.1 安全性考虑 4.2 用户体验优化 4.3 高可用性设计 5、Java中的单点登录实

    2024年01月23日
    浏览(59)
  • 【Java】线程安全问题

    在之前的文章中,已经介绍了关于线程的基础知识。 我的主页: 🍆🍆🍆爱吃南瓜的北瓜 欢迎各位大佬来到我的主页进行指点 一同进步!!! 我们创建两个线程t1和t2,对静态变量count执行++操作各50000次。 我们的预期结果是100000。但是当两个线程分别执行++操作时最后的结果

    2024年04月10日
    浏览(46)
  • java线程安全问题及解决

    当我们使用多个线程访问 同一资源 (可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程 只有读操作 ,那么不会发生线程安全问题。但是如果多个线程中对资源有 读和写 的操作,就容易出现线程安全问题。 案例: 火车站要卖票,我们模拟火车站的卖票

    2024年02月15日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包