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

news2024/11/24 20:40:46

线程安全问题

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

请添加图片描述

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

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

请添加图片描述

//锁对象:必须是一个唯一的对象(同一个地址)
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类中的代码即可。

请添加图片描述

// 同步方法
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锁

请添加图片描述

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个厨师(生产者线程),两个顾客(消费者线程)。

请添加图片描述

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

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

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

请添加图片描述

按照上面分析的思路和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();
    }
}

执行结果如下:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/993817.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分布式版本控制工具——git

✅<1>主页&#xff1a;&#xff1a;我的代码爱吃辣 &#x1f4c3;<2>知识讲解&#xff1a;Linux——git ☂️<3>开发环境&#xff1a;Centos7 &#x1f4ac;<4>前言&#xff1a;git是一个开源的分布式版本控制系统&#xff0c;可以有效、高速地处理从很…

路由缓存问题 | vue-router的导航守卫

路由缓存问题 带参路由&#xff0c;当参数发生变化时&#xff0c;相同的组件实例将被复用&#xff0c;组件的生命周期钩子不会被调用&#xff0c;导致数据无法更新。 两种解决方法&#xff1a; 1. 给 RouterView绑定key值&#xff0c;即 <RouterView :key"$route.ful…

SpringBoot原理-自动配置-原理分析-源码跟踪

自动配置原理 SpringBootApplication 该注解标识在SpringBoot项目的启动类上&#xff0c;是SpringBoot中最为重要的注解&#xff0c;该注解由三个部分组成。 SpringBootConfiguration&#xff1a;该注解与Configuration注解作用一样&#xff0c;用来声明当前类为一个配置类Comp…

解决SVN文件不显示绿色小钩图标问题

问题描述&#xff1a; 今天重新安装了SVN&#xff0c;发现从中央服务器拉取文件到本地仓库后&#xff0c;对应的文件没有绿色的小钩图标&#xff0c;于是查了一下解决方案&#xff0c;在这里总结一下。 解决方案一&#xff1a; 原因&#xff1a;状态缓存设置问题造成的。 在…

2024年java面试--mysql(1)

系列文章目录 2024年java面试&#xff08;一&#xff09;–spring篇2024年java面试&#xff08;二&#xff09;–spring篇2024年java面试&#xff08;三&#xff09;–spring篇2024年java面试&#xff08;四&#xff09;–spring篇2024年java面试–集合篇2024年java面试–redi…

Java设计模式-结构性设计模式(代理设计模式)

简介 为其他对象提供⼀种代理以控制对这个对象的访问&#xff0c;属于结构型模式。客户端并不直接调⽤实际的对象&#xff0c;⽽是通过调⽤代理&#xff0c;来间接的调⽤实际的对象应用场景 各⼤数码专营店&#xff0c;代理⼚商进⾏销售对应的产品&#xff0c;代理商持有真正的…

算法训练营day46|动态规划 part08:完全背包 (LeetCode 139. 单词拆分、多重背包理论基础)

文章目录 139. 单词拆分 (求排列方法)回溯思路分析背包思路分析代码实现思考总结 多重背包理论基础 139. 单词拆分 (求排列方法) 题目链接&#x1f525;&#x1f525; 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict&#xff0c;判定 s 是否可以被空格拆分为一个或多…

一文读懂LSTM及手写LSTM结构

torch.nn.LSTM是PyTorch中用于创建长短时记忆网络&#xff08;Long Short-Term Memory&#xff09;的类。LSTM是一种用于处理序列数据的循环神经网络&#xff08;Recurrent Neural Network&#xff0c;RNN&#xff09;变体。 官方给出的LSTM API 文档 以下是 torch.nn.LSTM 的…

LORA项目源码解读

大模型fineturn技术中类似于核武器的LORA&#xff0c;简单而又高效。其理论基础为&#xff1a;在将通用大模型迁移到具体专业领域时&#xff0c;仅需要对其高维参数的低秩子空间进行更新。基于该朴素的逻辑&#xff0c;LORA降低大模型的fineturn门槛&#xff0c;模型训练时不需…

Redis-带你深入学习数据类型list

目录 1、list列表 2、list相关命令 2.1、添加相关命令&#xff1a;rpush、lpush、linsert 2.2、查找相关命令&#xff1a;lrange、lindex、llen 2.3、删除相关命令&#xff1a;lpop、rpop、lrem、ltrim 2.4、修改相关命令&#xff1a;lset 2.5、阻塞相关命令&#xff1a…

deepin V23通过flathub安装steam畅玩游戏

deepin V23缺少32位库&#xff0c;在星火商店安装的steam,打开报错&#xff0c;无法使用&#xff01; 通过flathub网站安装steam,可以正常使用&#xff0c;详细教程如下&#xff1a; flathub网址&#xff1a;主页 | Flathub 注意&#xff1a;flathub下载速度慢&#xff0c;只…

【笔试强训选择题】Day38.习题(错题)解析

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;笔试强训选择题 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01; 文章目录 前言一、Day…

ChatGPT实战与私有化大模型落地

文章目录 大模型现状baseline底座选择数据构造迁移方法评价思考 领域大模型训练技巧Tokenizer分布式深度学习数据并行管道并行向量并行分布式框架——Megatron-LM分布式深度学习框架——Colossal-AI分布式深度学习框架——DeepSpeedP-tuning 微调 资源消耗模型推理加速模型推理…

基于SSM的学院实验中心管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

从数据页的角度看 B+Tree

InnoDB 是如何存储数据的&#xff1f; MySQL支持多种存储引擎&#xff0c;不同的存储引擎&#xff0c;存储数据的方式也不相同&#xff0c;我们最常使用的是 InnoDB 存储引擎。 在数据库中的记录是按照行来存储的&#xff0c;但是数据库的读取并不是按照 [ 行] 为单位&#x…

MySQL进阶 —— 超详细操作演示!!!(上)

MySQL进阶 —— 超详细操作演示&#xff01;&#xff01;&#xff01;&#xff08;上&#xff09; 一、存储引擎1.1 MySQL 体系结构1.2 存储引擎介绍1.3 存储引擎特点1.4 存储引擎选择 二、索引2.1 索引概述2.2 索引结构2.3 索引分类2.4 索引语法2.5 SQL 性能分析2.6 索引使用2…

BUUCTF rip 1

使用linux的file命令查看基本信息 64位 使用IDA64位进行反编译 看到gets就肯定有栈溢出 能看到有一个 _system函数&#xff0c;改函数能执行系统命令 既然反编译有这个函数说明有地方调用了他 果然在一个fun函数中有调用&#xff0c;执行的命令是 /bin/sh 也就是一个后门函数&…

【C++ • STL • 力扣】详解string相关OJ

文章目录 1、仅仅翻转字母2、字符串中的第一个唯一字符3、字符串里最后一个单词的长度4、验证一个字符串是否是回文5、字符串相加总结 ヾ(๑╹◡╹)&#xff89;" 人总要为过去的懒惰而付出代价 ヾ(๑╹◡╹)&#xff89;" 1、仅仅翻转字母 力扣链接 代码1展示&…

【Spring Cloud系列】 雪花算法原理及实现

【Spring Cloud系列】 雪花算法原理及实现 文章目录 【Spring Cloud系列】 雪花算法原理及实现一、概述二、生成ID规则部分硬性要求三、ID号生成系统可用性要求四、解决分布式ID通用方案4.1 UUID4.2 数据库自增主键4.3 基于Redis生成全局id策略 五、SnowFlake&#xff08;雪花算…

数据结构与算法-----顺序表(链表篇)

目录 前言 顺序表 链表 概念 与数组的不同 单链表 1. 创建节点 2.插入节点 尾插节点&#xff08;形成链表结构&#xff09; 向指定位置插入节点&#xff08;链表已有&#xff09; ​编辑 3.遍历链表数据 4.获取链表长度 5.删除节点 删除尾节点 删除指定节点 …