多线程(2)——线程的六种状态

news2024/12/24 2:20:08

1. 线程的所有状态

进程状态:

就绪:正在 cpu 上执行,或者随时可以去 cpu 上执行

阻塞:暂时不能参与 cpu 执行

Java 的线程,对于状态做了更详细的区分,不仅仅是就绪和阻塞了,六种:

  1. NEW:当前 Thread 对象虽然有了,但是内核的线程还没有(还没调用 start)
  2. TERMINATED:当前 Thread 对象虽然还在,但是内核的线程已经销毁了(线程已经结束了)
  3. RUNNABLE:就绪状态,正在 cpu 上运行 或 随时可以去 cpu 上运行
  4. BLOCKED:因为 锁竞争 引起的阻塞
  5. TIMED_WAITNG:有超时时间的等待,比如 sleep 或者 join 带参数版本
  6. WAITING:没有超时时间的等待 join /wait

上述线程状态都可以通过 jconsole 来观察

2. 线程安全

2.1 线程不安全代码:

public class Demo {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}

我们预期结果为 100000,运行结果为:

实际运行结果与预期不符,这就是 bug


当把代码改为:

上面写法中 t1 先执行,t1 执行完,t2 再执行,t1 和 t2 是串行执行的,这样就没问题

而一开始的代码中 t1 和 t2 是并发执行的,是有 bug 的

像这样因为多个线程并发执行引起的 bug 称为“线程安全问题”或者叫做“线程不安全


2.2 上例线程不安全的原因:

上述代码中的 count++ 操作,在 cpu 的视角来看,是 3 个指令

1) 把内存中的数据读取到 cpu 寄存器里 (load)

2) 把 cpu 寄存器里的数据 +1                  (add)

3) 把寄存器的值写回内存                       (save)

tip:由于不同架构的 cpu 有不同的指令集,不同的指令集里有不同的指令,针对这三个操作,不同 cpu 里的对应指令名称肯定是不同的!

cpu 在调度执行线程的时候,不一定什么时候就会把线程给切换走(抢占式执行,随机调度)

指令是 cpu 执行的最基本单位,要调度至少把当前指令执行完,不会执行一半调度走

但是由于这里 count++ 是三个指令,可能会出现 cpu 执行了其中的 1 个或 2 个或 3 个指令调度走的情况(都有可能,无法预测)

基于上面的情况,两个线程同时对 count 进行 ++ 就容易出现 bug

画图示例:

但上述的执行顺序,只是一种可能的调度顺序,由于调度过程是“随机”的,因此就会产生很多其他的执行顺序(下面就是得到不正确结果的调度,略写...)


tip:得到值 <50000 的特殊情况

 

上面这样,在 t1++ 一次的过程中,t2++ 两次,这样的结果一共是 ++3 次,实际上只得到了 1

这样 t1 和 t2 相互踩对方的结果就会出现 <50000 的情况(需要两个线程实际有效 的次数都得 < 2.5w)

2.3 线程不安全的原因

1) 线程在操作系统中是 随机调度,抢占式执行(根本原因)

2) 多个线程同时修改同一个变量

3) 修改操作不是“原子”的

4) 内存可见性问题

5) 指令重排序

3. 解决线程不安全问题(synchronized 关键字 -- 监视器锁 monitor lock)

最主要的方法就是把“非原子”的修改成“原子”的

3.1 synchronized 使用

synchronized ( ),是关键字,不是函数,( ) 中的并非“参数”

需要指定一个“锁对象”,通过锁对象来进行后续的判定,这里的 ( ) 可以指定任何的对象

3.1.1 针对上面例子加锁:

{ } 中的代码就是需要加锁的代码,只要是合法的 Java 代码,都可以放入

执行过程分析:

由于 t1 和 t2 都是针对 locker 对象加锁,t1 先加锁成功了,所以 t1 继续执行 { } 中的代码,t2 后加锁,发现 locker 对象已经被其他线程加锁了,所以 t2 只能阻塞等待

又因为 t1 的 unlock 操作一定是在 save 之后,确保了 t2 执行 load 的时候,t1 已经 save ,这样两者进行 ++ 操作,就不会因为穿插执行而导致相互覆盖对方结果了

本质上是把随机的并发执行过程强制变成了串行

tip:

1. 锁对象,最重要的是看多个线程是否是同一个锁对象

        针对同一个对象加锁,就会出现“阻塞”(锁竞争/锁冲突)

        针对不同对象加锁,不会出现“阻塞”,两个线程仍然是随机调度的并发执行

2. 锁对象不能用 int,double 这种内置类型,必须是 Object 及其子类

3. 加锁代码是比 join 串行效率高很多的,加锁只是将线程中的一小部分逻辑变为“串行执行”,剩下的其他部分仍然可以并发执行

3.1.2 三个线程对同一个对象加锁

假设有 1,2,3 线程

1 先拿到锁,2 和 3 阻塞等待,当 1 释放锁后, 2 和 3 谁先拿到锁是不一定的,是随机的,即使在代码中是 2 先加锁,3 后加锁,也不一定谁先拿到

3.2 synchronized 特性

3.2.1 底层原理

synchronized 是 JVM 提供的功能,synchronized 底层实现就是在 JVM 中通过 C++ 代码实现的,也是依靠 操作系统 提供的 api 实现的加锁,操作系统的 api 则是来自于 cpu 上支持的特殊指令来实现的

因此,加锁操作并不是 Java 独有的,其他语言也有加锁操作

系统原生的加锁 api 其实是两个函数:lock()、unlock()

不仅仅原生 api 是这样,很多编程语言的加锁操作也是类似的封装方法,如 C++/Python 加锁是一个函数,解锁是一个函数,像 Java 这样通过 synchronized 关键字来同时完成加锁解锁是比较少见的

系统原生的这种做法一个最大的问题就是:unlock 可能会执行不到


3.2.2 加锁其他写法

一个 Java 进程中,一个类的类对象是只有唯一一个的,类对象也是对象,所以也能写到 synchronized( ) 里面,写类对象和写其他对象没有任何本质的区别

synchronized 修饰一个普通的方法


1) 相当于针对 this 加锁


2) synchronized 修饰一个静态方法

相当于针对 对应的类对象 加锁

static 方法没有 this ,其也叫做类方法,和具体的实例无关,只和类相关,而 this 是指向实例的

这样的写法就是在给类对象加锁


3.2.3 线程问题之——死锁

分析:

1) add 方法的 synchronized 想要拿到锁,就需要 for 循环中的 synchronized 释放锁

2) for 循环的 synchronized 想要释放锁就需要执行到 }

3) 要想执行到 } 就需要执行完这里的 add

4) 但是 add 正在阻塞中

出现死锁的三种场景:

1) 一个线程针对一把锁,连续加锁两次

但是当我们运行时,发现程序正常运行了,结果并没有问题,没有发生死锁,这是因为 Java 的 synchronized 为了减少我们写出死锁的概率,引入了 “可重入锁” 的特殊机制,解决了上述问题


2) 两个线程两把锁

线程1、线程2、锁A、锁B

线程 1 先对 A 加锁,线程 2 对 B 加锁;线程 1 不是放锁 A 的前提下,再对 B 加锁,同时线程 2 在不释放锁 B 的前提下,再对 A 加锁

public class Demo17 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                System.out.println("t1 加锁 locker1 完成");

                //这里的 sleep 是为了确保 t1 和 t2 都先分别拿到 locker1 和 locker2,然后再分别拿对方的锁
                //如果没有 sleep,执行顺序就不可控,可能会出现某个线程一下拿到两把锁,另一个线程还没执行,导致无法构造出死锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker2) {
                    System.out.println("t1 加锁 locker2 完成");
                }
            }
        });

        Thread t2 = new Thread(() -> {
           synchronized (locker2) {
               System.out.println("t2 加锁 locker2 完成");

               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }

               synchronized (locker1) {
                   System.out.println("t2 加锁 locker1 完成");
               }
           }
        });

        t1.start();
        t2.start();
    }
}

运行结果:


3) N 个线程,M 个锁(经典模型:哲学家就餐问题)

现有五位哲学家,他们坐在一个圆桌旁边,每个人左手边有一根筷子,圆桌中间是他们要吃的大碗宽面

任何一个科学家要想吃到面条都需要拿起左手和右手的筷子,他们现在只会做两件事:1) 思考人生,放下手里的筷子;2) 吃面条,拿起左右手两边的筷子

通常情况下,这个模型是可以运行的,但是当所有哲学家在某一时刻,同时想要吃面条,同时拿起了自己左手边的筷子,他们就拿不到右手边的筷子了,由于这些哲学家很固执,当他们吃不到面条的时候,绝不会放下左手的筷子,此时就形成了死锁


总结:死锁的四个必要条件(缺一不可)

1) 锁是互斥的(锁的基本特性)

2) 锁是不可被抢占的(线程 1 拿到了锁 A,如果线程 1 不主动释放 A,线程 2 不能把锁 A 抢过来)(锁的基本特性)

3) 请求和保持:线程 1 拿到锁 A 之后,不释放 A 的前提下,去拿锁 B;如果是先释放 A ,再拿 B 就不会有问题(特殊情况,有些代码里面就需要写成请求保持的方式)

4) 循环等待/环路等待/循环依赖(多个线程获取锁的过程中,存在循环等待...)

假设代码按照请求和保持的方式获取到 N 个锁,只需要给锁编号(1,2,3,N...),约定所有的线程在加锁的时候都必须按照一定的顺序来加锁(比如,必须先针对编号小的锁加锁,后针对编号大的锁加锁)


哲学家就餐问题解决方案:

假设同一时间,所有哲学家拿起第一根筷子

给筷子编号(1~5),规定每个哲学家每次拿筷子都要从小往大拿,即:

第一位哲学家不能拿左手边的2号筷子,先拿起右手边的1号筷子

第二位拿起右手边的2号筷子

第三位拿起右手边的3号筷子

第四位拿起右手边的4号筷子

第五位不能拿左手边的5号筷子,阻塞等到右手边的1号筷子被放下

由于5号筷子是空闲的,第四位就可以拿起5号筷子并吃到面条,之后就会释放4号5号筷子,3号就可以拿起4号筷子吃面条......

只要遵守上述的拿起筷子的顺序,无论接下来这个模型运行顺序如何,出现怎么极端的情况都不会出现死锁了

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

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

相关文章

2024懒人精灵七天从入门到精通实战课程(付源码)

写在开始&#xff1a;对于想学习自动化技术的同学&#xff0c;给你们一些建议&#xff1b;如果你已经选择开始&#xff0c;还在坚持&#xff0c;我奉劝你坚持下去&#xff0c;水到渠成&#xff1b;如果你还没开始就选择放弃&#xff0c;我建议你就此放弃&#xff0c;老师也不鼓…

合宙Air780EP_LuatOS_MQTT应用指南

简介 Air780EP 是合宙的低功耗4G模组之一&#xff0c;支持LuatOS的脚本二次开发&#xff0c;即使是不太懂技术的老板&#xff0c;也能轻松使用 Air780EP开发产品。 本文应各位大佬邀请&#xff0c;详细讲解了Air780EP 的MQTT的应用教程&#xff01; MQTT协议具有长连接、低带宽…

开源AI智能名片商城系统:重塑大零售生态的创新实践与深度分析

摘要&#xff1a;在数字经济浪潮的推动下&#xff0c;零售行业正经历着前所未有的变革。传统零售模式面临消费者需求多样化、市场竞争加剧等多重挑战&#xff0c;而开源AI智能名片商城系统的出现&#xff0c;为零售行业的转型升级提供了新的思路和技术支持。本文深入探讨了开源…

C语言----字符串的匹配

字符串的匹配 实例说明&#xff1a; 本实例实现对两个字符串进行匹配操作&#xff0c;即在第一个字符串中查找是否存在第二个字符串。如果字符串完全匹配&#xff0c;则提示匹配的信息&#xff0c;并显示第二个字符串在第一个字符串中的开始位置&#xff0c;否则提示不匹配。 …

STM32 - 笔记

1 STM32的串口通信 【keysking的STM32教程】 第8集 STM32的串口通信_哔哩哔哩_bilibili 波特律动 串口助手

观成科技:海莲花活跃木马KSRAT加密通信分析

概述 自2023年8月至今&#xff0c;海莲花组织多次利用KSRAT远控木马对我国发起攻击。KSRAT通过HTTP协议与C&C服务器进行通信&#xff0c;每个样本都使用了不同的URL。其心跳包采用XOR算法进行加密&#xff0c;而控制指令包和数据回传包则使用了XOR以及“XORAES-128-CBC”组…

【C语言篇】递归详细介绍(基础概念习题及汉诺塔等进阶问题)

文章目录 递归是什么递归的思想递归的限制条件 递归举例求n的阶乘分析和代码实现画图推演 顺序打印一个整数的每一位分析和代码实现画图推演 递归与迭代递归求第n个斐波那契数迭代求第n个斐波那契数 拓展练习青蛙跳台阶问题递归求解迭代求解 汉诺塔问题 递归是什么 递归是学习…

FPGA使用sv生成虚拟单音数据

FPGA使用sv生成虚拟单音数据 之前一直使用matlab生成虚拟的数据&#xff0c;导出到txt或是coe文件中&#xff0c;再导入到fpga中进行仿真测试。 复杂的数据这样操作自然是必要的&#xff0c;但是平日使用正弦数据进行测试的话&#xff0c;这样的操作不免复杂&#xff0c;今日…

CentOS 安装Redis

在 CentOS 安装 Redis 操作系统&#xff1a;centos-7.9.2009-Core 1. 更新系统 首先&#xff0c;确保你的系统是最新的&#xff1a; sudo yum update -y2. 安装 EPEL 仓库 Redis 可能不在默认的 CentOS 仓库中&#xff0c;因此你需要安装 EPEL&#xff08;Extra Packages f…

【源码+文档+调试讲解】活力健身馆管理系统

摘 要 活力健身馆管理系统的目的是让使用者可以更方便的将人、设备和场景更立体的连接在一起。能让用户以更科幻的方式使用产品&#xff0c;体验高科技时代带给人们的方便&#xff0c;同时也能让用户体会到与以往常规产品不同的体验风格。 与安卓&#xff0c;iOS相比较起来&am…

springboot狱内罪犯危险性评估系统的设计与实现论文源码调试讲解

第一章系统成功运行案例 第2章 程序开发技术 2.1 Mysql数据库 开发的程序面向用户的只是程序的功能界面&#xff0c;让用户操作程序界面的各个功能&#xff0c;那么很多人就会问&#xff0c;用户使用程序功能生成的数据信息放在哪里的&#xff1f;这个就需要涉及到数据库的知识…

CTFHUB-web-RCE-eval执行

开启题目 查看源码发现直接用蚁剑连接就可以&#xff0c;连接之后发现成功了

冒烟测试:快速初步检测软件稳定性的关键步骤

目录 前言1. 冒烟测试的定义1.1 冒烟测试的起源1.2 冒烟测试的特点 2. 冒烟测试的重要性2.1 确保构建稳定性2.2 早期发现重大问题2.3 提高测试效率 3. 冒烟测试的实施方法3.1 制定测试计划3.2 选择测试用例3.3 执行测试3.4 分析测试结果 4. 冒烟测试的最佳实践4.1 自动化冒烟测…

Opera浏览器与IPXProxy代理IP集成步骤详解

​对于经常需要使用公共WiFi网络进行网络操作的人来说&#xff0c;安全性是至关重要的。作为Opera浏览器的用户&#xff0c;我非常关注隐私安全&#xff0c;也尝试过各种提高安全性的方法&#xff0c;其中使用IPXProxy代理IP是不错的方法之一。下面为大家带来Opera浏览器与IPXP…

【java】升级jetty-client解决Too many open files问题

文章目录 升级jetty-client解决Too many open files问题问题背景排查原因解决lsof命令拓展 升级jetty-client解决Too many open files问题 问题背景 生产环境的采集经过一段时间就会报错 Too many open files&#xff0c;导致接下来的采集都会失败&#xff0c;已经严重影响到…

NSSCTF练习记录:[SWPUCTF 2021 新生赛]include

题目&#xff1a; 随便传入一个file 因为存在include_once函数&#xff0c;可以使用php伪协议获取flag.php源码&#xff0c;再通过base64解码得到flag。 php:// 访问各个输入/输出流&#xff0c;常用php://filter和php://input&#xff0c;php://filter用于读取源码&#xff…

gin框架 自定义404错误页面,自定义500等服务端异常,业务异常,根据不同异常类型显示不同的异常页面方法 整理

在gin框架中&#xff0c;要显示自定义的异常页面&#xff0c;首先需要通过gin路由对象中的LoadHTMLFiles或者LoadHTMLGlob方法加载自定义的错误页面模板文件&#xff0c; 然后定义符合 gin.HandlerFunc 类型的路由处理函数/方法 &#xff0c;即只有一个参数(c *ginx.XContext)的…

如何理解供应链控制塔?详解供应链控制塔类型与架构!

随着经济全球化的不断深入&#xff0c;企业供应链的复杂性也在不断增加。从供应商到制造商&#xff0c;再到分销商和消费者&#xff0c;全球供应链网络的每一个环节都充满了动态变化和不确定性。在这样的背景下&#xff0c;传统的供应链管理模式已难以满足现代企业的需求&#…

Ackites/Killwxapkg

自动化反编译微信小程序&#xff0c;小程序安全评估工具&#xff0c;发现小程序安全问题&#xff0c;自动解密&#xff0c;解包&#xff0c;可还..自动化反编译微信小程序&#xff0c;小程序安全评估工具&#xff0c;发现小程序安全问题&#xff0c;自动解密&#xff0c;解包&a…

【消息队列】kafka如何保证消息不丢失?

&#x1f44f;大家好&#xff01;我是和风coding&#xff0c;希望我的文章能给你带来帮助&#xff01; &#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&#x1f44d;一下博主哦 &#x1f4dd;点击 我的主页 还可以看到和风的其他内容噢&#x…