Java多线程 - 线程安全和线程同步解决线程安全问题

news2024/11/27 0:31:49

文章目录

    • 线程安全问题
    • 线程同步
      • 方式一: 同步代码块
      • 方式二: 同步方法
      • 方式三: Lock锁

线程安全问题

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

举例: 取钱模型演示

需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。

如果小明和小红同时来取钱,而且2人都要取钱10万元,可能出现什么问题呢?

在这里插入图片描述

在取钱之前都需要判断余额是否足够, 例如两个线程同时执行, 两个线程都进行了余额判断, 发现余额充足;

此时小明线程取走10万, 账户余额为0; 小红线程由于此时已经判断过余额, 继续取钱的时候就不会继续判断余额, 直接将余额取出来; 那么两个人都取走了10万, 银行就亏了10万, 这就是多线程带来的安全问题

线程安全问题模拟, 我们将上面的例子用代码模拟出来多线程的安全隐患:

需求:

小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。

分析:

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

实现步骤:

模拟一个账户类给小明线程和小红线程, 我们模拟关键信息即可

public class Account {
    private double money;

    public  Account() {}

    public Account(double money) {
        this.money = money;
    }

    /**
        取钱方法
     */
    public void drawMoney(double money) {
        // 获取取钱人的名字
        String name = Thread.currentThread().getName();
        if (this.money >= money) { // 判断余额是否充足
            System.out.println(name + "取走了" + money + "元");
            this.money -= money;
        } else {
            System.out.println("余额不足");
        }
    }

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

在主类中, 创建2个线程对象,传入同一个账户对象; 启动2个线程,去同一个账户对象中取钱10万

public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        // 创建账户对象
        Account acc = new Account(100000);

        // 创建两个子线程, 并启动线程
        new DrawThread(acc, "小明").start(); // 小明取走了100000.0元
        new DrawThread(acc, "小红").start(); // 小红取走了100000.0元

        // 主线程睡眠两秒后, 查看余额
        Thread.sleep(2000);
        System.out.println(acc.getMoney()); // -100000.0
    }
}

线程同步

为了解决上面线程安全的问题。

取钱案例出现问题的原因

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

如何才能保证线程安全呢

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

线程同步的核心:

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

加锁的方式有三种方式: 同步代码块, 同步方法和Lock锁, 下面我们来分别学习加锁的方法

方式一: 同步代码块

作用:将出现线程安全问题的核心代码给上锁

原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

同步代码块上锁的格式:

synchronized(同步锁对象) {
  	// 操作共享资源的代码(核心代码)
}

例如我们给上面模拟的线程安全例子中的核心代码进行加锁操作:

进行加锁操作就解决了线程安全问题

public class Account {
    private double money;

    public  Account() {}

    public Account(double money) {
        this.money = money;
    }

    /**
        取钱方法
     */
    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();

        // 对核心代码进行加锁, 这里锁对象随便使用了一个字符串模拟(无实际代表意义)
        synchronized ("chen") {
            if (this.money >= money) {
                System.out.println(name + "取走了" + money + "元");
                this.money -= money;
            } else {
                System.out.println("余额不足");
            }
        }
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
}

锁对象注意:

理论上:锁对象只要对于当前同时执行的线程来说是唯一的一个对象即可。

但是实际上使用任意唯一的锁对象并不好, 会影响其他无关线程的执行, 例如上面例子中, 会将其他无关的账户也锁起来。

锁对象的规范要求:

规范上:建议使用共享资源作为锁对象。

  • 对于实例方法中, 建议使用this作为锁对象。
// 加锁, 使用共享资源作为锁对象
synchronized (this) {
}
  • 对于静态方法中, 建议使用字节码(类名.class)对象作为锁对象。
// 加锁, 使用共享资源作为锁对象
synchronized (类名.class) {
}

方式二: 同步方法

作用: 将出现线程安全问题的核心方法给上锁

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

同步方法的上锁格式:

修饰符 synchronized 返回值类型 方法名称(形参列表) {
	  // 操作共享资源的代码
}

同步方法上锁演示代码:

// 给核心方法上锁
public synchronized void drawMoney(double money) {
    String name = Thread.currentThread().getName();

    if (this.money >= money) {
        System.out.println(name + "取走了" + money + "元");
        this.money -= money;
    } else {
        System.out.println("余额不足");
    }
}

同步方法底层原理:

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

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

如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

方式三: Lock锁

Lock锁

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。

Lock实现提供比使用synchronized方法和语句可以获得更广泛灵活的锁定操作。

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。

方法名称说明
ReentrantLock()获得Lock锁的实现类对象

Lock的API:

方法名称说明
lock()获得锁
unlock()释放锁

Lock使用演示代码:

基本使用如下, 我们可以看出Lock使用是非常灵活的

public class Account {
    private double money;
    // 定义一个示例变量锁对象, 每创建一个类就会创建一个锁对象, 加final修饰, 表示不可替换
    private final Lock lock = new ReentrantLock();

    public  Account() {}
    public Account(double money) {
        this.money = money;
    }
    
    public synchronized void drawMoney(double money) {
        String name = Thread.currentThread().getName();

        // 调用锁对象上锁
        lock.lock();
        if (this.money >= money) {
            System.out.println(name + "取走了" + money + "元");
            this.money -= money;
        } else {
            System.out.println("余额不足");
        }
        // 调用锁对象解锁
        lock.unlock();
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
}

但是如果我们上锁和解锁之间的代码出现了异常, 永远都不会执行解锁操作, 所以更严谨的写法是将解锁的操作放到try…finally中, 保证会执行解锁的操作

public synchronized void drawMoney(double money) {
    String name = Thread.currentThread().getName();

    // 调用锁对象上锁
    try {
        lock.lock();
        if (this.money >= money) {
            System.out.println(name + "取走了" + money + "元");
            this.money -= money;
        } else {
            System.out.println("余额不足");
        }
    } finally {
        // 调用锁对象解锁
        lock.unlock();
    }
}

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

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

相关文章

建单向链表-C语言实现

任务描述 本关需要你建立一个带头结点的单向链表。 相关知识 什么是链表?链表和二叉树是C语言数据结构的基础和核心。 链表有多种形式,它可以是单链接的或者双链接的,可以是已排序的或未排序的,可以是循环的或非循环的。 本关让我们来学习单链表。 单链表 单向链表(单…

XC-16 SpringSecurity Oauth2 JWT

SpringSecurityOauth2用户认证需求分析用户认证与授权单点登录需求第三方认证需求用户认证技术方案单点登录技术方案Oauth2认证Oauth2认证流程2.2.2Oauth2在本项目中的应用SpringSecurity Oauth2认证解决方案SpringSecurityOauth2研目标搭建认证服务器导入基础工程创建数据库Oa…

一起自学SLAM算法:9.2 LSD-SLAM算法

连载文章,长期更新,欢迎关注: 下面将从原理分析、源码解读和安装与运行这3个方面展开讲解LSD-SLAM算法。 9.2.1 LSD-SLAM原理分析 前面已经说过,LSD-SLAM算法是直接法的典型代表。因此在下面的分析中,首先介绍一下直…

学习笔记:Java 并发编程④

若文章内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系博主删除。 视频链接:https://www.bilibili.com/video/av81461839配套资料:https://pan.baidu.com/s/1lSDty6-hzCWTXFYuqThRPw&am…

CSS语法格式与三种引入方式

文章目录第一章——CSS简介1.1 CSS语法格式1.2 CSS 位置1.3 CSS引入方式1.3.1.行内样式表(内联样式表)1.3.2 外部样式表1.3.3 内部样式表第一章——CSS简介 1.1 CSS语法格式 CSS 规则由两个主要的部分构成:选择器以及一条或多条声明。 选择…

C语言全局变量和局部变量

局部变量定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。例如:intf1(int a){ int b,c;//a,b,c仅在函数f1()内有效 return abc; } i…

各种CV领域 Attention (原理+代码大全)

人类在处理信息时,天然会过滤掉不太关注的信息,着重于感兴趣信息,于是将这种处理信息的机制称为注意力机制。 注意力机制分类:软注意力机制(全局注意)、硬注意力机制(局部注意)、和…

打工人必知必会(三)——经济补偿金和赔偿金的那些事

目录 参考 一、经济补偿金&赔偿金-用人单位承担赔偿责任 1、月平均工资是税前还是税后工资? 3、经济补偿金是否要交个人所得税?如何交? 二、劳动者承担赔偿责任 三、劳动者需要特别注意 参考 《HR全程法律顾问:企业人力资…

Day12 XML配置AOP

1 前言前文我们已经介绍了AOP概念Day11 AOP介绍&#xff0c;并将其总结如下&#xff1a;2 AOP 标签和expression表达式学习<?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.springframework.org/schema/beans"xmlns:x…

3.4只读存储器ROM

文章目录一、引子二、介绍1.MROM2.PROM3.EPROM4.Flash Memory5.SSD三、运行过程四、回顾一、引子 这一小节&#xff0c;我们学习只读存储器ROM。 上一小节&#xff0c;学习了两种RAM芯片&#xff0c;分别是SRAM和DRAM。详情请戳&#xff1a;3.3Sram和Dram RAM芯片可以支持随…

Pygame创建界面

今天开始对Python的外置包pygame进行学习&#xff0c;pygame是Python的游戏包&#xff0c;使用该包可以设计一些简单的小游戏。 前言 利用Python外置包创建一个简单界面&#xff0c;首先需要下载Python外置包pygame 使用语句&#xff1a;pip install pygame Display模块 创建…

红黑树知识点回顾

Rudolf Bayer 于1978年发明红黑树&#xff0c;在当时被称为对称二叉 B 树(symmetric binary B-trees)。后来&#xff0c;在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。 红黑树具有良好的效率&#xff0c;它可在近似O(logN) 时间复杂度下完成插入、删除、…

实验五、任意N进制异步计数器设计

实验五 任意N进制异步计数器设计 实验目的 掌握任意N进制异步计数器设计的方法。 实验要求 一人一组&#xff0c;独立上机。在电脑上利用Multisim软件完成实验内容。 实验内容 说明任意N进制异步计数器的构成方法 设计过程 集成计数器一般都设有清零端和置数输入端&#xff…

3.7动态规划--图像压缩

3.6多边形游戏&#xff0c;多边形最优三角剖分类似&#xff0c;仅仅是最优子结构的性质不同&#xff0c;这个多边形游戏更加具有一般性。不想看了&#xff0c;跳过。 写在前面 明确数组含义&#xff1a; l: l[i]存放第i段长度, 表中各项均为8位长&#xff0c;限制了相同位数…

ElasticSearch - RestClient操作ES基本操作

目录 什么是RestClient hotel数据结构分析 初始化RestClient 创建索引库 删除索引库 判断索引库是否存在 小结 新增文档 查询文档 更新文档 删除文档 批量导入文档 小结 什么是RestClient ES官方提供了各种不同语言的客户端&#xff0c;用来操作ES这些客户端的本质…

Java基础语法——方法

目录 方法概述 方法定义及格式 方法重载 •方法重载概述 •方法重载特点 方法中基本数据类型和引用数据类型的传递 方法概述 ——假设有一个游戏程序&#xff0c;程序在运行过程中&#xff0c;要不断地发射炮弹(植物大战僵尸)。发射炮弹的动作需要编写100行的代码&…

五、在测试集上评估图像分类算法精度(Datawhale组队学习)

文章目录配置环境准备图像分类数据集和模型文件测试集图像分类预测结果表格A-测试集图像路径及标注表格B-测试集每张图像的图像分类预测结果&#xff0c;以及各类别置信度可视化测试集中被误判的图像测试集总体准确率评估指标常见评估指标混淆矩阵PR曲线绘制某一类别的PR曲线绘…

密码学的100个基本概念

密码学的100个基本概念一、密码学历史二、密码学基础三、分组密码四、序列密码五、哈希函数六、公钥密码七、数字签名八、密码协议九、密钥管理十、量子密码2022年主要完成了密码学专栏的编写&#xff0c;较为系统的介绍了从传统密码到现代密码&#xff0c;以及量子密码的相关概…

C语言函数声明以及函数原型

C语言代码由上到下依次执行&#xff0c;原则上函数定义要出现在函数调用之前&#xff0c;否则就会报错。但在实际开发中&#xff0c;经常会在函数定义之前使用它们&#xff0c;这个时候就需要提前声明。所谓声明&#xff08;Declaration&#xff09;&#xff0c;就是告诉编译器…

《网络编程实战》学习笔记 Day9

系列文章目录 这是本周期内系列打卡文章的所有文章的目录 《Go 并发数据结构和算法实践》学习笔记 Day 1《Go 并发数据结构和算法实践》学习笔记 Day 2《说透芯片》学习笔记 Day 3《深入浅出计算机组成原理》学习笔记 Day 4《编程高手必学的内存知识》学习笔记 Day 5NUMA内存知…