【Java线程】工作三年线程安全活锁、饥饿这么常见还不知道?只知道死锁?

手撕Java系列-线程安全问题,死锁,活锁,解决方案,最佳实践 解决上述问题的关键在于理解多线程编程的基本原理,选择合适的同步机制和技术,以及遵循良好的编码实践。重要的是要注意,即使使用了线程安全的集合,不当的代码逻辑仍然可能导致线程安全问题,比如迭代过程中修改集合。这种情况可能导致不一致的状态,因为最终的结果取决于各个线程执行的确切顺序。:当一个线程修改了共享变量的值,另一个线程可能看不到这个更新,因为每个线程都有自己的工作内存(也叫缓存)。关键字来确保一个变量在所有线程间是可见的,即一旦一个线程改变了该变量的值,新值对于其他线程来说是可以立即看到的。 阅读详情

之前我们聊到多线程的实现,但是线程开发不只是实现一个线程这么简单,线程的实现只是多线程开发的第一步,实现线程后我们还需要保证线程运行的安全性,高效性。不安全的线程实现会导致程序运行结果错误,也可能会导致程序永久性卡死也就是死锁。为了避免这些问题,我们就需要了解什么是线程安全。

什么是线程安全?

线程安全经常在工作中被提到,比如:你的对象是不安全的,你的线程运行结果不对,虽然线程安全经常被提到,但我们可能对线程安全并没有一个明确的定义。那么到底什么是线程安全呢?

《Java Concurrency In Practice》的作者 Brian Goetz 对线程安全是这样理解的,当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行问题,也不需要进行额外的同步,而调用这个对象的行为都可以获得正确的结果,那这个对象便是线程安全的。

Brian Goetz想表达的意思是,如果这个对象是安全的,那么对于使用者而言,在使用的时候不需要考虑多个线程同时写入或读写安全的问题,也不需要额外加锁,那么这个对象我们才能称之为线程安全的对象。

线程安全问题有哪些?

在我们实际开发过程中经常会遇到线程不安全的情况,这里我们介绍3种典型的线程安全问题。

  1. 启动线程时导致线程安全问题。
  2. 多线程运行结果错误。
  3. 活跃性问题。

启动线程时导致线程安全问题

线程创建和启动时导致的线程安全问题,我们创建对象并进行发布和初始化供其他类或对象使用是常见的操作,但如果我们操作的时间或地点不对,就可能导致线程安全问题。如代码所示:

public class ThreadSafe {
    private static List<String> persions;
    public static void main(String[] args) {
        new Thread(() -> {
            persons = new ArrayList<>();
            persons.add("TodoCoder1");
            persons.add("TodoCoder2");
            persons.add("TodoCoder3");
        }).start();
        System.out.println(persons.size());
        System.out.println(persons.get(1));
    }
}
复制代码

我们创建一个共享变量List, main函数中创建并启动一个线程,在线程中给List初始化并添加元素,在主线程中打印结果。试想这个时候程序会出现什么情况?实际上会发生空指针异常或数组越界。

这又是为什么呢?因为 persons 这个成员变量在线程中初始化并添加数据,而线程的启动需要一定的时间,但是我们的 main 函数并没有进行等待就直接获取数据,导致 persons还没有初始化完成或者没有添加完数据,这就是在错误的时间或地点发布或初始化造成的线程安全问题。

多线程运行结果错误

首先,来看多线程同时操作一个变量导致的运行结果错误。

public class ThreadSafe {
    volatile static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 10000; j++) {
                    i++;
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        thread1.start();
        Thread thread2 = new Thread(runnable);
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(i);
    }
}
复制代码

如代码所示,首先定义了一个 int 类型的静态变量 i,然后启动两个线程,分别对变量 i 进行 10000 次 i++ 操作。理论上得到的结果应该是 20000,但实际结果却远小于理论结果,比如可能是15796,也可能是16923,每次的结果都还不一样,这是为什么呢?

这是因为多线程下,线程的执行和调度是由CPU决定的,而CPU 的调度线程是以时间片为单位进行分配的,每个线程都可以得到一定量的时间片。但如果线程拥有的时间片耗尽,它将会被暂停执行并让出 CPU 资源给其他线程,这样就有可能发生线程安全问题。而对于i++操作,看上去是一行代码,实际上它不是一个原子性的操作,它的执行步骤分三步如下:

  1. 第一个步骤是读取,即:i=1;
  2. 第二个步骤是增加,即:i+1;
  3. 第三个步骤是保存,即:i=2;

如果线程1先那到i=1, 然后执行 i+1,结果还没来得急保存下来, 这个时候线程1被切到线程2执行,线程2取的值依然是 i=1, 那么线程2执行完 i+1, i=2后 i的结果是2,线程1的结果也是2,最终的结果就是2了,而不是我们预期的 i=3 , 这就发生了线程安全问题了。

对于这个情况的结决方案,可以用原子类来处理,这里不展开讲了。

活跃性问题

什么是活跃性问题呢?字面上我们可以把它解释成 线程不活跃了 ,也就是程序始终得不到运行的最终结果,相比于前面两种线程安全问题带来的数据错误或报错,活跃性问题带来的后果可能更严重,那么都哪些问题能让线程一直得不到结果呢?总结为以下三种:

  1. 死锁
  2. 活锁
  3. 饥饿

死锁

死锁是最常见的活跃性问题,死锁是指两个线程之间相互等待对方资源,但同时又互不相让,都想自己先执行,然后就一直等待,如下:

public class ThreadSafe {
    private static Object o1 = new Object();
    private static Object o2 = new Object();
    public static void thread1() {
        synchronized (o1) {
            //拿到o1锁
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //等待 thread2释放 o2锁
            synchronized (o2) {
                System.out.println("线程1成功拿到两把锁");
            }
        }
    }
    public static void thread2() {
        synchronized (o2) {
            //拿到o2锁
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //等待 thread1释放 o1锁
            synchronized (o1) {
                System.out.println("线程2成功拿到两把锁");
            }
        }
    }
​
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(ThreadSafe::thread1);
        Thread thread2 = new Thread(ThreadSafe::thread2);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
}
复制代码

首先代码中创建两个对象o1,o2作为 synchronized 锁的对象,线程1 先获取到o1对象锁然后执行逻辑获者sleep 1秒,同时线程2获取到o2对象锁也sleep1秒,休息完后线程 1 想获取 o2 锁,线程 2 想获取 o1 锁,但锁都被对方持有,这时就发生了死锁,就这样一直等待对方先释放资源,导致程序得不到任何结果也不能停止运行。

活锁

第二种活跃性问题是活锁,活锁与死锁非常相似,也是程序一直等不到结果,但对比于死锁,活锁是活的,什么意思呢?因为正在运行的线程并没有阻塞,它始终在运行中,却一直得不到结果。

比如:在消息队列中,一般处理的时候,执行报错时,由于队列的重试机制会重新把消息放在队列头进行优先重试处理,但这个消息本身无论被执行多少次,都无法被正确处量,周而复始,最络导致线程一直忙碌,但程序始终得不到结果,这就是活锁问题。

饥饿

第三个典型的活跃性问题是饥饿,饥饿是指线程需要某些资源时始终得不到,尤其是CPU资源,得不到CPU执行权就会导致线程一直不能运行而产生问题。

比如:在用线程池的时候,如果设置的核心线程数太少,在执行的线程遇到 大文件读取的IO阻塞 ,或者 数据库插入死锁 ,亦或者 HTTP请求网络阻塞 且没有设置超时时间这几个情况都会导致其它线程得不到执行资源,处于长时间的饥饿状态。

以上就是一些线程安全问题,我们在开发中要多多考虑这些,这些场景在高并发中尤为常见,在面试中能考虑这么多的也是个加分项。

java 多线程饥饿现象的问题解决方法 主要介绍了java 多线程饥饿现象的问题解决方法的相关资料,需要的朋友可以参考下 立即下载

相关推荐

nsight-system教程

参考nsight-compute使用教程-优快云博客。

youzjuer的博客 1894

并发编程不能离开的锁

为了解决上述的问题,一个最常见的解决方案就是加锁。通过加锁,保证了操作的原子性,只有获得锁的任务才能执行相关流程,从而保证最后得到的结果是可预知的。比如在电商系统中,下单支付后更改库存,通过使用悲观锁或者乐观锁来保证不会超售。锁是用于控制多任务同时访问共享资源的一种同步机制。通常,我们会将锁分成两大类,悲观锁和乐观锁。悲观锁即认为一定存在竞争,所以在处理之前加上互斥锁,存在排他性,只有获得到该锁的任务才能处理,其他的会被挂起。

hbnn111的专栏 1134

欧姆龙PMAC CK3M控制器新手避坑指南:从开箱到EtherCAT总线配置的完整流程

本文详细介绍了欧姆龙PMAC CK3M控制器的完整配置流程,从开箱检查到EtherCAT总线配置,特别针对新手常见问题提供避坑指南。内容包括硬件连接、软件安装、EtherCAT主站初始化、从站配置、电机参数设置等关键步骤,帮助工程师快速掌握PMAC控制器的使用技巧。

weixin_42532938的博客 391

线程、线程安全、线程同步、锁、线程池

当对象被反序列化时,这些变量的值不会恢复,而是被初始化为其默认值(例如,数字类型为 0,对象类型为 null,布尔类型为 false)在 Java 中,volatile 关键字用于修饰变量,确保对该变量的读写操作都是直接从主存中进行的,而不是从而不是线程的工作内存中。这是一个动态的过程:有自身的产生、存在和消亡的过程,这也是进程的生命周期。线程池中的线程达到(超过)核心线程数,超出的数量会根据存活时间,进行销毁,直到数量达到核心线程数,如果线程的数量少于核心线程数,不会消亡。

Er222220的博客 812

OrangePi Kunpeng Pro 安装 ROS2 + Gazebo

OrangePi Kunpeng Pro 的优点很明显,内置openEuler,配备PD电源,做到开箱即用;板子设计也不错,风扇噪音不大,散热良好,昇腾芯片又确保小板子的性能,接口丰富是这类板子共同的优点,也很好的继承了下来,甚至还可以外接固态,可靠性大大提升。使用起来,哪怕不用openEuler的话,也可以很方便的刷Ubuntu来实现一些需要依赖的开发工作,这次没有跑AI模型,所以对算力的优势还没有很明显的感受,下次可以试一下yolo跑跑看。

Arcan 1827

线程的死锁、活锁和饥饿现象

线程的活锁、死锁和饥饿现象

m0_62609939的博客 3038

rk3588 rkaiq_3A_server 无法解析json文件记录

rk3588 rkaiq_3A_server 无法解析json文件记录

neardi_dev的博客 1万+

Java中的死锁、活锁以及Java服务器CPU突然飙升问题排查

还在为cpu突然飙升烦恼吗? 如果是的话,好好看看这篇文章吧,兴许能帮到你。 1. 死锁 死锁一般发生在两个或者多个线程一直在等待另一个线程释放锁或者资源的情形。这种情况可能会导致线程直接挂掉,因为都在等待锁。 1.代码示例 两个线程在完成指定操作时,都需要获取锁,T1线程是lock1->lock2,T2线程是lock2->lock1。因此两个线程都在以相反的顺序获取锁。 @GetMapping("/deadlock") public void deadlock() {

生生不息,则源源不断 3433

如何利用通达信进行选股集合操作

每个交易者实际上都有自己的选股策略,这是必然的,也会有自己的主观干预,但是还是需要从客观热点股票进行一个比对的,才能说主观和客观相结合不至于​偏离方向。 这几天我也发了节前最后一天我的选股策略盘中的选择结果, 我也从网上拿到了热点股票池,当拿到之后你会发现如何从几十个股票热点股票选出核心票,这就是个问题了,这里面一定要通过股价涨跌幅来进行过滤,而不是通过指标,因为一切指标都是根据价格变化而改变的了,所以​这一步不要走弯路。 1、把拿到的热点股票名单,导入通达信自定义板块 2、删除

抢财猫股票课堂 2584

基于RK3588与昇腾310B的异构K8s集群部署与NPU统一调度实战

本文介绍了基于RK3588与昇腾310B的异构K8s集群部署方案,通过HAMi+Volcano实现NPU统一调度。方案包含硬件配置、操作系统准备(银河麒麟/欧拉)、容器运行时部署及K8s集群搭建等关键步骤,支持多厂商NPU设备的统一抽象和精细化切分,为边缘AI计算提供高效资源管理能力。

huanghm88的专栏 281

JavaEE 第7节 线程饥饿及其解决办法

线程饥饿)指的是多线程中,某些线程无法获得相关资源或者执行机会(阻塞,BLOCKED)长时间如此对线程的推进和响应造成明显影响的现象左边的蘑菇头和右边的小人都是线程,左边的蘑菇头因为在所中没有自己想要的资源所以从锁中出来,但是由于它的优先级比较高一从锁里出来,转头有跑进锁里面了,完全没有给右边的小进去的机会。当饥饿到一定程度,赋予线程的任务即使完成也不在具有实际意义的时候就说明这个线程被饿死了。

HYY的博客 1723

OpenCV-Python实战(10)——详解 OpenCV 轮廓检测

在计算机视觉领域,轮廓通常指图像中对象边界的一系列点。因此,轮廓通常描述了对象边界的关键信息,包含了有关对象形状的主要信息,该信息可用于形状分析与对象检测和识别。在本文中,将首次通过简单示例了解轮廓的基本概念,然后通过实际示例来了解如何检测和压缩轮廓,最后利用介绍如何利用图像矩描述检测到的轮廓属性。

盼小辉丶的博客 3万+

并发编程难题:死锁、活锁、饥饿深度剖析

并发编程领域,死锁、活锁及饥饿问题频繁出现,成为影响程序稳定性和性能的关键因素。这些问题不仅增加了程序运行的复杂性和不确定性,还可能直接导致程序崩溃,给开发者带来了极大的挑战。本文将剖析这些并发编程难题的现象与根源,揭示其在编程实践中的具体表现,以帮助开发者更好地应对复杂并发环境,确保程序的稳定、高效运行。

Rye的博客 3129

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Java并发编程实战 11 | 线程活跃问题(死锁,活锁和饥饿)

第二种活跃度问题是活锁。活锁与死锁非常相似,都是指程序永远得不到最终结果,但它们的表现形式有所不同。活锁中的线程不会像在死锁中那样完全卡住,而是不断地尝试某些操作,但这些操作会导致它们无法继续前进。比如,在路上我们相遇,都会礼貌地让路,结果你往右走,我往左走,又撞在一起,最后谁也过不去。情况是这样的:为了演示活锁的情况,我们可以对之前的死锁示例代码进行一些修改。以下是修改后的逻辑:类似地,两个线程都需要两个锁才能完成工作。每个线程都获取了第一个锁,但发现第二个锁不可用。

大厂小码农的博客 2058

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

死锁、活锁和饥饿是什么意思?

写在前面 本文隶属于专栏《100个问题搞定Java并发》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢! 本专栏目录结构和文献引用请见100个问题搞定Java并发 解答 死锁是指两个或者两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,他们将无法推进下去。 如果线程的智力不够, 且都秉承着“谦让”的原则,主动将资源释放给他人使用,那么就会导致资源不断地在两个线程间跳动,而没有一个线程可以同时拿到所有资源正常执行。这种情况就是活锁。

Shockang的博客 8758

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

java中死锁与活锁的区别,死锁与饥饿的区别

死锁是指两个或多个线程相互等待对方持有的资源,导致这些线程永远无法继续执行的状态。换句话说,死锁是一种“循环等待”的情况,线程之间形成了一个闭环的依赖链,使得所有涉及的线程都无法继续。活锁是指线程或进程因某种条件不断变化而无法完成任务的状态。与死锁不同,活锁中的线程并没有被阻塞,它们一直在活动,但由于彼此之间的干扰,它们无法取得进展。饥饿是指某个线程长期得不到所需的资源(如 CPU 时间、锁或输入/输出资源),导致该线程无法继续执行或执行速度极其缓慢。

Flying_Fish_roe的博客 1393

死锁、活锁、饥饿锁、无锁

死锁、活锁、饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现了这三种情况,即线程不再活跃,不能再正常地执行下去了。 死锁 死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。 举个例子,A同学抢了B同学的钢笔,B同学抢了A同学的书,两个人都相互占用对方的东西,都在让对方先还给自己自己再还,这样...

AI天才研究院 1万+

Java多线程三大困境:死锁、活锁与饥饿的区别

文章探讨Java多线程中死锁、活锁与饥饿三大困境的区别与影响,分析它们如何导致系统性能下降甚至崩溃。通过实例说明各自特点:死锁是资源竞争引起的互相等待,活锁是反复重试未果,饥饿是一个线程长时间得不到服务。强调了合理管理线程资源和调度策略的重要性,并提供了预防与解决这三大问题的实用方法。

yanpeng2580的博客 986

线程活跃性问题(死锁、活锁、饥饿)

活锁表示执行任务的线程没有被阻塞,但由于某些条件没有被满足导致重复的去尝试执行任务(失败->尝试->失败),“活锁”与“死锁”的区别在于“死锁”线程一直处于等待状态而“活锁”执行线程在不断的执行;“饥饿”是指线程执行时无法获取到它所需要的资源比如(CPU),一种情况是“线程”优先级分配不合理导致部分线程一直无法被CPU调度执行;另一种情况是某线程持有某个操作的“锁”但其又处于无限循环而又不释放锁,此时其它等待获取此锁的线程获取不到就出现了饥饿问题;饥饿问题会导致服务器响应变差;详见本章序号1、7、8;

weixin_45876411的博客 1470

死锁、饥饿和活锁

死锁、饥饿和活锁 原文地址 Java并发编程系列之十二:死锁、饥饿和活锁 正文 死锁发生在一个线程需要获取多个资源的时候,这时由于两个线程互相等待对方的资源而被阻塞,死锁是最常见的活跃性问题。 这里先分析死锁的情形: 假设当前情况是线程A已经获取资源R1,线程B已经获取资源R2,之后线程A尝试获取资源R2,这个时候因为资源R2已经被线程B获得了,所以线程A只能阻塞直到线程B释放资源R2。另一方面,...

kaven 1114

Java死锁、活锁、饥饿的区别

Java死锁、活锁、饥饿的区别 Java死锁、活锁、饥饿的区别Java死锁、活锁、饥饿的区别1. Java死锁1.1. 死锁定义1.2.产生死锁的原因1.3. 死锁的比喻1.4. 产生死锁的必要条件1.5.处理死锁的思路。2. Java活锁2.1. 活锁定义2.2. 活锁的比喻2.3. 活锁和死锁的区别3. Java饥饿3.1. 饥饿定义3.2. Java 中导致饥饿的原因:3.3. 饥饿的比喻3...

周鹏飞的IT学习之路 1141

死锁 活锁 饥饿 出现原因及解决方案

文章目录死锁概念死锁示例为什么会出现死锁呢?如何解决死锁呢?解决死锁代码实现活锁概念活锁示例:如何解决活锁呢?饥饿概念如何解决饥饿呢? 死锁 概念 死锁:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。 说白了就是:两个线程互相持有对方所需的资源,互不释放且互相等待 死锁示例 /** * 死锁 * * @author m */ public class Account { ...

男人要霸气的博客 2417

Java多线程之死锁、活锁与饥饿

文章目录Java多线程之死锁死锁的影响死锁的四个必要条件如何定位死锁修复死锁的策略线上发生死锁应该怎么办常见修复策略避免策略哲学家就餐问题多种解决方案检测与恢复策略死锁检测算法死锁恢复机制鸵鸟策略实际工程中如何避免死锁其他活性故障活锁什么是活锁如何解决活锁问题工程中的活锁实例:消息队列饥饿 Java多线程之死锁 死锁发生在并发情况中,当两个(或者多个)线程(进程)相互持有对方所需要的资源,又不主动...

bob_man的博客 3365

有关Java死锁和活锁的联系

本文主要探讨了多线程编程中的死锁和活锁问题。死锁是因线程相互等待资源而导致的系统停滞,需满足互斥、请求保持、不剥夺和循环等待四个条件;可通过预防、避免或检测来解决。活锁则是线程不断改变状态相互响应但无法推进任务,表现为CPU占用但无实际进展;可通过随机退避、限制重试次数或调整锁获取顺序来处理。两者虽表现不同,但都是线程协调不当所致。文章提供了Java代码示例和具体解决方案,强调了理解这些概念对开发健壮并发程序的重要性。

weixin_50055999的博客 1219

Java 并发常见问题:死锁 / 活锁 / 饥饿的排查与解决方案

在多线程并发编程中,除了数据安全问题,线程协作异常是另一类高频问题,其中死锁、活锁、饥饿是最典型的三类问题。这些问题会导致线程无法正常执行、系统性能下降甚至服务不可用,且排查难度高 —— 死锁可能隐藏数月,在高并发场景下才会触发。本文将深入剖析这三类问题的产生原因、典型场景、排查方法,并提供可落地的解决方案与避坑指南。

y245166349的专栏 1409
上一篇: 优秀程序员的代码都是如何分层的?
下一篇: 用户密码加密存储十问十答,一文说透密码安全存储
JavaShark
博客等级 码龄4年 184粉丝 438原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值