JavaEE_多线程(二)

news2025/3/13 2:55:53

目录

  • 1. 线程的状态
  • 2. 线程安全
    • 2.1 线程不安全问题的原因
  • 3. 线程安全中的部分概念
    • 3.1 原子性
    • 3.2 可见性
    • 3.3 指令重排序
  • 4. 解决线程安全问题
    • 4.1 synchronized关键字
      • 4.1.1 可重入
      • 4.1.2 synchronized使用
    • 4.2 volatile关键字
      • 4.2.1 volatile使用
  • 5. wait和notify
    • 5.1 wait()方法
    • 5.2 notify()方法


1. 线程的状态

  1. NEW: Thread对象已经有了.但是内核里的PCB还没有(还没有调用start方法)
  2. TERMINATED: 内核PCB没了,线程结束了,Thread对象还在
  3. RUNNABLE: 就绪状态(线程正在CPU上运行,或者线程正在排队)
  4. WAITING: 由于wait这种不固定时间的方式产生的阻塞
  5. TIMED_WAITING: sleep 触发的线程阻塞
  6. BLOCKED: synchronized 触发的线程阻塞

2. 线程安全

一个代码,在多线程环境下执行不出bug就可以视为线程安全,反之,一个代码在单线程下执行的效果与多线程下执行的效果不一样,就可以视为线程不安全

我们先举一个线程不安全的例子,来直观的观察线程不安全

    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();
        // 等待两个线程全部执行完毕

        // 我们预期的结果是10W,但是实际上的结果一般是小于10W的
        // 并且每次执行的结果都不一样
        System.out.println("count:"+count);

    }

2.1 线程不安全问题的原因

接下来我们介绍一下线程不安全的原因

  1. 罪魁祸首是在操作系统中线程是抢占式执行的,随机调度的
  2. 多个线程同时针对一个变量进行修改
  3. 修改操作,不是原子的
  4. 内存可见性
  5. 指令重排序

3. 线程安全中的部分概念

3.1 原子性

什么是原子性
可以简单理解为一段代码要么全部执行,要么全部不执行
可以把一段代码想象成一个房间,每个线程都是一个想进房间的人,如果没有任何的保护机制,当A进入房间后,B也可以进入房间,此时就会破坏A的隐私了

那么不保证原子性会产生什么问题呢?
一条Java语句不一定是原子的,也不一定只是一条指令
比如一个简单的++操作
count++这个操作,站在CPU的角度上,是通过三个指令来完成的

  1. load: 把数据从内存读取到cpu寄存器中
  2. add: 把寄存器中的数据进行+1
  3. save: 把寄存器中的数据保存到内存中
    上述的代码在针对count进行修改的时候,单线程下并不会产生问题,但是在多线程下,两个线程的指令并不都是保持原子性执行的,这才导致了与预期不符的结果

3.2 可见性

可见性是指一个线程对共享变量值的修改,能够及时地被其他线程看到

主内存是指硬件角度的内存,工作内存则是指cpu寄存器和高速缓存
共享变量存在于主内存中, 每一个线程都有自己的工作内存,当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据,当线程要修改一个共享变量的时候,也会先修改工作内存的副本,再同步回主内存

当线程1修改了在它的工作内存中修改了共享变量a的值后,线程2的工作内存中a的值不一定会及时发生变化(因为主内存不一定能及时同步)这个时候代码就容易出现问题

3.3 指令重排序

本质上也是编译器的优化出现了问题,在单线程模式下,JVM和CPU指令集会进行优化,编译器对于指令重排序的前提是"保证逻辑不出现问题",这一点在单线程下是容易实现判断的,但是在多线程下就容易出现问题了,编译器很难在编译阶段对代码执行效果进行预测,因此指令重排序后很容易逻辑和之前不对等

4. 解决线程安全问题

4.1 synchronized关键字

synchronized一般要搭配{}代码块使用
当某个线程执行到某对象的synchronized时,被synchronized修饰的代码块就相当于被加锁了,此时其他线程也执行到同一个的synchronized就会产生阻塞等待,只有等上一个对象退出synchronized的时候,才会解锁

synchronized (锁对象) {
}

锁对象是谁并不重要,重要的是通过这个对象来区分两个线程是否竞争同一个锁,如果两个线程针对同一个对象进行加锁,就会产生锁竞争,一旦产生竞争,一个线程能拿到锁继续执行代码,一个线程拿不到锁,就只能阻塞等待,等前一个线程释放锁之后,他才有机会拿到锁
如果不是针对同一个对象进行加锁,就不会产生锁竞争

4.1.1 可重入

这里引入一个概念–死锁,顾名思义,就是两个或多个进程互相等待,谁也解不开锁
死锁的成因,涉及到四个必要条件

  1. 互斥使用(锁的基本特性): 当一个线程持有一把锁之后,另一个线程也想要获取到锁,就要阻塞等待
  2. 不可抢占(锁的基本特性): 当锁已经被线程1拿到之后,线程2只能等待线程1主动释放,不能强行抢过来
  3. 请求保持(代码结构): 一个线程尝试获取多把锁,先拿到锁1之后,在尝试获取锁2,获取锁2的时候锁1不会释放
  4. 循环等待(代码结构): 等待的依赖关系形成环了
    避免死锁的核心就是破除上述任意一个必要条件

但是synchronized是可重入锁,不会出现自己把自己锁死的情况
在可重入锁的内部,包含 线程持有者 和 计数器 两个信息
如果某个线程加锁的时候,发现锁已经被人占用了,而且恰好占用的正是自己,那么仍然可以继续获取到锁,并且计数器会加一
进一步的,无论锁有多少层,都是要在最外层才能释放锁.锁对象中,不光要记录谁拿到了锁,还要记录锁被加了几次,每加一次锁,计数器就+1,每解锁一次,计数器就-1,当出了最后一个大括号{},计数器恰好减成零,此时才会真正释放锁(才能被别的线程获取到)

常见死锁情况(不可重入锁)

  1. 一个线程,一把锁,连续加锁两次,就会死锁
  2. 两个线程,两把锁,线程1获取锁A,线程2获取锁B,此时1再尝试获取B,2再尝试获取A
  3. N个线程,M把锁,一种典型的情况,哲学家就餐问题

4.1.2 synchronized使用

有了加锁.就可以把一组不是原子的操作,变成"原子操作"

class Counter {

    synchronized public void increase() {
        // 1. 修饰普通方法:锁的Counter对象
    }

    public void increase2() {
        synchronized (this) {
            // 2. 明确指定锁那个对象:锁当前对象
        }
    }
    synchronized public static void increase3() {
        // 3. 修饰静态方法: 锁的Counter类对象
    }

    public static void increase4() {
        synchronized (Counter.class) {
            // 4. 明确指定锁那个对象: 锁类对象
        }
    }
}

我们要重点理解的是,锁的是哪个对象,两个线程竞争锁了同一个对象的锁才会出现竞争

4.2 volatile关键字

volatile关键字修饰的变量能保证内存可见性,禁止指令重排序
在这里插入图片描述
代码在写入volatile修饰的变量的时候:
改变线程工作内存中volatile变量副本的值,将修改后的副本的值从工作内存刷新到主内存

代码在读取volatile修饰的变量的时候:
从主内存中读取volatile变量的最新值到线程的工作内存中,从工作内存中读取volatile变量的副本
结合上面的图就可以看出,每次读取值的时候,都是最新的准确的变量的值,volatile保证了内存可见性,不过volatile强制读取内存,会比直接访问工作内存要慢很多,为了数据的准确性而牺牲了速度

4.2.1 volatile使用

    public static int isQuit = 0;
    //public static volatile int isQuit = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (isQuit == 0) {
                // do nothing
            }
            System.out.println("线程1结束");
        });

        t1.start();

        Thread t2 = new Thread(() -> {
           Scanner scanner = new Scanner(System.in);
           isQuit = scanner.nextInt();
        });
        t2.start();
        // 当我们输入0的时候,线程并不会结束,谪显然是一个bug
    }

这个例子就是典型的内存可见性问题,那为什么会产生内存可见性呢?

  1. load读取内存中isQuit的值到寄存器中
  2. 通过cmp指令比较寄存器的值是否是0,来决定是否该继续循环,但是由于while循环的非常快,短时间内就会进行大量load和cmp操作
  3. 此时编译器/JVM就发现,虽然进行了很多次的load操作.但是每次load操作的结果都是一样的,并且load操作又是比较费时间的,一次load操作花的时间相当于上万次的cmp了
  4. 所以编译器做了一个大胆的决定,只有第一次循环的时候,才读了内存,后续就不在读内存了,而是直接从寄存器中取出isQuit的值
    我们只需要给isQuit加上volatile关键字,就能解决这个问题了

5. wait和notify

由于线程是抢占式执行的,无法保证线程执行的先后顺序,但是在实际开发过程中,我们需要合理协调多个线程的执行先后顺序

5.1 wait()方法

wait要做的事情:

  1. 释放当前的锁(释放锁的前提是先加上锁),把线程放到等待队列中
  2. 让进程进入阻塞
  3. 当线程被唤醒的时候,重新获取锁

wait要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常
wait结束等待的条件:

  1. 其他线程调用该对象的notify方法
  2. wait等待时间超时(wait方法提供一个带有timeout参数的版本,来制定等待时间)
  3. 其他线程调用该等待线程的interrupt方法,导致wait抛出异常
Object object = new Object();
        synchronized (object) {
            System.out.println("wait之前");
            // 把wait要放到synchronized里面来调用,保证确实是拿到了锁
            object.wait();
            // 这里wait之后就会一直等待下去,这个时候就是用到了另一个唤醒方法notify
            System.out.println("wait之后");
        }

5.2 notify()方法

notify方法要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,唤醒等待的线程并使他们重新获取该对象的对象锁
如果有多个线程等待,则有线程调度器随机挑选出来呈wait状态的线程(并没有先来后到的规矩)
在notify方法后,当前线程不会马上释放该对象锁,要等到执行notify方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁

    public static void main(String[] args) {
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (object) {
                System.out.println("执行之前");
                try {
                    object.wait();
                    //1. 释放当前的锁
                    //2. 让线程进入阻塞
                    //3. 当线程被唤醒的时候,重新获取锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("执行之后");
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (object) {
                System.out.println("进行通知");
                // 进行通知后,才会打印线程1中的 "执行之后"
                object.notify();
            }
        });
        t1.start();
        t2.start();
    }

notify只是唤醒某一个在等待的线程.此外还有notifyAll方法,可以一次性唤醒所有等待的线程. 虽然是同时唤醒多个线程,但是多个线程之间还是需要竞争锁,所以并不是同时执行,而是仍有先后的执行

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

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

相关文章

【unity小技巧】分享vscode如何进行unity开发,且如何开启unity断点调试模式,并进行unity断点调试(2025年最新的方法,实测有效)

文章目录 前言一、前置条件1、已安装Visual Studio Code&#xff0c;并且unity首选项>外部工具>外部脚本编辑器选择为Visual Studio Code [版本号]&#xff0c;2、在Visual Studio Code扩展中搜索Unity&#xff0c;并安装3、同时注意这个插件下面的描述&#xff0c;需要根…

【Hadoop】详解HDFS

Hadoop 分布式文件系统(HDFS)被设计成适合运行在通用硬件上的分布式文件系统&#xff0c;它是一个高度容错性的系统&#xff0c;适合部署在廉价的机器上&#xff0c;能够提供高吞吐量的数据访问&#xff0c;非常适合大规模数据集上的应用。为了做到可靠性&#xff0c;HDFS创建了…

Spring(4)——响应相关

一、返回静态页面 1.1**RestController和Controller** 想返回如下页面&#xff1a; 如果我们依旧使用原来的**RestController** 可以看到的是仅仅返回了字符串。 此时将**RestController改为Controller** 可以看到这次返回的是html页面。 那么**RestController和Controller…

axure11安装教程包含下载、安装、汉化、授权(附安装包)图文详细教程

文章目录 前言一、axure11安装包下载二、axure11安装教程1.启动安装程序2.安装向导界面3.安装协议协议页面2.选择安装位置3.开始安装4.完成安装 三、axure11汉化教程1.axure11汉化包2.axure11汉化设置 四、axure11授权教程1.打开axure112.设置使用方式3.输入许可证号4.axure11安…

Redis-缓存穿透击穿雪崩

1. 穿透问题 缓存穿透问题就是查询不存在的数据。在缓存穿透中&#xff0c;先查缓存&#xff0c;缓存没有数据&#xff0c;就会请求到数据库上&#xff0c;导致数据库压力剧增。 解决方法&#xff1a; 给不存在的key加上空值&#xff0c;防止每次都会请求到数据库。布隆过滤器…

Windows server网络安全

摘要 安全策略 IP安全策略&#xff0c;简单的来说就是可以通过做相应的策略来达到放行、阻止相关的端口&#xff1b;放行、阻止相关的IP&#xff0c;如何做安全策略&#xff0c;小编为大家详细的写了相关的步骤&#xff1a; 解说步骤&#xff1a; 阻止所有&#xff1a; 打…

Python从入门到精通1:FastAPI

引言 在现代 Web 开发中&#xff0c;API 是前后端分离架构的核心。FastAPI 凭借其高性能、简洁的语法和自动文档生成功能&#xff0c;成为 Python 开发者的首选框架。本文将从零开始&#xff0c;详细讲解 FastAPI 的核心概念、安装配置、路由设计、请求处理以及实际应用案例&a…

Leetcode做题记录----2

1、两数之和 思路&#xff1a; 1、不能使用相同元素&#xff0c;可以想到哈希表&#xff0c;&#xff0c;C#中可以通过字典建立当前值和下标的关系 2、显然&#xff0c;依次判断数组中的每个数即可 3、定义other target - num[ i ] 这个other就是我们用于在字典中进行寻找…

批量合并 Word 文档,支持合并成一个 Word,也支持按文件夹合并

我们经常会碰到需要将多个 Word 文档批量合并成一个 Word 文档的场景&#xff0c;比如需要合并后打印、合并后方便整理存档等等。如果是人工的操作&#xff0c;会非常的麻烦。因此我们通常会借助一些批量处理脚本或者寻找批量处理的工具来帮我们实现批量合并 Word 文档的操作。…

项目实操分享:一个基于 Flask 的音乐生成系统,能够根据用户指定的参数自动生成 MIDI 音乐并转换为音频文件

在线体验音乐创作&#xff1a;AI Music Creator - AI Music Creator 体验者账号密码admin/admin123 系统架构 1.1 核心组件 MusicGenerator 类 负责音乐生成的核心逻辑 包含 MIDI 生成和音频转换功能 管理音乐参数和音轨生成 FluidSynth 集成 用于 MIDI 到音频的转换 …

神经网络为什么要用 ReLU 增加非线性?

在神经网络中使用 ReLU&#xff08;Rectified Linear Unit&#xff09; 作为激活函数的主要目的是引入非线性&#xff0c;这是神经网络能够学习复杂模式和解决非线性问题的关键。 1. 为什么需要非线性&#xff1f; 1.1 线性模型的局限性 如果神经网络只使用线性激活函数&…

动态规划详解(二):从暴力递归到动态规划的完整优化之路

目录 一、什么是动态规划&#xff1f;—— 从人类直觉到算法思维 二、暴力递归&#xff1a;最直观的问题分解方式 1. 示例&#xff1a;斐波那契数列 2. 递归树分析&#xff08;以n5为例&#xff09; 3. 问题暴露 三、第一次优化&#xff1a;记忆化搜索&#xff08;Memoiza…

ubuntu下在pycharm中配置已有的虚拟环境

作者使用的ubuntu系统位于PC机上的虚拟机。系统版本为&#xff1a; 在配置pycharm解释器之前你需要先创建虚拟环境以及安装pycharm。 作者创建的虚拟环境位于/home/topeet/miniconda3/envs/airproject/&#xff0c;如下图所示&#xff1a; 作者安装的pycharm版本为2023社区…

爬虫中一些有用的用法

文本和标签在一个级别下 如果文本和a标签在一个级别下 比如&#xff1a; # 获取a标签后的第一个文本节点text_node a.xpath(following-sibling::text()[1])[0].strip() 将xpath的html代码转换成字符串 etree.tostring(root, pretty_printTrue, encoding"utf-8")…

DeepIn Wps 字体缺失问题

系统缺失字体 Symbol 、Wingdings 、Wingdings2、Wingdings3、MT—extra 字体问题 问了下DeepSeek 在应用商店安装或者在windows 里面找 装了一个GB-18030 还是不行 在windows里面复制了缺失的字体 将字体复制到DeepIn 的字体目录&#xff08;Ubuntu 应该也是这个目录&am…

【webrtc debug tools】 rtc_event_log_to_text

一、rtc_event_log 简介 在学习分析webrtc的过程中&#xff0c;发现其内部提供了一个实时数据捕获接口RtcEventLog。通过该接口可以实时捕获进出webrtc的RTP报文头数据、音视频配置参数、webrtc的探测数据等。其内容实现可参考RtcEventLogImpl类的定义。其文件所在路径 loggin…

数字IC后端项目典型问题(2025.03.10数字后端项目问题记录)

小编发现今天广大学员发过来的问题都比较好&#xff0c;立即一顿输出分享给大家&#xff08;每天都有好多种类的数字后端问题&#xff09;。后续可能会经常通过这种方式来做分享。其实很多问题都是实际后端项目中经常遇到的典型问题。希望通过这种方式的分享能够帮助到更多需要…

Redis 持久化详解:RDB 与 AOF 的机制、配置与最佳实践

目录 引言 1. Redis 持久化概述 1.1 为什么需要持久化&#xff1f; 1.2 Redis 持久化的两种方式 2. RDB 持久化 2.1 RDB 的工作原理 RDB 的触发条件 2.2 RDB 的配置 2.3 RDB 的优缺点 优点 缺点 3. AOF 持久化 3.1 AOF 的工作原理 AOF 的触发条件 3.2 AOF 的配置…

说一下spring的事务隔离级别?

大家好&#xff0c;我是锋哥。今天分享关于【说一下spring的事务隔离级别&#xff1f;】面试题。希望对大家有帮助&#xff1b; 说一下spring的事务隔离级别&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Spring的事务隔离级别是指在数据库事务管理中…

Java 大视界 -- 基于 Java 的大数据实时数据处理框架性能评测与选型建议(121)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…