[JavaEE]线程的状态与安全

news2024/9/22 21:23:13


专栏简介: JavaEE从入门到进阶

题目来源: leetcode,牛客,剑指offer.

创作目标: 记录学习JavaEE学习历程

希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长.

学历代表过去,能力代表现在,学习能力代表未来! 


目录 

1. 线程状态 

1.1 观察线程的所有状态

 1.2 线程的状态和状态转移的意义 

2.线程安全

2.1 线程安全的概念:

 2.2 线程安全问题的原因

 2.3 从原子性角度解决线程安全问题

 synchronized 关键字使用方法:


1. 线程状态 

1.1 观察线程的所有状态

线程的状态 Thread.State 是一个枚举类型. 可通过遍历查看其所有类型.

public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()){
            System.out.println(state);
        }
    }

  • 1. NEW: 创建了 Thread 对象 , 但还没有调用 start (内核中还没有创建对应的PCB)
  • 2. TERMINATED: 表示内核中的 PCB 已执行完毕 , 但Thread对象还在.
  • 3. RUNNABLE: 可运行的. 分为两种情况 a).正在CPU上执行的 b).在就绪队列中 , 随时可以去CPU上执行. 一般不做区分.
  • 4. WAITING: 表示线程 PCB 正在阻塞队列中
  • 5. TIMED_WAITING: 表示线程 PCB 正在阻塞队列中
  • 6. BLOCKED: 表示线程 PCB 正在阻塞队列中

 1.2 线程的状态和状态转移的意义 

通过下面代码来演示 , 相比于单线程 , 多线程效率的提升.

假设有两个变量 a 和变量 b , 现需要将两个变量各自自增100亿次.(典型的 CPU 密集型场景)

Tips: 编写多线程代码时 , 不能调用完 start 方法后就立即结束计时 , 还需调用 jion 方法等待 t1 和 t2 两个线程结束. 这就好比 main线程是裁判员 , t1 和 t2 是准备赛跑的运动员 , 裁判一声令下还没等运动员反应过来就立即结束计时 , 这显然是不合常理的.裁判需等待运动员跑过终点线再结束计时.

 public static void main(String[] args) throws InterruptedException {
//       serial();
        concurrency();
    }
    /**
     * 多线程执行
     * @throws InterruptedException
     */
    public static void concurrency() throws InterruptedException {
        Thread t1 = new Thread(()->{
            long a = 0;
            for (long i = 0; i < 10000_0000_00L; i++) {
                a++;
            }
        });
        Thread t2 = new Thread(()->{
            long b = 0;
            for (long i = 0; i < 10000_0000_00L; i++) {
                b++;
            }
        });
        long startTime = System.currentTimeMillis();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        long endTime = System.currentTimeMillis();
        System.out.println("执行时间"+ (endTime-startTime)+"ms");
    }

    /**
     * 单线程执行
     */
    public static void serial(){
        long a = 0;
        long b = 0;
        long startTime = System.currentTimeMillis();
        for (long i = 0; i < 10000_0000_00L; i++) {
            a++;
        }
        for (long i = 0; i < 10000_0000_00L; i++) {
            b++;
        }
        long endTime = System.currentTimeMillis();
        System.out.println("执行时间: "+(endTime-startTime)+"ms");
    }

观察执行结果我们可以发现 , 相比于单线程执行 , 多线程执行可以节省大量时间 , 但并非我们认为的节省一半时间 , 这是因为多线程在调度时还会有额外的开销 , 而且不能保证多线程一定是在两个CPU上执行.

由此我们可以得出结论: 不是说使用多线程就一定能提高效率!!还需考虑以下两点:

  • CPU是否是多核 (现在CPU基本都是多核)
  • 当前核心是否空闲 (如果CPU的所有核心都已满载 , 此时启用再多的线程也无济于事)

2.线程安全

2.1 线程安全的概念:

线程不安全的主要原因是多线程的抢占式执行带来的随机性 , 原本在单线程中 , 代码按照固定的顺序执行 , 那么程序的执行结果就是固定的 ,  如果有了多线程 , 代码执行顺序的可能性就从一种情况变成无数种情况!!只要有一种情况 , 程序执行结果不正确 , 就会视为线程不安全. 

如果多线程环境下代码的运行结果符合我们的预期 , 即是在单线程环境下预期的结果 , 则说这个线程是线程安全的.

线程不安全示例:

创建两个线程分别对 count 自增5w次 , 按照预期执行结果应是的 count = 10w次.

class Counter{
    public int count;
    public void add(){
        count++;
    }
}
public class ThreadDemo2 {

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("count = "+counter.count);
    }
}

多次运行观察结果与我们预期相差较大 , 明显出现了bug. 

 那么程序为什么会出现上述的bug呢?

 count++ 操作本质上要分为三步:

  • 1. 先把内存中的值 , 读取到CPU的寄存器中. load
  • 2. 把CPU寄存器里的数值进行+1运算.           add
  • 3. 把得到的结果都写到内存中.                       save

如果两个线程并发执行count++ , 此时相当于两组 load add save 进行执行 , 此时不同的线程调度顺序就可能产生结果上的差异. 如下图所示 , 线程的调度顺序有无数种可能 , 但只有第一种执行顺序是安全的.

正确执行顺序: t1 线程先进行 load 操作 , 将count=0传入寄存器中 , 再进行 add 操作将寄存器中的值+1 , 最后执行 save 操作将寄存器中的值保存到内存中. t2 线程操作顺序与 t1 线程一致 , 最终计算结果为 2.

错误执行顺序: t1 和 t2 先后执行 load 操作 , 此时两个寄存器中 count=0.接着 t2 执行 add 操作将寄存器中的值+1 , 最后执行 save 操作 , 将count=1保存到内存中. 然后 t1 执行 add 和 save 操作 , 最后还是将count=1保存到内存中 , 此时我们发现经历了两次自增 , 结果还是1.造成该结果的原因是 t1 读取了 t2 还未提交的脏数据.(脏读)


 2.2 线程安全问题的原因

1.[根本原因] 抢占式执行 , 随机调度.

多线程本身的特点 , 无能为力.

2.[代码结构] 修改共享数据

在上述不安全的多线程代码中 , 涉及到多个线程对 counter.count 变量进行修改 , 此时这个counter.count 就是一个多线程都能访问到的共享数据.

 Tips: counter.count 这个变量就在堆上 , 因此可以被多个线程访问.

3.原子性

一条Java语句不一定是原子的 , 也不一定只是一条指令.

比如 我们刚才看到的 count++ 其实就是三步操作:

  • 从存储把数据读到CPU寄存器
  • 更新数据
  • 把数据写回到CPU

如果一个线程正在进行操作 , 中途其他线程突然插进来 , 如果这个操作被打断了 , 结果很可能是错误的.这个问题的本质还是多线程的抢占式执行 , 如果线程不是"抢占"的 , 即使不是原子的也没有问题.因此解决这个线程安全问题 , 最主要的手段就是从原子性入手 , 把这个非原子的操作变成原子的 , 常见办法就是加锁.

4.内存可见性

可见性指 , 一个线程对共享变量值的修改 , 能够及时的被其他线程看到.后续会在volatile关键字专栏做更详细的讲解.

5.指令重排序(本质上是编译器优化出bug)

一段代码的编写是这样的:

1.去前台去U盘

2.去学习10min

3.去前台取快递

在单线程中执行时 , JVM 和 CPU 指令集 , 会对其进行优化 , 按照1->3->2 的方式执行 , 这样可以少跑一次柜台提高代码执行的效率  , 这种叫做指令重排序.编译器指令重排序的前提是"保持代码逻辑不会发生变化" , 在单线程的环境下代码执行逻辑可以很好的预测 , 但是在多线程的环境下 , 代码复杂度更高 , 编译器很难在编译时期就对代码的执行结果进行预测 , 因此激进的重排序可能导致优化后的逻辑与之前不等价.


 2.3 从原子性角度解决线程安全问题

通过加锁操作把不是原子的操作变为"原子"的.因此我们可以使用 synchronized 关键字对线程加锁 , 如果两个线程同时尝试加锁 , 此时只有一个线程能成功 , 另一个线程只能阻塞等待(BLOCKED) , 一直阻塞到刚才的线程释放锁 , 另一个线程才能加锁成功.

lock 的阻塞就把刚才的 t2 的 load 推迟到 t1 的 save 之后 , 从而避免了脏读.加锁虽说是保证原子性 , 其实并不是让这三个操作一次性完成 , 也不是这三步操作过程中不执行调度 , 而是让其他也想执行的线程阻塞等待.(加锁的本质就是把并发变成串行)

打个比方就是 , 一个女生如果没有男朋友就是没有加锁的状态 , 其他男生都可以去追求她 , 一但有了男朋友 , 这个女生就加锁了 , 其他男生想追求只能等 , 这个女生和他男朋友分手相当于释放锁 , 释放锁之后其他男生才能去追求.

修改部分代码:

class Counter{
    public int count;
    public synchronized void add(){
        count++;
    }
}

运行结果符合预期 , synchronized 关键字下篇文章会专门讲解 , 这里不展开赘述. 


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

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

相关文章

k-means算法进行数据分析应用

简介 kmeans算法又名k均值算法,K-means算法中的k表示的是聚类为k个簇&#xff0c;means代表取每一个聚类中数据值的均值作为该簇的中心&#xff0c;或者称为质心&#xff0c;即用每一个的类的质心对该簇进行描述。 其算法思想大致为&#xff1a;先从样本集中随机选取 k…

【王道操作系统】2.3.3 实现临界区进程互斥的硬件实现方法

实现临界区进程互斥的硬件实现方法 文章目录实现临界区进程互斥的硬件实现方法1.中断隐藏方法2.TestAndSet指令3.Swap指令1.中断隐藏方法 2.TestAndSet指令 执行TSL指令时&#xff0c;它的内部运转逻辑&#xff1a;假设lock现在为false&#xff0c;代表临界资源A空闲&#xff…

AssertionError: Torch not compiled with CUDA enabled解決方案

在執行pytorch代碼的時候&#xff0c;突然看到報錯 AssertionError: Torch not compiled with CUDA enabled 這說明了 1. 你pytoch确实安装了 2. 你安装的是cpu版本 作为验证&#xff0c;你可以在python编辑器输入下列代码 解决方案 首先&#xff0c;安装Nvidia toolkit su…

(深度学习快速入门)第二章:从线性神经网络入手深度学习(波士顿房价案例)

文章目录一&#xff1a;波士顿房价预测数据集说明二&#xff1a;Pytorch搭建模型&#xff08;1&#xff09;数据处理&#xff08;2&#xff09;网络结构&#xff08;3&#xff09;损失函数&#xff08;4&#xff09;优化方法&#xff08;5&#xff09;训练预测&#xff08;6&am…

pytorch应用(入门4)MLP实现MNIST手写数字分类

深层神经网络 前面一章我们简要介绍了神经网络的一些基本知识&#xff0c;同时也是示范了如何用神经网络构建一个复杂的非线性二分类器&#xff0c;更多的情况神经网络适合使用在更加复杂的情况&#xff0c;比如图像分类的问题&#xff0c;下面我们用深度学习的入门级数据集 M…

MyBatisPlus ---- 条件构造器和常用接口

MyBatisPlus ---- 条件构造器和常用接口1. wapper介绍2. QueryWrappera>例1&#xff1a;组装查询条件b>例2&#xff1a;组装排序条件c>例3&#xff1a;组装删除条件d>例4&#xff1a;条件的优先级e>例5&#xff1a;组装select子句f>例6&#xff1a;实现子查询…

dubbo源码实践-SPI扩展-自适应扩展机制

目录 1 前提必备知识 2 术语定义 3 自适应扩展机制的特点 4 扩展点实践 4.1 用户自定义自适应扩展 4.2 dubbo生成自适应扩展 4 自适应扩展类的用途 1 前提必备知识 具体的使用和原理就不说了&#xff0c;网上有人写的挺好的了。 可以参考&#xff1a; Dubbo SPI之自适…

【北京理工大学-Python 数据分析-1.1】

数据维度 维度&#xff1a;一组数据的组织形式 一维数据&#xff1a;由对等关系的有序或无序数据构成&#xff0c;采用线性组织形式。包括列表、集合和数组&#xff08;python中不常见&#xff0c;但在C和Java中比较常见&#xff09;类型。 列表&#xff1a;数据类型可以不同…

讲座笔记:Representation Learning on Networks

1 传统机器学习 传统机器学习需要进行很多的特征工程 我们希望模型自动学习特征&#xff0c;而不是用人为特征工程的方式1.1 目标 1.2 难点 graph更复杂&#xff0c;CNN和RNN很难直接应用 ——>复杂的拓扑结构&#xff08;不像CNN有网格的概念&#xff09;——>没有固定…

国家队入场,中国数字资产交易市场或将迎来新一轮“洗牌”

‍‍数据智能产业创新服务媒体——聚焦数智 改变商业数字化已经成为中国文化产业的催化剂&#xff0c;一大批文化资源在数字技术的赋能下焕发了崭新的生机。随着数字化的升级与科技进步&#xff0c;数字经济正在成为改变全球竞争格局的关键力量&#xff0c;各国家都争先出台相…

【微服务】Nacos 健康检查机制

目录 一、前言 二、注册中心的健康检查机制 三、Nacos 健康检查机制 四、临时实例健康检查机制 五、永久实例健康检查机制 六、集群模式下的健康检查机制 七、小结 &#x1f496;微服务实战 &#x1f496; Spring家族及微服务系列文章 一、前言 在前文中&#xff0c;…

使用 Helm 安装 MQTT 服务器-EMQX

EMQX ℹ️ Info: 使用 EMQX 通过 Helm3 在 Kubernetes 上部署 EMQX 4.0 集群 | EMQ emqx/deploy/charts/emqx at main-v4.4 emqx/emqx (github.com) emqx/values.yaml at main-v4.4 emqx/emqx (github.com) emqx/emqx-operator: A Kubernetes Operator for EMQ X Broker (git…

Arch Linux/Manjaro安装pycharm

首先换清华源以加快速度 &#xff08;已经换源的小伙伴可以跳过这一步&#xff09; 首先安装vim&#xff0c;用来编辑文件&#xff0c;已经安装过的可以跳过这一步。 sudo pacman -S vim 然后使用vim编辑配置文件 sudo vim /etc/pacman.d/mirrorlist 打开文件以后按inser…

Javaweb——第二章 Jsp和Servlet

目录 2.1 JSP概述 2.2 Servlet概述 2.3 jsp和servlet的区别和联系&#xff1a; ​编辑 2.4 Jsp的生命周期 2.4.1 JSP编译 2.4.2 JSP初始化 2.4.3 JSP执行 2.4.4 JSP清理 2.5 Servlet 2.5.1 Servlet的工作模式 2.5.2 Servlet创建方式 2.5.3 Servlet生命周期 2.1 JS…

JAVA之网络编程学习

文章目录一 java网络编程初识1.1 概述1.2 C/S 架构&B/S架构认识1.2.1 C/S架构1.2.2 B/S架构1.3 网络通信两个要素1.4 IP(Inet Adderss)1.5 InetAddress演示1.6 端口号1.7 InetSocketAddress使用1.8 网络通信协议二 TCP网络编程2.1 信息通信案例2.1.1 TcpClientDemo2.1.2 Tc…

聊聊接口文档的事儿

1、前言 大家好&#xff0c;欢迎来到我的吉鹿&#xff08;记录&#xff09;空间。 最近在做一个前后端分离的项目时&#xff0c;由于后端提供的 API 接口文档实在是一言难尽&#xff0c;导致了开发的效率大大降低。于是我出手了&#xff0c;我决定薅完我20几年的头发来肝一下…

TC275——10GPT12_PWM_Generation

产生PWM的方式有很多&#xff0c;这里尝试使用TC275的GPT12模块&#xff0c;来产生具有固定频率和可变占空比的PWM信号。 GPT12就是General Purpose Timer Unit通用定时器模块&#xff0c;它包含5个16位定时器&#xff0c;被分给GPT1和GPT2。 这里使用GPT1&#xff08;T2、T3…

LinuxQQ3.0体验和下载方式

体验 2022年12月31日推出了LinuxQQ3.0版本&#xff0c;2.0版本特别复古 3.0特别丝滑 支持QQ空间 管理界面与WindowsQQ一致 支持截图和表情&#xff0c;传输文件图片很方便 下载方式 下载链接 im.qq.com/linuxqq/index.shtml 选择合适的版本下载即可 从下载文件夹中可以找到…

.Net 6实现旋转验证码

前几篇文章&#xff0c;介绍了.Net 6实现的滑动验证码功能&#xff0c;最近把滑动验证码的ImageSharp替换成了SkiaSharp&#xff0c;其中抠图部分参考了pojianbing大神的代码。滑动验证码完成之后&#xff0c;心想着。做一个旋转验证码。其实旋转验证码跟滑动验证码及其类似。 …

跨系统实时同步数据解决方案

数据量太大&#xff0c;单存储节点存不下&#xff0c;就只能把数据分片存储。 数据分片后&#xff0c;对数据的查询就没那么自由。如订单表按用户ID作为Sharding Key&#xff0c;就只能按用户维度查询。我是商家&#xff0c;我想查我店铺的订单&#xff0c;做不到。&#xff0…