线程的一些事(2)

news2025/5/12 20:20:36

在java中,线程的终止,是一种“软性”操作,必须要对应的线程配合,才能把终止落实下去

然而,系统原生的api其实还提供了,强制终止线程的操作,无论线程执行到哪,都能强行把这个线程干掉。

这样的操作Java的api中没有提供的,上述的做法弊大于利,强行取结束一个线程,很可能线程执行到一半,会出现一些残留的临时性质的“错误”数据。

public class ThreadDemo12 {
    public static void main(String[] args) {
        boolean isQuit = false;
        Thread t = new Thread(() -> {
            while (!isQuit){
                System.out.println("我是一个线程,工作中!!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            //当前是死循环,给了个错误指示
          /*  System.out.println("线程工作完毕!");*/
        });
        t.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("让t线程结束!");
        isQuit = true;
    }
}

我们将变量isQuit作为main方法中的局部变量。

弹出了警告,这就涉及到lambda表达式的变量捕获了,当前捕获的变量是isQuit所以对于isQuit来说,它要么加上final,要么不去进行修改。 

isQuit是局部变量的时候,是属于main方法的栈帧中,但是Thread lambda是又自己独立的栈帧的,这两个栈帧的生命周期是不一致的

这就可能导致main方法执行完了,栈帧就销毁了,同时Thread的栈帧还在,还想继续使用isQuit--

在java中,变量捕获的本质就是传参,就是让lambda表达式在自己的栈帧创建一个新的isQuit并把外面的isQuit的值拷贝过来(为了避免isQuit的值不同步,java就不让isQuit来进行修改)

等待线程


多个线程的执行顺序是随机的,虽然线程的调度是无序的,但是可以通过一些api来影响线程执行的顺序。

join就可以,

public class ThreadDemo14 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("我是一个线程,正在工作中...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("线程执行结束");
        });

        t.start();

       /* Thread.sleep(5000);*/
        //这个操作就是线程等待
        t.join();
        System.out.println("这是主线程,期望这个日志在 t 结束后打印");
    }
}

这种方法比sleep方法要好很多,毕竟谁也不知道t线程啥时候结束,用join可以让线程等 t 线程结束后再执行,这时候main线程的状态就是“阻塞”状态了。

Thread类基本的使用

1.启动线程        start方法

理解 run 和 start 区别

2.终止线程              核心让run方法能够快速结束

非常依赖 run 内部的代码逻辑

Thread   isInterrupted(判定标志位)/interrupt(设置标志位)

如果提前唤醒sleep会清楚标志位

3.等待线程 join 让一个线程等待另一个线程结束

线程之间的顺序我们无法控制,但我们可以控制结束顺序

获取线程引用

Thread.currentThread()获取到当前线程的 引用(Thread 的引用)

如果是继承Thread,直接使用 this 拿到线程实例

如果不是则需要使用  Thread.currentThread();

线程的状态

就绪:这个线程随时可以去 cpu 上执行

阻塞:这个线程暂时不方便去cpu上执行

java中线程又以下几种状态:

1.NEW Thread 对象创建好了,但是还没有调用 start 方法在系统中创建线程.
2.TERMINATED Thread 对象仍然存在,但是系统内部的线程已经执行完毕了
3.RUNNABLE 就绪状态.表示这个线程正在 cpu 上执行,或者准备就绪随时可以去 cpu 上执行4.TIMED WAITING 指定时间的阻塞, 就在到达一定时间之后自动解除阻塞.使用 sleep 会进入这个状态.使用带有超时时间的join也会
5.WAITING  不带时间的阻塞 (死等),必须要满足一定的条件,才会解除阻塞
6.BLOCKED 由于锁竞争,引起的阻塞,

 线程安全问题,来看下面的一段代码

public class ThreadDemo19 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
       //随便创建个对象都行
      /*  Object locker = new Object();*/

        //创建两个线程,每个线程都针对上述 count 变量循环自增 5w次
        Thread t1 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
              /*  synchronized(locker) {
                    count++;
            }*/
                count++;
            }
        });
        Thread t2 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
              /*  synchronized(locker) {
                    count++;
                }*/
                count++;
            }
        });
        t1.start();
        t2.start();

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

        //打印count结果
        System.out.println("count = " + count);
    }
}

 我们发现这个结果是错的,我们计算的结果应该是100000.

这就涉及到线程安全问题了

count++是由三个指令构成的
1.load     从内存中读取数据到cpu寄存器
2.add      把寄存器中的值 + 1
3.save      把寄存器的值写回到内存中

对于单个线程是没有这种问题的,但是对于多线程就会冒出来问题

我们发现预期是进行两次count++后返回的count为2,但是因为两个线程在读取时出现了问题,第二个线程读取的数据是还未进行更新的数据,这就导致出现了错误。

如果是这样的顺序自然没有问题了

 我们需要的进行顺序应该时等第一个线程save后第二个线程再进行load。

本质时因为线程之间的调度时无序的时抢占式执行

这就不得不提到String这个“不可变对象”了

1.方便JVM进行缓存(放到字符串常量池中)
2.hash值固定
3.线程安全的

线程不安全原因

1.根本原因  操作系统上的线程时“抢占式执行”“随即调度” => 线程之间执行顺序带来了很多变数

2.代码结构  代码中多个线程,同时修改同一个变量

1.一个线程修改一个变量

2.多个线程读取同一个变量

3.多个线程修改不同变量

这些都不会有事

3.直接原因 上述的线程修改操作本身不是’原子的‘

4.内存可见性问题

5.指令重排序问题

对于3这个问题我们可以找办法来解决

1.对于抢占式执行修改,这是无法改变的事

2.对代码结构进行调整,这是个办法,但在有些情况下也是不适用的

3.可以通过特殊手段将着三个指令打包为一个“整体”,我们可以对其进行加锁

加锁

目的:把三个操作,打包成一个原子操作

进行加锁的时候需要先准备好锁对象,一个线程针对一个锁对象加锁后,当其他线程对锁对象进行加锁,则会产生阻塞(BLOCKED)(锁冲突/锁竞争),一直到前一个线程释放锁为止

要加锁得用到synchronized。

 进入()就会加锁(lock),出了{ }就会解锁(unlock),synchronnized 是调用系统的 api 进行加锁,系统api本质上是靠 cpu 上特定指令完成加锁

当t1加锁后,在没解锁的情况下,t2再想进行加锁就会出现阻塞

在t1没有解锁的情况下,即使t1被调度出cpu,t2也还是在阻塞

即使这样会影响到执行效率,但也比串行要快不少。

我们只是对count加锁使得count串行,但for循环还是可以进行“并发”执行的

 

加锁之后结果就正确了。

 对于对象的话只要不是同一个对象就不会有竞争这一说。

1.如果一个线程加锁,一个不加,是不会出现锁竞争的

2.如果两个线程,针对不同的对象加锁,也还是会存在线程安全问题

3.

把count放到一个 Test t 对象中,通过add来修改锁对象的时候可以写作this

相当于给this加锁(锁对象 this)

对于静态方法的话相当于给类对象加锁

我们可不可以加两个锁呢?

是否会打印hello?

 为啥会打印成功?不应该出现锁冲突吗?

当前由于是同一个线程,此时锁对象,就知道了第二次加锁的线程,就是持有锁的线程,第二次操作,就可以直接放行不会出现阻塞。

这个特性被称为“可重入”

一旦上述的代码出现了阻塞,就称为“死锁”

可重入锁就是为了防止我们在“不小心”中引入的问题

当我们在第一次加锁的时候,计数器会进行加一操作,当第二次进行加锁的时候,大仙加锁的线程和持有锁线程是一个线程,这个时候就会加锁成功,并且计数器加一。

等到了计数器为0的时候才是真正的解锁了,对于可重入锁来说:

1.当前这个锁是被哪个线程持有的

2.加锁次数的计数器

计数器可以帮助线程清楚的记录有几个锁。

加锁能够解决线程安全问题,但同时也引入了一个新的问题就是死锁。

死锁的三种典型场景

1.一个线程一把锁

如果锁是不可重入锁,并且对一个线程对这把锁进行加锁两次

2.两个线程,两把锁

线程  1  获得 锁A

线程   2  获得 锁B

接下来 1 尝试获取B, 2 尝试获取 A就同样出现死锁了!!!     

一旦出现“死锁”,线程就“卡住了”无法继续工作

public class ThreadDemo22 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A){
                //sleep一下是给t2时间让t2也能拿到B
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //尝试获取B,并没有释放A
                synchronized (B){
                    System.out.println("t1拿到两把锁");
                }
            }

        });

        Thread t2 = new Thread(() -> {
            synchronized (B){
                //sllep一下,是给t1时间,让t1能拿到A
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //尝试获取A并没有获取B
                synchronized (A){
                    System.out.println("t2拿到了两把锁");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

就像这样。

 3.N个线程M把锁

哲学家就餐

 解决死锁问题的方案

产生死锁的四个必要条件

1.互斥使用,获取锁的过程是互斥的,一个线程拿到了这把锁,另一个线程也想获取,就需要阻塞等待。
2.不可抢占,一个线程拿到了锁之后,只能主动解锁,不能让别的线程强行把锁抢走。
3.请求保持,一个线程拿到了锁 A 之后,在持有A的前提下,尝试获取B
4.循环等待,环路等待

由于四个都是必要条件,所以只要破环一个就解决问题了。

1,2.锁最为基本的特性

3.代码结构要看实际需求

4.代码结构的,最为容易破坏

指定一定的规则,就可以有效的避免循环等待

1.引入额外的筷子

2.去掉一个线程

3.引入计数器,限制最多同时所少人吃饭

4.引入加锁顺序的规则

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

public class ThreadDemo23 {
    private static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flag == 0){

            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(() -> {
            System.out.println("请输入flag的值:");
            Scanner scanner = new Scanner(System.in);
            flag = scanner.nextInt();
        });
           t1.start();
           t2.start();
    }
}

运行代码发现,并没有我们想象的打印t1线程结束,而是直接不动了。

在这个过程中有两个关键的点

1.load 操作执行的结果,每次都是一样的(要想输入,过几秒才能输入,在这几秒都不知道循环都已经执行了上百亿次了)
2.load 操作开销远远超过 条件跳转

访问寄存器的操作速度,远远超过访问内存

由于load开销大,并且load的结果又一直没有变化,所以jvm就会怀疑load操作有必要存在的必要吗?

此时jvm就可能做出代码优化,把上述load操作,给优化掉(只有前几次进行load,后续发现,load反正都一样,静态分析代码,也没看到哪里改了flag,因此就把load操作,干掉了),干掉之后,就相当于不再重复读内存直接使用寄存器之前”缓存“的值,大幅度的提高循环的执行速度

多线程的情况下很容易出现误判,这里相当于 t2 修改了内存,但是 t1 没有看到这个内存优化,就称为”内存可见性“问题

我们发现在刚刚的代码加上sleep就会执行成功,即使sleep时间有多小。 因为不加sleep一秒钟可能循环上百亿次,load开销非常大,优化迫切程度就更高。

加了sleep,一秒钟可能循环的次数就可能变为1000次,这样load开销相对来说就小了,所以优化迫切程度就想对来说就低了。

内存可见性问题,其实是个高度依赖编译器优化的问题,啥时候触发这个问题,都不知道

所以干脆希望不要出现内存可见性问题,将上述优化给关闭了

这就要使用关键字 volatile 来对上述的优化进行强制的关闭(虽然开销大了,效率低了。但是数据准去性/逻辑正确性提高了)。

volatile 关键字

核心功能就是保证内存可见性(另一个功能进制指令重排序)

在上述的代码中,编译器发现,每次循环都要读取内存,开销太大,于是就把读取内存操作优化成读取寄存器操作,提高效率

在JMM模型的表述下

在上述代码中,编译器发现,每次循环都要读取”主内存“,就会把数据从”主内存“中复制到”工作内存“中,后续每次都是读取”工作内存“。 

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

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

相关文章

使用lldb看看Rust的HashMap

目录 前言 正文 读取桶的状态 获取键值对 键值对的指针地址 此时&#xff0c;读取数据 读取索引4的键值对 多添加几个键值对 使用i32作为键&#xff0c;&str作为值 使用i32作为键&#xff0c;String作为值 前言 前面使用ldb看了看不同的类型&#xff0c;这篇再使用…

2025最新免费视频号下载工具!支持Win/Mac,一键解析原画质+封面

软件介绍 适用于Windows 2025 最新5月蝴蝶视频号下载工具&#xff0c;免费使用&#xff0c;无广告且免费&#xff0c;支持对原视频和封面进行解析下载&#xff0c;亲测可用&#xff0c;现在很多工具都失效了&#xff0c;难得的几款下载视频号工具&#xff0c;大家且用且珍…

Newton GPU 机器人仿真器入门教程(零)— NVIDIA、DeepMind、Disney 联合推出

系列文章目录 目录 系列文章目录 前言 一、快速入门 1.1 实时渲染 1.2 USD 渲染 1.3 示例&#xff1a;创建一个粒子链 二、重要概念 三、API 参考 3.1 求解器 3.1.1 XPBD 求解器 3.1.2 VBD 求解器 3.1.3 MuJoCo 求解器 3.2 关节控制模式 四、Newton 集成 4.1 Is…

【C++】学习、项目时Debug总结

这里写目录标题 1. 内存问题1.1. 内存泄漏1.1.1. 内存泄漏案例检查方法1.1.2. 主线程提前退出导致【控】1.1.3. PostThreadMessage失败导致的内存泄漏**【控】**1.1.4. SendMessage 时关闭客户端【控】1.1.5. 线程机制导致【**控】**1.1.6. exit&#xff08;0&#xff09;导致【…

26考研——中央处理器_指令流水线_指令流水线的基本概念 流水线的基本实现(5)

408答疑 文章目录 六、指令流水线指令流水线的基本概念流水线的基本实现流水线设计的原则流水线的逻辑结构流水线的时空图表示 八、参考资料鲍鱼科技课件26王道考研书 六、指令流水线 前面介绍的指令都是在单周期处理机中采用串行方法执行的&#xff0c;同一时刻 CPU 中只有一…

AI Agent-基础认知与架构解析

定义 AI Agent 可以理解为一种具备感知、决策和行动能力的智能实体&#xff0c;能够在复杂的环境中自主运行&#xff0c;并根据环境变化动态调整自身行为&#xff0c;以实现特定目标。与传统的人工智能程序相比&#xff0c;AI Agent 具有更强的自主性、交互性和适应性。它不仅能…

C语言--字符函数

C语言--字符函数 一、字符函数1.1 iscntrl1.2 isspace1.3 isdigit1.4 isxdigit1.5 islower1.6 isupper1.7 isalpha1.8 isalnum1.9 ispunct1.10 isgraph1.11 isprint 在编程的过程中&#xff0c;我们会经常处理字符&#xff0c;为了方便操作&#xff0c;C语言标准库中提供了一系…

菜鸟之路Day30一一MySQL之DMLDQL

菜鸟之路Day30一一MySQL之DML&DQL 作者&#xff1a;blue 时间&#xff1a;2025.5.8 文章目录 菜鸟之路Day30一一MySQL之DML&DQL一.DML0.概述1.插入语句&#xff08;insert&#xff09;2.更新语句&#xff08;update&#xff09;3.删除语句&#xff08;delete&#xf…

基 LabVIEW 的多轴电机控制系统

在工业自动化蓬勃发展的当下&#xff0c;多轴伺服电机控制系统的重要性与日俱增&#xff0c;广泛应用于众多领域。下面围绕基于 LabVIEW 开发的多轴伺服电机控制系统展开&#xff0c;详细阐述其应用情况。 一、应用领域与场景 在 3D 打印领域&#xff0c;该系统精确操控打印头…

《Go小技巧易错点100例》第三十二篇

本期分享&#xff1a; 1.sync.Map的原理和使用方式 2.实现有序的Map sync.Map的原理和使用方式 sync.Map的底层结构是通过读写分离和无锁读设计实现高并发安全&#xff1a; 1&#xff09;双存储结构&#xff1a; 包含原子化的 read&#xff08;只读缓存&#xff0c;无锁快…

需求分析阶段测试工程师主要做哪些事情

在软件测试需求分析阶段&#xff0c;主要围绕确定测试范围、明确测试目标、细化测试内容等方面开展工作&#xff0c;为后续测试计划的制定、测试用例的设计以及测试执行提供清晰、准确的依据。以下是该阶段具体要做的事情&#xff1a; 1. 需求收集与整理 收集需求文档&#x…

项目模拟实现消息队列第二天

消息应答的模式 1.自动应答: 消费者把这个消息取走了&#xff0c;就算是应答了&#xff08;相当于没有应答) 2.手动应答: basicAck方法属于手动应答(消费者需要主动调用这个api进行应答) 小结 1.需要实现生产者,broker server&#xff0c;消费者这三个部分的 2.针对生产者和消费…

5.Redission

5.1 前文锁问题 基于 setnx 实现的分布式锁存在下面的问题&#xff1a; 重入问题&#xff1a;重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中&#xff0c;可重入锁的意义在于防止死锁&#xff0c;比如 HashTable 这样的代码中&#xff0c;他的方法都是使用 sync…

dify 部署后docker 配置文件修改

1&#xff1a;修改 复制 ./dify/docker/.env.example ./dify/docker/.env 添加一下内容 # 启用自定义模型 CUSTOM_MODEL_ENABLEDtrue# 将OLLAMA_API_BASE_URL 改为宿主机的物理ip OLLAMA_API_BASE_URLhttp://192.168.72.8:11434# vllm 的 OPENAI的兼容 API 地址 CUSTOM_MODE…

数据结构——排序(万字解说)初阶数据结构完

目录 1.排序 2.实现常见的排序算法 2.1 直接插入排序 ​编辑 2.2 希尔排序 2.3 直接选择排序 2.4 堆排序 2.5 冒泡排序 2.6 快速排序 2.6.1 递归版本 2.6.1.1 hoare版本 2.6.1.2 挖坑法 2.6.1.3 lomuto前后指针 2.6.1.4 时间复杂度 2.6.2 非递归版本 2.7 归并排序…

快速入门深度学习系列(3)----神经网络

本文只针对图进行解释重要内容 这就是入门所需要掌握的大部分内容 对于不懂的名词或概念 你可以及时去查 对于层数 标在上面 对于该层的第几个元素 标在下面 输入层算作第0层 对于第一层的w b 参数 维度如下w:4*3 b:4*1 这个叫做神经元 比如对于第一层的神经元 这里说的很…

在线工具源码_字典查询_汉语词典_成语查询_择吉黄历等255个工具数百万数据 养站神器,安装教程

在线工具源码_字典查询_汉语词典_成语查询_择吉黄历等255个工具数百万数据 养站神器&#xff0c;安装教程 资源宝分享&#xff1a;https://www.httple.net/154301.html 一次性打包涵盖200个常用工具&#xff01;无论是日常的图片处理、文件格式转换&#xff0c;还是实用的时间…

Linux 阻塞和非阻塞 I/O 简明指南

目录 声明 1. 阻塞和非阻塞简介 2. 等待队列 2.1 等待队列头 2.2 等待队列项 2.3 将队列项添加/移除等待队列头 2.4 等待唤醒 2.5 等待事件 3. 轮询 3.1 select函数 3.2 poll函数 3.3 epoll函数 4. Linux 驱动下的 poll 操作函数 声明 本博客所记录的关于正点原子…

Java开发经验——阿里巴巴编码规范经验总结2

摘要 这篇文章是关于Java开发中阿里巴巴编码规范的经验总结。它强调了避免使用Apache BeanUtils进行属性复制&#xff0c;因为它效率低下且类型转换不安全。推荐使用Spring BeanUtils、Hutool BeanUtil、MapStruct或手动赋值等替代方案。文章还指出不应在视图模板中加入复杂逻…

机器人手臂“听不懂“指令?Ethercat转PROFINET网关妙解通信僵局

机器人手臂"听不懂"指令&#xff1f;Ethercat转PROFINET网关妙解产线通信僵局 协作机器人&#xff08;如KUKA iiWA&#xff09;使用EtherCAT控制&#xff0c;与Profinet主站&#xff08;如西门子840D CNC&#xff09;同步动作。 客户反馈&#xff1a;基于Profinet…