JavaEE——多线程的状态及线程安全问题

news2024/11/16 11:51:38

目录

一、线程的状态

1、NEW

2、 TERMINATED

3、RUNNABLE

4、TIMED_WAITING

5、 BLOCKED

6、WAITING

二、线程安全问题

1、线程不安全的原因

2、一个线程不安全的实例

3、加锁操作

4、产生线程不安全的原因

什么是内存可见性呢?

解决方案?

5、指令重排序


Java 线程共有几种状态?状态之间怎么切换的?
NEW: 安排了工作 , 还未开始行动 . 新创建的线程 , 还没有调用 start 方法时处在这个状态 .
RUNNABLE: 可工作的 . 又可以分成正在工作中和即将开始工作 . 调用 start 方法之后 , 并正在
CPU 上运行 / 在即将准备运行 的状态 .
BLOCKED: 使用 synchronized 的时候 , 如果锁被其他线程占用 , 就会阻塞等待 , 从而进入该状
.
WAITING: 调用 wait 方法会进入该状态 .
TIMED_WAITING: 调用 sleep 方法或者 wait( 超时时间 ) 会进入该状态 .
TERMINATED: 工作完成了 . 当线程 run 方法执行完毕后 , 会处于这个状态 .

一、线程的状态

1、NEW

new表示安排了工作还未开始行动。

看代码:while()循环里面啥也不用给,我们直接通过t.getState()这个方法获取指定线程的状态

public class demo14 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
 
            }
        });
        System.out.println(t.getState());//通过这个方法获取指定线程的状态
        t.start();
    }
}

2、 TERMINATED

TERMINATED:表示工作完成了。

代码:这里我们直接去掉while()循环,啥也不用干,那肯定代码是执行完了,先调用start方法,然后获取我们的线程状态。

public class demo14 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

3、RUNNABLE

RUNNABLE:表示就绪状态。处于这个状态的线程,就是在就绪队列中,随叫随到,随时可以到CPU上执行。

代码:直接给一个空的while循环,不用加sleep等操作,方便我们观察代码的就绪状态。

public class demo14 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
 
            }
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

4、TIMED_WAITING

即代码中调用了sleep就会进入到TIMED_WAITING,意思就是当前线程在一定时间内是阻塞状态。

public class demo14 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

5、 BLOCKED

表示排队等待其他事情。详情参考本文下面的3、加锁操作。

6、WAITING

也表示排队等待其他事情。

OK,到这里就可以总结出线程状态转换的简易图。


二、线程安全问题

谈到线程安全,那么这里是很重要的一个板块,在面试中如果遇到多线程的问题,基本都会涉及到线程安全问题,同时也是一个难点,希望大家认真阅读。

1、线程不安全的原因

操作系统调度线程的时候是随机的,线程之间是抢占式执行,正因为这种特性很可能会导致程序出现一些bug。

2、一个线程不安全的实例

这里我们使用两个线程,对同一个整型变量各自-自增5w次,看最终的结果。

class Counter{
    public int count = 0;
    public void increase(){
        count++;
    }
 
public class demo15 {
    private static  Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
 
        t1.join();
        t2.join();
        //必须要在t1,t2都执行完了之后,再打印count
        //否则,t1,t2,main都是并发执行的关系,导致t1,t2还没执行完,就先执行了下面的打印操作
        //在main中打印两个线程执行自增完成之后,得到的count结果
        System.out.println(counter.count);
    }
}

注意这里我们需要等待线程t1,t2都结束之后再打印结果,因为main线程和t1,t2线程是并发执行的关系,所以使用到了join函数

我们发现结果并不是10w,而是一个介于5w - 10w之间的数字,那么为什么会这样呢?

要想知道其中原因,我们就要来了解一个count++到低是如何实现的。

站在CPU的角度:count++实际上是三个CPU指令。

1、把内存中的count的值,加载到CPU寄存器中。(load)

2、把寄存器中的值+1。(add)

3、把寄存器的值写回到内存的count中。(save)

由于线程调度是抢占式执行的,并且这个count++操作是分为3步来完成的,那么t1,t2同时执行这三个操作的时候,顺序上是有很多种可能的,这就可能导致我们最终的结果达不到10w。

结果为什么在5w - 10w之间?

 这5w并发相加中,有时候可能是串行的(+2),有时候可能是交错的,串行与交错多少次咋们不知道,这都是随机的,极端情况下,所有操作都是串行的,结果就是10w,所有操作都是交错的,结果就是5w,不过都是小概率事件。

3、加锁操作


那么如何解决上述线程不安全问题呢?

答案:加锁!

这里举个例子:比如李华准备去ATM机里面取钱,那么ATM机器一般是在一个小房间里面,当李华走进去的时候他就可以给这个房间的门上锁,这样赵四,王五等等想取钱的话只能等李华取完(这个等待操作就是阻塞),李华取完钱后便可以给这个房间解锁,下一个人才能进去取钱。

如何加锁?

那么java中加锁的方式有很多种,最常使用的是 synchronized 关键字。我们可以给上述代码重大的自增函数increase()前面加上synchronized 关键字就是加锁成功了。

 public int count = 0;
 synchronized public void increase(){
        count++;
 }

我们在看运行结果,这下便是10w,符合我们的预期。

那么给方法加上 synchronized 关键字之后,此时进入方法就会自动加锁,离开方法就会自动锁,当一个线程加锁成功的时候,其他线程尝试加锁就会触发阻塞等待。这个时候线程的状态就称为BLOCKED。

4、产生线程不安全的原因

1、线程是抢占式执行的,线程间的调度充满随机性。(线程不安全的根本原因)

2、多个线程对同一个变量进行修改操作。

3、针对变量的操作不是原子的,通过加锁操作就是把几个指令打包成一个原子的。

4、内存可见性。

什么是内存可见性呢?

假设我们这里有两个线程t1,t2;t1只进行读操作,t2在合适的时候会进行修改操作。

t1 这个线程在循环读这个变量。那么读取内存操作,相比于读取寄存器,是一个非常低效的操作作。 (慢 3-4 个数量级)
因此在 t1 中频繁的读取这里内存的值就会非常低效。
而且如果 t2 线程迟迟不修改,t1 线程读到的值又始终是一样的值!!
因此,t1 可能会 直接从寄存器里读(即不执行 load )一旦 t1 做出了这种大胆的假设,此时万一 t2 修改了 count 值, t1 就不能感知到了。

import java.util.Scanner;
 
public class demo16 {
    public static int isquit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(()-> {
            while (isquit == 0){
 
            }
            System.out.println("循环结束,t线程退出!");
        });
        t.start();
 
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个isquit的值:");
        isquit = sc.nextInt();
        System.out.println("main线程执行完毕!");
    }
}

 

那么这个案例就很好的说明了上述内存可见性的问题,我们手动输入isquit的值为2,那么while循环结束按理说应该打印 System.out.println("循环结束,t线程退出!");正是因为t线程一直反复读isquit的值,于是它直接从寄存器读,那么我们修改了isquit的值,它也就感知不到了。所以就没有打印 循环结束,t线程退出! 这条语句。

解决方案?

1、使用synchronized 关键字。synchronized 不光能够保证原子性,同时也能够保证内存可见性。被synchronized 包裹起来的代码,编译器就不敢轻易做出上述假设,就相当于手动禁止了编译器的优化。

2、使用volatile关键字。volatile和原子性无关,但是能够保证内存可见性。使得编译器每次都重新从内存中读取isquit的值。

  public static volatile int isquit = 0;

5、指令重排序

这个操作也是编译器优化的一种操作,编译器会智能的调整代码的前后顺序从而提高程序的效率。

保证逻辑不变的前提,再去调整顺序。使用synchronized 也能够禁止指令从排序!!

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

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

相关文章

【Linux学习】1-2 新建虚拟机ubuntu环境

1.双击打开VMware软件&#xff0c;点击“创建新的虚拟机”&#xff0c;在弹出的中选择“自定义&#xff08;高级&#xff09;” 2.点击下一步&#xff0c;自动识别ubuntu光盘映像文件&#xff0c;也可以点击“浏览”手动选择&#xff0c;点击下一步 3.设置名称及密码后&#xf…

web - RequestResponse

##Request&Response 1&#xff0c;Request和Response的概述 Request是请求对象&#xff0c;Response是响应对象。这两个对象在我们使用Servlet的时候有看到&#xff1a; 此时&#xff0c;我们就需要思考一个问题request和response这两个参数的作用是什么? request:获取请…

基于微信小程序的竞赛答题小程序开发笔记(一)

开发背景调研 中小学学科答题小程序&#xff0c;适合各中小学校方&#xff0c;老师或者家长。通过互动和参与式学习&#xff0c;小程序能够通过游戏化元素提升学习的积极性和参与度&#xff0c;从而提升学习效率&#xff0c;促进学生自主学习 功能规划 分类题库&#xff1a;…

专题八_链表_算法专题详细总结

目录 链表 1.常用技巧 1&#xff09;画图&#xff01;&#xff01;&#xff01; -> 直观 形象 便于我们理解 2&#xff09;引入虚拟“头”节点 1.便于处理边界条件 2.方便我们对链表进行操作 3.不要吝啬空间&#xff0c;大胆定义变量 4.快慢双指针 1.判断链表是否…

redis学习(014 实战:黑马点评:优惠券秒杀——1人只可以下1单问题解决方案)

黑马程序员Redis入门到实战教程&#xff0c;深度透析redis底层原理redis分布式锁企业解决方案黑马点评实战项目 总时长 42:48:00 共175P 此文章包含第54p-第p55的内容 文章目录 一人一单问题分析第一种写法 查询后进行添加第二种写法 加悲观锁在用户上加悲观锁&#xff08;提…

Vue 响应式监听 Watch 最佳实践

一. 前言 上一篇文章我们学习了 watch 的基础知识&#xff0c;了解了它的基本使用方法及注意事项&#xff0c;本篇文章我们继续了解在Vue 中 响应式监听 watch 的妙用。了解 watch 的基础使用请参考上一篇文章&#xff1a; 详解 Vue 中 Watch 的使用方法及注意事项https://bl…

53 语言模型(和之后用来训练语言模型的数据集)_by《李沐:动手学深度学习v2》pytorch版

系列文章目录 文章目录 系列文章目录理论部分使用计数来建模N元语法总结 代码读取长序列数据随机采样顺序分区 小结练习 理论部分 在上一部分中&#xff0c;我们了解了如何将文本数据映射为词元&#xff0c;以及将这些词元可以视为一系列离散的观测&#xff0c;例如单词或字符…

(务必收藏)推荐市面上8款AI自动写文献综述的网站

在当前的学术研究和论文写作中&#xff0c;AI技术的应用已经变得越来越普遍。特别是在文献综述这一环节&#xff0c;AI工具能够显著提高效率并减少人工劳动。以下是市面上8款推荐的AI自动写文献综述的网站&#xff1a; 一、千笔-AIPassPaper 是一款备受好评的AI论文写作平台&…

java 框架组件

Java 框架是一系列预先编写好的、可复用的软件组件&#xff0c;它们旨在帮助开发者快速构建高质量的应用程序。Java 社区拥有众多优秀的框架&#xff0c;涵盖了从 Web 开发到大数据处理的各个领域。下面是一些流行的 Java 框架及其主要用途&#xff1a; Spring框架&#xff1a;…

基于丹摩智算部署SD3+ComfyUI文生图详解

目录 丹摩智算简介SD3ComfyUI文生图简介 SD3ComfyUI文生图部署步骤1.1、实例创建 操作步骤从HF-mirror下载SD3模型安装git安装ComfyUI 丹摩智算简介 丹摩智算官网&#xff1a;https://www.damodel.com/home 丹摩智算&#xff08;DAMODEL&#xff09;是一款专为AI应用打造的智…

网红挣钱太容易了

你看最近这个三只羊小Y哥&#xff0c;因为月饼质量问题、因为大闸蟹的问题&#xff0c;上了好多次热搜&#xff0c;掉粉了几百万。还是有很多人在赶着要买他们家的东西。 你是他的粉丝&#xff0c;他是你的屠夫。只要冠以“全网最低价”的名号&#xff0c;就会有无数的粉丝跑过…

应用层协议 --- HTTP

序言 在上一篇文章中&#xff0c;我们在应用层实现了一个非常简单的自定义协议&#xff0c;我们在我们报文的首部添加了报文的长度并且使用特定的符号分割。但是想做一个成熟&#xff0c;完善的协议是不简单的&#xff0c;今天我们就一起看看我们每天都会用到的 HTTP协议 。 UR…

华语童声璀璨新星陈千言、陈万语闪耀荣登2024年度最受媒体欢迎女歌手

华语童声璀璨新星陈千言、陈万语闪耀荣登2024年度最受媒体欢迎女歌手 近日&#xff0c;华语乐坛传来一则令人振奋的消息&#xff0c;11 岁的双胞胎姐妹花陈千言和陈万语荣获 2024 华语童声最受媒体欢迎女歌手和第 15 届华语金曲奖优秀童星两项大奖&#xff01; 陈千言和陈万语…

前缀和(2)_【模板】二维前缀和_模板

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 前缀和(2)_【模板】二维前缀和_模板 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目…

centos磁盘逻辑卷LVM创建

centos磁盘逻辑卷LVM创建 一、磁盘逻辑卷LVM说明二、centos磁盘使用情况三、LVM安装指南1.LVM工具安装1. yum list lvm2. yum search lvm3. yum search pvcreate4. yum list lvm25. yum install lvm2 2.创建物理卷2.1磁盘情况查看2.2创建物理卷&#xff08;PV&#xff09; 3.创…

单词搜索问题(涉及递归等)

目录 一题目&#xff1a; 二思路解释&#xff1a; 三解答代码&#xff1a; 一题目&#xff1a; newcode题目链接&#xff1a; 单词搜索_牛客题霸_牛客网 二思路解释&#xff1a; 思路&#xff1a;个人理解是找到word中的第一个元素&#xff0c;然后去递归的上下左右查找&am…

python实现财会人工智能分享课件

前言&#xff1a; 一、财会与人工智能之间的联系 人工智能是计算机科学的一个分支&#xff0c;旨在模拟人类智能。自20世纪50年代起&#xff0c;AI经历了多个发展阶段&#xff0c;从规则基础系统到现在的深度学习技术&#xff0c;已经在医疗、制造、金融等多个行业得到广泛应用…

Volume数据管理

Volume 容器销毁时&#xff0c;保存在容器内部文件系统中的数据都会被清除&#xff0c;为了持久化保存容器的数据&#xff0c;可以使用kubernetes Volumevolume的生命周期独立于容器&#xff0c;Pod中的容器可能被销毁和重建&#xff0c;但Volume会被保留本质上&#xff0c;Kub…

【机器学习】Flux.jl 生态

官方API https://fluxml.ai/Flux.jl/stable/ecosystem/ 官网给出了 Flux’s model-zoo&#xff0c; 是一个庞大的案例库&#xff0c; 可以提供直观的参考&#xff0c; 并且还列举了基于 Flux.jl 开发的第三方库。 机器视觉 ObjectDetector.jl YOLO 抓取的“预备跑” 图像Met…

Vue3:作用域插槽

目录 一.性质 1.数据传递性 2.自定义显示方式 3.复用性 4.解耦性 二.作用 1.提高灵活性 2.增强可维护性 3.简化数据流 4.提升用户体验 三.使用 1.父组件 2.子组件 四.代码 1.父组件代码 2.子组件代码 五.效果 作用域插槽&#xff08;Scoped Slots&#xff09;…