【JavaEE初阶】多线程(二)线程状态以及多线程安全问题

news2024/11/25 15:43:49

摄影分享~~
在这里插入图片描述
在这里插入图片描述

文章目录

  • 线程的状态
  • 多线程带来的风险
    • 线程安全
    • 线程安全的原因
    • 解决线程不安全问题(加锁)
    • synchronized关键字-监视器锁monitor lock
      • synchronized的特性
    • java中的死锁问题
      • 死锁
      • 死锁的三个典型情况
      • 死锁的四个必要条件
      • 如何避免死锁?
    • Java 标准库中的线程安全类

线程的状态

状态是针对当前的线程调度的情况进行描述的。
线程是调度的基本单位,状态是线程的属性。

  1. NEW:创建了Thread对象,但是还没调用start(内核中还没有创建PCB)
  2. TERMINATED:表示内核中的pcb已经执行完毕了,但是Thread对象还在。
  3. RUNNABLE:可运行的(包括正在CPU上执行的和在就绪队列中随时可以去CPU上执行的)
  4. WAITING
  5. TIMED_WAITING
  6. BLOCKED

4~6三个状态都是阻塞状态。(都是表示线程PCB正在阻塞队列中)只不过是不同原因的阻塞。
在这里插入图片描述

TERMINATED状态中,内核中线程的PCB被释放了,此时代码中的t对象也就没用了。Java中对象的生命周期自有其规则,这个生命周期和系统内核中的线程并非完全一致,**内核的线程释放的时候,无法保证Java代码中的t对象也立即释放。**此时t对象标识为:无效。虽然t对象无效了,但是t对象依旧可以完成调用函数等操作。
一个线程只能start一次。

public class ThreadDemo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 100; i++) {
                for (int j = 0; j < 1000_0000; j++) {
                    int a = 10;
                    a += 10;
                }
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        //未start之前是new状态
        System.out.println("start之前:"+t.getState());
        t.start();
        //t执行中的状态runable
        for (int i = 0; i < 1000; i++) {
            System.out.println("t 执行中的状态: " + t.getState());
        }
        t.join();
        // 线程执行完毕之后, 就是 TERMINATED 状态
        System.out.println("t 结束之后: " + t.getState());
    }
}

在这里插入图片描述
在这里插入图片描述

多线程的意义:
多线程可以更充分利用多核心的CPU资源,从而加快程序的运行效率。

多线程带来的风险

线程安全

线程安全的问题的根本原因就是抢占式执行,带来的随机性。
我们来看一个例子:

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

public class ThreadDemo12 {
    public static void main(String[] args) {
        Counter counter = new Counter();

        // 搞两个线程, 两个线程分别针对 counter 来 调用 5w 次的 add 方法
        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) {
            e.printStackTrace();
        }

        // 打印最终的 count 值
        System.out.println("count = " + counter.count);
    }
}

以上代码我们预期结果是100000次,但是运行结果如下:
在这里插入图片描述
为什么出现这个bug呢?
count++;本质上在操作系统中分成三 步:

  1. 先把内存中的值,读取到CPU的寄存器上(load)
  2. 把CPU寄存器中的值进行+1操作。(add)
  3. 把读到的结果写到内存中。(save)

当两个线程并发执行count++时,就相当于load add save同时执行。此时就会产生结果上的差异。
可能执行的方式:
在这里插入图片描述
箭头是时间轴,靠上就是先执行,靠下就是后执行。
由于线程之间是随机调度的,导致此处的调度顺序充满其他可能性。
在这里插入图片描述
在这里插入图片描述

线程安全的原因

  1. 【根本原因】抢占式执行,随机调度
  2. 代码结构:多个线程同时修改一个变量。
  3. 原子性:如果修改操作是原子的,影响不是很大。但是如果是非原子的,出现问题的概率就会增加很多。
  4. 内存可见性问题
  5. 指令重排序(本质上是编译器优化出现了bug):单个线程里,顺序发生调整。

要想解决线程安全问题,主要手段就是从原子性入手,把非原子的操作,变成原子的。加锁。

解决线程不安全问题(加锁)

上面我们说到。通过加锁,我们可以把不是原子的,转成原子的。

在这里插入图片描述
加了synchronized之后,进入add方法就会加锁,出了add方法就会解锁。
如果两个线程同时尝试加锁,此时只有一个能获取锁成功,另一个只能阻塞等待(BLOCK)。一直阻塞到刚才的线程释放锁(解锁),当前线程才能加锁成功。

在这里插入图片描述
加锁,说是保证原子性,但并不是说让这里的三个操作一次性完成,也不是这三步操作过程中不进行调度,而是让其他也想操作的线程阻塞等待。加锁本质上是把并发变成了并行。
加锁操作会影响程序的速率,在实际过程中我们要通过实际情景来对其进行合理加锁。

synchronized使用方法:

  1. 修饰方法
    (1)修饰普通方法(锁对象是this)
    (2)修饰静态方法(锁对象是类对象(Counter.class))
  2. 修饰代码块(显示/手动指定锁对象)

加锁,要明确执行对哪个对象进行加锁的。
如果两个线程针对同一个对象加锁,会产生阻塞等待。(锁竞争/锁冲突)
如果两个对象针对不同对象加锁,不会参数阻塞等待。(不会锁竞争/锁冲突)
一定要注意锁对象是哪个!

在这里插入图片描述

synchronized关键字-监视器锁monitor lock

监视器锁也就是synchronized。有时会在异常中看到这个词。

synchronized的特性

  1. 互斥
    synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
    同一个对象 synchronized 就会阻塞等待.
  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

理解 “阻塞等待”: 针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝 试进行加锁,就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的 线程, 再来获取到这个锁。
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这
也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

  1. 可重入
    synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
    也就是说,一个线程针对同一个对象,连续加锁两次,是否会有问题。如果没问题就叫做可重入,如果有问题就叫做不可重入的。
    在这里插入图片描述
    上述代码中,锁对象是this,只要有线程调用add,进入add方法的时候,就会先加锁。进入add方法时,又遇到了代码块,再次尝试加锁。站在this的角度看(锁对象),自己已经被另外的线程占用了,第二次的加锁是否要阻塞等待呢?
    如果允许上述操作,这个锁就是可重入的。如果不允许,就是不可重入的。就会产生死锁

java中的死锁问题

死锁

程序中一旦出现死锁,就会导致线程崩溃了(无法继续执行后续工作)。程序就会产生严重的bug。死锁一般是非常隐蔽的。

死锁的三个典型情况

  1. 一个线程,一把锁,连续加锁两次。如果锁是不可重入锁,就会死锁
    java中synchronized和ReentrantLock都是可重入锁。
  2. 两个线程两把锁,t1和t2各自先针对锁A和锁B加锁。再尝试获取对方的锁。
    举个例子:某人把家里钥匙锁在了车里,把车钥匙锁在了家里;小红写完了英语作业,想要抄小兰的数学作业,小兰写完了数学作业,想要抄小红的英语作业。但是两人都不开口。这个场景就僵住了~
public class ThreadDemo13 {
    public static void main(String[] args) {
        Object yingyu = new Object();
        Object shuxue = new Object();

        Thread xiaohong = new Thread(() -> {
            synchronized (yingyu) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (shuxue) {
                    System.out.println("小红抄到了小兰的数学作业!小红写完了所有作业。");
                }
            }
        });
        Thread xiaolan = new Thread(() -> {
            synchronized (shuxue) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (yingyu) {
                    System.out.println("小兰抄到了小红的英语作业!小兰写完了所有作业。");
                }
            }
        });
        xiaohong.start();
        xiaolan.start();
    }
}

在这里插入图片描述
通过运行结果可知,此时没有线程拿到两把锁。
在这里插入图片描述
BLOCK表示获取锁,获取不到阻塞状态。
java:16行
在这里插入图片描述
在这里插入图片描述
java:32
在这里插入图片描述
针对这样的死锁问题,需要借助jconsole这样的工作来进行定位,看线程的状态和调用栈。就可以分析代码再哪里死锁了。

  1. 多个线程多把锁。
    经典案例:哲学家就餐问题
    在这里插入图片描述
    每个哲学家有两种状态:
    1.思考人生(相当于线程阻塞状态)
    2.拿起筷子吃面条(相当于线程获取到所然后执行一些计算的状态)
    由于系统的随机操作,这五个哲学家,随时都可能想吃面条,也随时都有可能要思考人生。
    想要吃面条就需要拿起左手和右手两根筷子。
    假设出现了极端情况就会死锁,即同一时刻,所有哲学家同时拿起左手的筷子,都要等待右边的哲学家把筷子放下。
    那么如何解决这个问题呢?
    编号(给筷子编号)。先拿编号小的,再拿编号大的。
    在这里插入图片描述
    这样一来,A先拿1,B先拿2,C先拿3,D先拿4,E先拿1。此时E就必须等待A使用完1,再拿起5吃面。E吃完D使用5和4吃…
    这样就解决了死锁问题。
public class ThreadDemo13 {
    public static void main(String[] args) {
        Object yingyu = new Object();
        Object shuxue = new Object();

        Thread xiaohong = new Thread(() -> {
            // 假设 yingyu 是 1 号, shuxue 是 2 号, 约定先拿小的, 后拿大的.
            synchronized (yingyu) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (shuxue) {
                    System.out.println("小红抄到了小兰的数学作业!小红写完了所有作业。");
                }
            }
        });
        Thread xiaolan = new Thread(() -> {
            synchronized (yingyu) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (shuxue) {
                    System.out.println("小兰抄到了小红的英语作业!小兰写完了所有作业。");
                }
            }
        });
        xiaohong.start();
        xiaolan.start();
    }
}

在这里插入图片描述

死锁的四个必要条件

  1. 互斥使用:线程1拿到了锁,线程2就等待。
  2. 不可抢占:线程1拿到锁之后,必须是线程1主动释放,线程2不能强制获取。
  3. 请求和保持:线程1拿到锁A之后,再尝试获取锁B,A这把锁还是保持的。
  4. 循环等待:线程1尝试获取到锁A和锁B 线程2尝试获取到锁B和锁A;线程1在获取B的时候等待线程2释放B,同时线程2在获取锁A的时候等待线程1释放锁A;

如何避免死锁?

如何避免死锁?(以循环等待为突破口)
方法:给锁编号,然后指定一个固定的顺序(从小到大)来加锁,任意线程加多把锁的时候,都让线程遵守上述顺序,此时循环等待自然破除。这是解决死锁最简单可靠的办法。

Java 标准库中的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

但是还有一些是线程安全的. 使用了一些锁机制来控制.

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer

还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的

  • String

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

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

相关文章

家用洗地机怎么选?2023高性价比家用洗地机推荐

相信大家和我一样是妥妥的“懒人一族”了&#xff0c;不喜欢做家务、不喜欢碰脏水、不喜欢花费过多的时间在家务上&#xff0c;但是却想有一个整洁干净的家居环境。而作为家务清洁中面积最大、耗时耗力最多的就是扫地拖地了。传统的地面清洁方式&#xff0c;要先用扫把扫一遍&a…

Git上传本地代码到Github

参考 https://zhuanlan.zhihu.com/p/138305054 文章目录 上传本地代码到github仓库&#xff08;1&#xff09;创建一个空文件夹&#xff0c;在该文件夹下右键&#xff0c;Git bash here&#xff08;2&#xff09;初始化仓库&#xff08;3&#xff09; 建立本地与github上项目的…

ChatGPT 速通手册——让 ChatGPT 来写正则表达式

regex 生成 正则表达式可谓是一门让广大程序员们又爱又恨的技术。它易学难精&#xff0c;而且可维护性又差&#xff0c;别说交接给其他同事&#xff0c;同一个人写的正则表达式&#xff0c;三个月后回头再看&#xff0c;也可能完全不知所云。 因此&#xff0c;让 ChatGPT 来写…

【计网 从头自己构建协议】一、libpcap 介绍 手撕以太网帧

上一篇&#xff1a;IndexError: list index out of range 下一篇&#xff1a;[【计网 从头自己构建协议】二、收发 ARP 请求帧与响应帧] 介绍 理论的学习总是枯燥的&#xff0c;想要加深对理论的理解&#xff0c;最好的方法就是自己实践一遍。 想要亲手实现各种协议&#xf…

通讯方式连接成功,其他原因导致的连接失败解决方案

一、电脑中有其他品牌visa导致的冲突&#xff08;以tekvisa为例&#xff09; 1、删除tekvisa 2、下载一个NI Package Manager&#xff0c;卸载里面所有NI的东西 &#xff08;https://www.ni.com/zh-cn/support/downloads/software-products/download.package-manager.html#32…

❀五一劳动节来啦❀

今年“五一”&#xff0c;4月29日至5月3日放假调休&#xff0c;共5天。 如果你在5月4日到5月6日请假3天&#xff0c;加上5月7日周日&#xff0c;就可以形成9天的假期。 一&#xff0c;五一劳动节的由来⭐ 国际劳动节又称“五一国际劳动节”“国际示威游行日”&#xff08;英语…

人生中最好的等待叫做来日可期,社科院与杜兰大学金融管理硕士等你惊艳岁月

有句话说&#xff1a;“去日不可追&#xff0c;来日犹可期”。过去的已经过去&#xff0c;不管好的、坏的都已成为我们的回忆。人生中最好的等待就是未来可期。别辜负现在的好时光&#xff0c;努力做想做的事。社科院与杜兰大学金融管理硕士项目等你惊艳时光。 所有出众者的背…

【计算机网络:自顶向下方法】(三) 运输层 (TCP | UDP | 复用 | 传输原理rdt)

【计算机网络&#xff1a;自顶向下方法】 3.1 概述 传输层协议是在端系统中实现的传输层将发送的应用程序进程接受到的报文转换成传输层分组 (运输层报文段)实现的方法/过程 &#xff1a; 将应用报文划分为较小的块&#xff0c;并为每块加上传输层首部以生成传输层报文段ff。I…

【C++】类和对象(中篇)—— 默认成员函数,const成员函数,运算符重载

前言 类和对象没有技巧&#xff0c;只有多加练习&#xff0c;多多尝试自己完成代码&#xff0c;例如各种运算符的重载&#xff0c;或是实现一个自己的日期类 目录 一、类的六个默认成员函数 二、构造函数 2.1 概念 2.2 特点 2.3 默认无参的构造函数 三、析构函数 3.1 概…

ORB305与CISCO路由器构建L2TP over IPSec VPN操作手册

1、网络拓扑在思科路由器与ORB305之间建立一个安全隧道&#xff0c;对客户路由器端设备子网&#xff0c;与思科路由器端服务器子网之间的数据流进行安全保护&#xff0c;组网拓扑图如图所示。 2、思科路由器端配置指导(此处以多数客户使用专线上网形式为例)Cisco&#xff08;AR…

90年三本程序员,8年5跳,年薪4万变92万……

很多时候&#xff0c;虽然跳槽可能带来降薪的结果&#xff0c;但依然有很多人认为跳槽可以涨薪。近日&#xff0c;看到一则帖子。 发帖的楼主表示&#xff0c;自己8年5跳&#xff0c;年薪4万到92万&#xff0c;现在环沪上海各一套房&#xff0c;再干5年码农&#xff0c;就可以…

2022年NOC大赛创客智慧编程赛道图形化scratch初赛题,包含答案解析

目录 一、单选题 二、多选题 三、判断题 下载打印文档做题: 一、单选题

项目干系人是什么?如何有效管理项目干系人?

项目干系人是指对项目具有利益关系或影响力的个人、团体或组织。他们可能会对项目的目标、范围、进度、成本、质量等方面产生影响&#xff0c;因此&#xff0c;有效地管理项目干系人是项目管理成功的关键之一。 一、干系人识别和分类 项目经理应该首先识别和分类所有与项目有关…

STM32模数转换器(ADC)

1.ADC的简要 我们首先说一下ADC的转换过程&#xff0c;然后说一下原理&#xff0c;当然如果嫌啰嗦可以直接跳过。 ADC是英文Analog-to-Digital Converter缩写&#xff0c;翻译过来就是模数转换器&#xff0c;是指将连续变化的模拟信号转换为离散的数字信号的器件。A/D转换的作…

DiffusionDet源码阅读(1)

本文仅仅适用于已经通读过全文的小伙伴 本文代码节选自 mmdet 中的 DiffusionDet 代码&#xff0c;目前该代码还处于 Development 阶段&#xff0c;所以我博客里写的代码和之后的稳定版本可能稍有不同&#xff0c;不过不用担心&#xff0c;我们只看最关键的部分 DDPM中扩散部…

mybatis中大数据量foreach插入效率对比

1.controller代码 RequestMapping("/testInsert")public String testInsert(Integer sum){testService.testInsert(sum);return "发送成功";}2.service代码 Overridepublic void testInsert(Integer sum) {long start System.currentTimeMillis();List<…

LightGBM面试题

1.偏差 vs 方差? 偏差是指由有所采样得到的大小为m的训练数据集&#xff0c;训练出的所有模型的输出的平均值和真实模型输出之间的偏差。 通常是由对学习算法做了错误的假设导致的描述模型输出结果的期望与样本真实结果的差距。分类器表达能力有限导致的系统性错误&#xff0c…

基于AT89C52单片机的温度检测报警设计

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/87708680?spm1001.2014.3001.5503 源码获取 主要内容&#xff1a; 本系统的设计主要是了解了单片机微型计算机&#xff0c;根据现实生活的需要以及已掌握的理论知识…

Linux应用程序开发:静态库与动态库的制作及使用

目录 一、库的简介二、静态库与动态库的简介三、静态库制作与调用案例四、动态库制作与调用案例 一、库的简介 库是一种可执行的二进制文件&#xff0c;是编译好的代码。使用库可以提高开发效率。而Linux库的种类可分为动态库和静态库。 二、静态库与动态库的简介 1、静态库&a…

第十七届中国CFO大会圆满举办 用友蝉联中国CFO首选智能财务厂商!

4月21日&#xff0c;由财政部指导、《新理财》杂志社主办、用友等单位协办的「数字财务 智能引领」第十七届中国CFO大会在北京圆满举办&#xff01;业内专家、权威学者以及众多来自央国企等知名大型企业的财务领路人荟聚一堂&#xff0c;共襄中国CFO领域的顶尖盛会&#xff0c;…