多线程(四):线程安全

在开始讲解线程安全之前我们先来回顾一下我们学了那些东西了:

1. 线程和进程的认识

2. Thread 类的基本用法

3. 简单认识线程状态

4. 初见线程安全

上一章结束时看了一眼线程安全问题,本章将针对这个重点讲解。

一个代码在单线程中能够安全执行,但是在多线程中就容易出现错误;其本质原因就是线程在系统中的调度是无序的 / 抢占式执行的。

再看一眼上一章末尾的题,两个线程各执行 5w 次自增操作,最后的结果为什么是一个小于 10w 的随机数。

上节课也画了图:

 线程不安全的原因

我们在这里讨论一下照成线程不安全的原因有哪些?

  1. 多线程的抢占式执行(罪魁祸首)
  2. 多个线程修改同一个变量 【如果是一个线程修改一个变量 => 安全】【多个线程读取一个变量 => 安全】【多个线程修改不同变量 => 安全】
  3. 修改操作不是原子的 
  4. 内存可见性引起的线程不安全
  5. 指令重排序引起的线程不安全

那么我们就开始本章内容的讲解

对于 多线程的抢占式执行多个线程修改同一个变量 这两点不是我们能够改变的,我们就直接跳过,直接看第三条

修改操作不是原子的 

这里说到的原子性,数据库中 事物的原子性 是一个概念, 原子性意味着不可再分,说明每个操作都是最小单位。

例如上述例题: 每次自增操作都不算是最小操作,我们还可以对其进行划分,将一次 add 操作,分为三个小操作:load 、 add 、 save ;

任意某个操作对应单个 cpu 指令就是原子的, 对应多个 cpu 操作就是非原子的。

正是应该这个操作不是原子的,导致了俩个线程的指令排序存在更多的变数

既然我们发现了这个问题了,我们该如何解决呢?

保证操作的原子性

既然它不是原子的,那么我们就可以通过加锁操作让它变成原子性的。

就比如:

我们要上厕所,为了让别人也进来,所以需要锁门,我们就给门 加了个锁,那么上完厕所以后,就解锁,剩下的两个人就继续 抢占式 上厕所。

那么这个锁呢就可以保证 “原子性” 的效果

锁的核心操作就两个,加锁和解锁。 

对于上述的一个锁,当谁抢到了,其他线程就需要等待,也就发生了 阻塞等待,直到拿到锁的线程释放为止。

那么如何对线程进行加锁呢?

加锁 和 解锁

Java提供了关键字:synchronized,Java直接用 synchronized 这个关键字实现加锁过程。

还是上一章中最后一段的线程自增 5w 次的例子:

代码如下

class Count {
    private int count = 0;
    public void add() {
        synchronized (this) {
            count++;
        }
    }
    public int get() {
        return count;
    }
}
public class demo11 {
    public static void main(String[] args) throws InterruptedException {
        Count count = new Count();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.get());
    }
}

唯一不同的点在于:

我们加了关键字。

 这里给它加了个代码块,这个代码块有啥用呢?

一旦进入 被 synchronized 修饰的代码块时,就出发加锁机制, 一旦离开了这个代码块就会触发解锁机制。

而且我们在 synchronized 后面加了一个(this)这里的 this 就是锁对象

谁调用 this 就是谁,就对谁进行加锁操作。

例如:

 如果两个线程,针对同一个对象进行加锁,就会造成锁竞争(一个拿到锁,另一个线程阻塞等待)。如果两个对象针对不同的锁竞争就不会照成锁竞争。

现在重点来说一下锁括号里面的东西:

() 里的锁对象,可以是写作任意一个Object 对象,但是不能是 内置类型(内置类型就是基本数据类型)。

这括号主要就是为为了告诉大家,多个线程针对同一个对象加锁就会出现锁竞争,如果针对不同的对象加锁,就不会出现锁竞争了,再也没有别的作用

加锁以后,操作就变成原子的了,原来的操作就变成为了:

 那么再次执行的时候就变成为了:

 由于 t1 已经率先lock 了,t2 再次尝试 lock 就会出现阻塞等待的情况。

此时就可以保证 t2 的load 一定是在 t1 save 之后,此时计算的结果就一定是安全的。

加锁的本质其实就是变成串行化。

那么对比 join 方法,join也是实现串行化,join 方法是让两个线程都是实现串行化,而加锁只是让加锁的部分串行,其他部分还是并发执行的。

无论如何,加锁可能会造成阻塞,代码阻塞,对于程序的效率还是会有影响的。

内存可见性引起的线程不安全

我们先来写个 bug 在来说原因。

看代码:

import java.util.Scanner;

public class demo12 {
    public static boolean flag = false;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (!flag) {

            }
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            flag = scanner.nextBoolean();
        });
        t1.start();
        t2.start();
    }
}

我们在来运行一遍:

可以看到输入了true 之后代码还在跑,同样可以在 jconsole 里看到线程还在执行,为什么这一段代码还继续执行呢。

这里就涉及到内存可见性了。

我们在执行这段代码的时候,进入到 while 循环, !flag 为真 在这个过程中又发生了两个 原子性的操作, 一个是 load :从内存读取数据到 cpu 寄存器;一个是 cmp (在cpu中可以叫别的名字):比较寄存器内的值是否为 false 。

这两个操作,load 消耗的时间远远高于 cmp 。

读内存虽然比读硬盘 快个几千倍 ; 读寄存器又要比 读内存快个几千倍

这样换算下来 每秒钟就要执行上亿次。

那么这样看下来,编译器发现 load 的开销很大,并且每次的结果都一样,那么编译器就做了一个非常大胆的操作,直接将 load 优化掉了(去掉了),只有第一次执行的 load 真正执行了,后续只循环 cmp 不执行 load 。

  所谓的内存可见性就是在多线程的环境下,编译器对于代码优化,产生了误判,从而引起的 bug ,从而导致我们代码的 bug 。

那么我们就可以通过 让编译器对这个场景暂停优化 :

这里就需要使用另一个关键字: volatile 

该关键字的含义就是:被它修饰的变量,此时编译器就会停止上述的优化。能够保证每次都是从内存上重新读取数据。

volatile关键字的作用主要有如下两个:

  1. 保证内存可见性:基于屏障指令实现,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
  2. 保证有序性:禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。

volatile不能保证原子性,volatile 使用的场景是:一个线程读,一个线程写的情况,而 synchronized 则适用于多线程写。 

volatile 的这个效果,称为 “保证内存可见性”。

而 synchronized 不确定是否也能保证内存可见性,网上资料 众说纷纭 。

volatile 还有一个效果,禁止指令重排序。

指令重排序

什么是指令重排序?

这也是编译器优化手段的一种,调整了代码的执行顺序,但是前后的逻辑不改变,效率更高。

如果是单线程的实现逻辑,结果并不会改变,但是在多线程中就会产生问题。

举例:

有个学生对象: Student s;

线程: t1  :s = new Student();

线程: t2  :if (s != null)  s.learn();

大体可以分为三个步骤:

1. 申请内存空间

2. 调用构造方法(初始化内存的数据)

3. 把对象的引用赋值给s (内存地址的赋值)

如果是个单线程,此处可以发生指令重排序, 2 和 3 谁先谁后都可以。

t1执行1和3,即将执行2的时候,t2开始执行,t2拿到的就不是一个空的对象,是一个非空的,他就去调用cow的方法,但是实际上,t1还没有初始化,调用方法,会产生bug,所以我们可以在cow对象前加关键字volatile,保证执行顺序。

那么本章的 线程安全 就到这里,下一章继续多线程内容。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

204. 计数质数 (埃式筛法详解)——【Leetcode每日一题】

素数最朴素判断思路&#xff1a;&#xff08;一般会超时&#xff09; 对正整数 n&#xff0c;如果用 2 到 n\sqrt{n}n​ 之间的所有整数去除&#xff0c;均无法整除&#xff0c;则 n 为素数又称为质数。 为什么到n\sqrt{n}n​ 就可以了&#xff0c;因为因数如果存在一定是成对…

【三】一起算法---栈:STL stack、手写栈、单调栈

纸上得来终觉浅&#xff0c;绝知此事要躬行。大家好&#xff01;我是霜淮子&#xff0c;欢迎订阅我的专栏《算法系列》。 学习经典算法和经典代码&#xff0c;建立算法思维&#xff1b;大量编码让代码成为我们大脑的一部分。 ⭐️已更系列 1、基础数据结构 1.1、链表➡传送门 1…

使用Node.js+Koa 从零开始写个人博客系统——后端部分(一)

使用Node.jsKoa 从零开始写个人博客系统系列 提示&#xff1a;在此文章中你可以学习到的内容如下&#xff1a; 1 如何使用Koa快速搭建项目 2 对Koa的核心组件Koa-Route的简单使用 3 3层架构思想 4 nodejs的ORM框架——sequelize的使用 5 sequelize-auto的使用 6 简单的增删查改…

【蓝桥杯嵌入式】第十三届蓝桥杯嵌入式国赛客观题以及详细题解

题1 概念题。 USRAT&#xff1a;异步串口通信&#xff0c;常用于数据传输&#xff1b;SW-DP&#xff1a;SWD 的全称应该是 The Serial Wire Debug Port (SW-DP),也就是串行调试端口&#xff0c;是 >ARM 目前支持的两种调试端口之一&#xff1b;JTAG-DP&#xff1a;另一个调试…

git基本用法教程(fork软件+git命令)

git基本用法教程1. git commit2. git branch3. git checkout4. git merge5. git rebase6. 在提交树中移动7. 撤销变更8. 整理提交记录9. 提交的技巧10. git clone11. git push12. git pull13. git fetch14. git flow15. git stash16. fork的使用当然除了环境和demo的运行和改写…

chartgpt 告诉我的,loss 函数的各种知识

一、libtorch中常见的损失函数及其使用场景的总结1. CrossEntropyLoss:CrossEntropyLoss&#xff08;交叉熵损失&#xff09;主要用于分类任务。它适用于多分类问题&#xff0c;其中每个样本只属于一个类别&#xff08;互斥&#xff09;。该损失函数将预测概率与真实标签的one-…

应届生投腾讯,被面试官问了8个和 ThreadLocal 相关的问题。

问&#xff1a;谈一谈ThreadLocal的结构。 ThreadLocal内部维护了一个ThreadLocalMap静态内部类&#xff0c;ThreadLocalMap中又维护了一个Entry静态内部类&#xff0c;和Entry数组。 Entry类继承弱引用类WeakReference&#xff0c;Entry类有一个有参构造函数&#xff0c;参数…

【数据结构】用队列实现栈

&#x1f4af;&#x1f4af;&#x1f4af; 本篇总结利用队列如何实现栈的相关操作&#xff0c;不难观察&#xff0c;栈和队列是可以相互转化的&#xff0c;需要好好总结它们的特性&#xff0c;构造出一个恰当的结构来实现即可&#xff0c;所以本篇难点不在代码思维&#xff0c;…

大数据应用——Hadoop运行模式(伪分布式运行)

4.2 伪分布式运行模式4.2.1 启动HDFS并运行MapReduce程序1. 分析 &#xff08;1&#xff09;配置集群&#xff08;2&#xff09;启动、测试集群增、删、查没有改&#xff08;多台机子麻烦&#xff09;&#xff08;3&#xff09;执行WordCount案例2. 执行步骤&#xff08;1&…

前端vue实现导出pdf文件报告组件

大屏项目有一个需求&#xff0c;需要对展示的内容进行文件导出&#xff0c;但是目前后台没有相关的逻辑&#xff0c;所以只能前端硬上&#xff0c;在参考了其他许多的逻辑之后&#xff0c;目前我自己这边做了一套比较笨的组件&#xff0c;通过拼接标签这种方法来实现对你想需要…

队列-我的基础算法刷题之路(六)

本篇博客旨在整理记录自已对队列的一些总结&#xff0c;以及刷题的解题思路&#xff0c;同时希望可给小伙伴一些帮助。本人也是算法小白&#xff0c;水平有限&#xff0c;如果文章中有什么错误之处&#xff0c;希望小伙伴们可以在评论区指出来&#xff0c;共勉 &#x1f4aa;。…

seaborn从入门到精通03-绘图功能实现02-分类绘图Categorical plots

seaborn从入门到精通03-绘图功能实现02-分类绘图Categorical plots总结参考关系-分布-分类分类绘图-Visualizing categorical data图形级接口catplot--figure-level interface导入库与查看tips和diamonds 数据分类散点图参考分布散点图stripplot分布密度散点图-swarmplot&#…

进程与线程

文章目录进程与线程进程什么是进程进程的组成程序段数据段程序控制块例子线程什么是线程线程的组成线程描述信息程序计数器栈内存例子进程与线程的区别进程与线程 进程 什么是进程 ​ 什么是进程呢&#xff1f;简单来说&#xff0c;进程是程序的一次启动执行。什么是 程序呢…

【C#进阶】C# 集合类

序号系列文章16【C#进阶】C# 索引器17【C#进阶】C# 委托18【C#进阶】C# 事件文章目录前言1、集合类是什么2、动态数组&#xff08;ArrayList&#xff09;3、压缩数组&#xff08;BitArray&#xff09;4、哈希表&#xff08;Hashtable&#xff09;5、队列&#xff08;Queue&…

【数据结构】链表OJ题

目录面试题 02.04 分割链表剑指 Offer II 027 回文链表160 相交链表141 环形链表142 环形链表 II138 复制带随机指针的链表面试题 02.04 分割链表 定义lesshead和greaterhead链接小于和大于等于k的值分别设置哨兵位和尾节点指针最后将两表去除哨兵位再链接 struct ListNode* p…

内存泄漏和内存溢出的区别

参考答案 内存溢出(out of memory)&#xff1a;指程序在申请内存时&#xff0c;没有足够的内存空间供其使用&#xff0c;出现 out of memory。内存泄露(memory leak)&#xff1a;指程序在申请内存后&#xff0c;无法释放已申请的内存空间&#xff0c;内存泄露堆积会导致内存被…

论文解读:PP-LiteSeg: A Superior Real-Time Semantic Segmentation Model

发表时间&#xff1a;2022 论文地址&#xff1a;https://arxiv.org/abs/2204.02681 项目地址&#xff1a;https://github.com/PaddlePaddle/PaddleSeg PP-LiteSeg&#xff0c;一个新的轻量级实时语义分割任务模型&#xff0c;在分割精度和推理速度之间实现了一种最先进的权衡…

JVM垃圾回收机制

文章目录JVM垃圾回收机制如何确定该对象是垃圾引用计数可达性分析如何释放对象常用策略JVM垃圾回收机制 以对象为单位来进行回收 如何确定该对象是垃圾 Java 中使用 可达性分析方法 Python 中时使用 引用计数方法 引用计数 使用额外的计数器&#xff0c;来记录某个对象有多少个…

【致敬未来的攻城狮计划】连续打卡第4天+物联网操作系统概述

开启攻城狮的成长之旅&#xff01;这是我参与的由 CSDN博客专家 架构师李肯&#xff08;http://yyds.recan-li.cn&#xff09;和 瑞萨MCU &#xff08;https://www.renesas.cn/cn/zh&#xff09; 联合发起的「 致敬未来的攻城狮计划 」的第 4 天&#xff0c;点击查看活动计划详…

【Vue3】用Element Plus实现列表界面

&#x1f3c6;今日学习目标&#xff1a;用Element Plus实现列表界面 &#x1f603;创作者&#xff1a;颜颜yan_ ✨个人格言&#xff1a;生如芥子&#xff0c;心藏须弥 ⏰本期期数&#xff1a;第四期 &#x1f389;专栏系列&#xff1a;Vue3 文章目录前言效果图目录简介修改vite…
最新文章