【并发编程】多线程安全问题,如何避免死锁

news2024/11/17 13:40:00

文章目录

    • 概念
      • 进程
      • 线程
      • 对比
    • 代码使用
      • 进程
      • 线程
    • 线程创建方式
    • 线程的生命周期和状态
    • 停止线程
    • 方法介绍
      • sleep() / wait()
      • 为什么 wait() 不被定义在 Thread 中?sleep() 定义在 Thread 中?
      • run()/start()
    • 为什么使用多线程?
    • 线程安全问题
    • 线程死锁
      • 如何避免死锁?
    • 总结

从今天开始阿Q将陆续更新java并发编程专栏,期待您的订阅。

在系统学习线程之前,我们先来了解一下它的概念,与经常提到的进程做个对比,方便记忆。

概念

线程和进程是操作系统中的两个重要概念,它们都代表了程序运行时的执行单位,它们的出现是为了更好地管理计算机资源和提高系统的运行效率,使用它们可以实现多任务同时运行,从而提高系统资源的利用率。

进程

进程是程序的一次执行过程,系统运行的每一个程序都是一个进程,它是操作系统资源分配的最小单位,也是系统运行程序的基本单位。

在同一个操作系统中,多个进程可以并发执行,每个进程都拥有各自独立的内存空间,相互之间不会产生影响。如下图,windows 系统的任务管理器页面运行的进程就对应着一个一个的应用。

在这里插入图片描述

在 Java 中,启动 main 函数就是启动了一个 JVM 进程,而 main 函数所在的线程就是进程中的一个线程,也称主线程。

线程

线程是比进程更小的执行单位,一个进程中包含多个线程,通过 JMX 来看看一个普通的 Java 程序有哪些线程:

public class MultiThread {
    public static void main(String[] args) {
            // 获取 Java 线程管理 MXBean
            ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
            // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
            ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
            // 遍历线程信息,仅打印线程 ID 和线程名称信息
            for (ThreadInfo threadInfo : threadInfos) {
                    System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
            }
    }
}

/**
执行结果:
[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //调用对象 finalize 方法的线程
[2] Reference Handler //清除 reference 线程
[1] main //main 线程,程序入口
*/

线程共享所属进程的堆和方法区,独有程序计数器、虚拟机栈、本地方法栈等资源。如下图:红色为线程共享区域,蓝色为线程独占区域。

在这里插入图片描述

线程的创建、销毁、切换的开销比进程小,能够更快地完成任务,并且可以充分利用多核处理器的优势,所以又被称为轻量级进程。同一进程的线程间可能会进行资源的交互,而进程间是不存在的。

对比

总的来说,进程与线程的区别主要有以下几点:

  • 进程是操作系统分配资源的最小单位,而线程是CPU调度的最小单位。
  • 进程之间相互独立,拥有各自独立的内存空间,线程之间共享进程的内存空间。
  • 进程切换开销大,线程切换开销小。
  • 进程间通信复杂,线程间通信简单。

代码使用

进程

在 java 代码中通过使用 ProcessBuilder 类来创建一个名为 notepad.exe 的进程,即打开记事本应用程序。

public class CreateProcessTest {
    public static void main(String[] args) {
        try {
            ProcessBuilder processBuilder = new ProcessBuilder("notepad.exe");
            Process process = processBuilder.start();

            int exitCode = process.waitFor();
            System.out.println("进程已结束,退出码为:" + exitCode);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

然后调用 waitFor() 方法等待进程结束,并获取进程的退出码。

最后运行这个程序,会看到一个新的记事本窗口弹出来,这就是我们创建的进程。

在这里插入图片描述

当关闭记事本窗口时,程序会继续执行,输出进程的退出码。

线程

使用 Thread 类来创建一个新的线程:在 Thread 构造函数中,传入一个 Runnable 接口的实现,该实现定义了线程的任务。

这里只是简单地输出一些信息并模拟一个耗时任务。

public class CreateThreadTest {
    public static void main(String[] args) {
        // 创建一个新的线程
        Thread thread = new Thread(() -> {
            System.out.println("线程开始执行");
            try {
                Thread.sleep(10000); // 模拟线程执行耗时任务
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程执行完成");
        });
        // 启动线程
        thread.start();
        System.out.println("主线程继续执行");
    }
}

然后调用 start() 方法来启动线程,当线程启动后,它会执行 run() 方法中定义的任务,同时主线程会继续执行。

执行结果如下:

主线程继续执行
线程开始执行
线程执行完成

线程创建方式

①. 继承Thread类创建线程类

  • 定义 Thread 类的子类,并重写该类的 run() 方法,该 run() 方法的方法体就代表了线程要完成的任务。因此把 run() 方法称为执行体。

  • 创建 Thread 子类的实例,即创建了线程对象。

  • 调用线程对象的 start() 方法来启动该线程。

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("1111111");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

②. 通过 Runnable 接口创建线程类

  • 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体。

  • 创建 Runnable 实现类的实例,并依此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。

  • 调用线程对象的 start() 方法来启动该线程。

public class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println("1111111");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        Thread threadNew = new Thread(thread);
        threadNew.start();
    }
}

③. 通过 Callable 和 Future 创建线程

  • 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

  • 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

  • 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

public class MyThread implements Callable {

    @Override
    public Object call() throws Exception {
        return 1+1;
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        FutureTask futureTask = new FutureTask<>(myThread);
        new Thread(futureTask).start();
        Object o = null;
        try {
            o = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(o.toString());
    }
}

线程的生命周期和状态

在这里插入图片描述

  • NEW: 初始状态,线程被创建出来但没有被调用 start() 。
  • RUNNABLE: 运行状态,线程被调用了 start() 等待运行的状态。
  • BLOCKED :阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

操作系统层面,线程有 READTY 和 RUNNING 两种状态,而在 JVM 层面,只有 RUNNABLE 一种状态,因为每个线程在获取到 CPU 时间片时只会运行 0.01秒左右就会被切换执行其他的线程,线程切换速度之快,就没必要区分这两种状态了。

停止线程

在 Java 中,可以通过调用线程的 interrupt() 方法来中止线程。但是这并不意味着线程会立即停止执行,它只是设置了一个中断标志,线程可以通过检查这个标志来自行终止。 具体来说,当线程被中断时,可以通过以下方式来检查中断标志:

  1. 调用 Thread.currentThread().isInterrupted() 方法检查当前线程是否被中断。
  2. 调用 Thread.interrupted() 方法检查当前线程是否被中断,并清除中断状态。

在实际应用中,如果需要中止一个线程,可以在执行任务的循环中检查中断标志,如果中断标志被设置,则退出循环,从而中止线程的执行。

方法介绍

  • Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入阻塞,但不释放对象锁,millis 后线程自动苏醒进入可运行状态。作用:给其它线程执行机会的最佳方式。
  • Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的 cpu 时间片,由运行状态变会可运行状态,让 OS 再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证 yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield() 不会导致阻塞。
  • obj.wait(),当前线程调用对象的 wait() 方法,当前线程释放对象锁,进入等待队列。依靠 notify()/notifyAll() 唤醒或者 wait(long timeout) timeout 时间到自动唤醒。
  • t.join()/t.join(long millis),当前线程里调用其它线程1的 join 方法,当前线程阻塞,但不释放对象锁,直到线程1执行完毕或者 millis 时间到,当前线程进入可运行状态。
  • obj.notify() 唤醒在此对象监视器上等待的单个线程,选择是任意性的。
  • notifyAll() 唤醒在此对象监视器上等待的所有线程。

sleep() / wait()

共同点 :两者都可以暂停线程的执行。

区别

  • sleep() 方法不释放锁, wait() 方法释放锁;
  • wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。

为什么 wait() 不被定义在 Thread 中?sleep() 定义在 Thread 中?

wait 是想让获得对象锁的线程暂停执行,会自动释放当前线程占有的对象锁。每个对象都有对象锁,既然要释放对象锁,就得对对象进行操作。

因为 sleep() 是让当前线程暂停执行,不需要获得对象锁,所以不涉及到对象类。

run()/start()

  • 通过调用 Thread 类的 start() 方法来启动一个线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此 Thread.start() 调用方法 run() 来完成其运行状态, 这里方法 run() 称为线程体,它包含了要执行的这个线程的内容, run() 方法运行结束, 此线程终止。然后CPU再调度其它线程。
  • run() 方法是在本线程里的,只是线程里的一个函数, 而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接调用 run() 方法必须等待 run() 方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start() 方法而不是 run() 方法。

为什么使用多线程?

  • 单个线程被IO阻塞,其它线程还可以获取 cpu,提高系统资源的利用率。
  • 线程被称为轻量级的进程,它的切换与调度成本远远小于进程。
  • 利用好多线程机制可以大大提高系统整体的并发能力以及性能。
  • 现在到了多核的时代,多个线程可以并行执行,减少了线程上下文切换的开销。

CPU 通过给每个线程分配 CPU 时间片 来实现线程切换,在进行线程切换时需要保存当前线程正在执行任务的状态(也就是我们所说的上下文),以便下次切回到这个任务时,还可以继续执行该任务,即任务从保存到再加载的过程就是一次上下文切换。

多线程的缺点:多线程并发执行可能会导致内存泄漏、死锁、线程不安全等问题。

线程安全问题

在 Java 中,多线程并发操作同一个共享变量时,就可能会发生线程安全问题。 在 Java 中保证线程安全的常用手段有以下三个:

  1. 使用锁机制:锁机制是一种用于控制多个线程对共享资源进行访问的机制。在 Java 中,锁机制主要有两种:synchronized 关键字和 Lock 接口。synchronized 关键字是 Java 中最基本的锁机制,它可以用来修饰方法或代码块,以实现对共享资源的互斥访问。而 Lock 接口是 Java5 中新增的一种锁机制,它提供了比 synchronized 更强大、更灵活的锁定机制,例如可重入锁、读写锁等;
  2. 使用线程安全的容器:如 ConcurrentHashMap、Hashtable、Vector。需要注意的是,线程安全的容器底层通常也是使用锁机制实现的;
  3. 使用本地变量:线程本地变量是一种特殊的变量,它只能被同一个线程访问。在 Java 中,线程本地变量可以通过 ThreadLocal 类来实现。每个 ThreadLocal 对象都可以存储一个线程本地变量,而且每个线程都有自己的一份线程本地变量副本,因此不同的线程之间互不干扰。

线程死锁

多个线程同时被阻塞,他们都在等待某个资源被释放,由于线程无限期的阻塞,导致程序不可能正常终止,我们把这种现象称为死锁。

如下图所示,线程1拥有资源A的锁A,想要获取资源B的锁B,但是此时资源B的锁B正被线程2拥有,而线程2却想要获取线程1拥有的锁A,所以俩线程会无限等待,造成死锁。

在这里插入图片描述

如何避免死锁?

产生死锁的四个必要条件

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

如何预防和避免线程死锁?

破坏死锁的产生的必要条件即可:

  • 破坏请求与保持条件 :一次性申请所有的资源。
  • 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

总结

本文我们从大家熟知的线程和进程入手,通过对比他俩的使用场景、代码使用来方便记忆。随后对线程的创建方式以及生命周期进行了详细的讲解。然后介绍了线程使用过程中的一些方法,方便大家更好的入手。最后对多线程的安全问题和死锁问题进行了总结,希望大家做到温故而知新,要不然很容易忘记概念性的东西。

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

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

相关文章

可孚医疗:「最懂互联网」的医疗器械企业是如何炼成的?

如果说钉钉在过去的标签是软件&#xff0c;是低代码。那么在医疗这个赛道里&#xff0c;这些标签已经不足以成为钉钉价值的侧写&#xff0c;除了固有标签之外&#xff0c;在可孚医疗的场景里&#xff0c;钉钉可以连接&#xff0c;可以成为智能BI&#xff0c;也更可以做到内外部…

(旧版) 家居购项目 1.分页导航 2.购物车 3.订单生成 4.权限验证 5.事务管理 6.上传图片

文章目录 &#x1f400;Java后端经典三层架构&#x1f407;MVC模型&#x1f407;开发环境搭建&#x1f407;会员注册&#x1f333;前端验证用户注册信息&#x1f333;思路分析&#x1f349;创建表&#x1f349;创建实体类&#x1f349;DAO&#x1f34c;MemberDAOImpl &#x1f…

kali磁盘空间不足,软连接解决apt下载问题

知识点&#xff1a; ①在Linux系统下(其他操作系统也有类似的规定)&#xff0c;磁盘的分区大致可以分为三类&#xff0c;分别为主分区、扩展分区和逻辑分区等等。 ②主分区可以有1-4个&#xff0c;扩展分区可以有0-1个&#xff0c;逻辑分区编号从5开始。 ③主分区可以直接进行格…

数据库信息速递 DataStax与谷歌合作将向NoSQL AstraDB引入向量搜索技术

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

机器学习5:基于线性回归理解减少“损失”的方法

在上节《机器学习4&#xff1a;基本术语》中&#xff0c;笔者介绍了“损失&#xff08;Loss&#xff09;”的定义&#xff0c;在训练模型时&#xff0c;减少损失&#xff08;Reducing Loss&#xff09;是极为关键的&#xff0c;只有“损失”足够小的机器学习系统才有实用价值。…

【数据库】mysql主从复制与读写分离

文章目录 一、读写分离1. 什么是读写分离2. 为什么要读写分离3. 什么时候要读写分离4. 主从复制与读写分离5. Mysql 主从复制原理&#xff08;主从复制的类型&#xff09;6. Mysql 主从复制的工作过程7. Mysql 读写分离原理 二、主从复制的配置操作1. 环境配置2. 搭建 MySQL主从…

从第一性原理揭秘爱因斯坦相对论

摘要&#xff1a; 本文首先将探讨狭义相对论的基本原理及其起源。接着&#xff0c;我们将深入分析狭义相对论的世界观给我们的认知带来了哪些本质的改变。最后&#xff0c;我们将探讨狭义相对论为何无法解决引力的矛盾性问题&#xff0c;以及广义相对论是如何有效解决此类问题的…

最大匹配问题---男女匹配问题(算法)

扯淡&#xff1a; 今天期末复习的时候发现一个算法很有意思&#xff0c;就是男女最大分配对象问题&#xff0c;几对男女最多能凑够几对对象。 根据社会主义核心价值观&#xff0c;我们最好整一夫一妻制&#xff0c;分配一人一对象&#xff0c;我辈义不容辞。 题目分析&#…

需求分析六步法

需求收集可能看起来不言自明&#xff0c;但它很少得到应有的充分关注。就像运动前伸展或睡前刷牙一样&#xff0c;这是一项经常被忽视的简单任务。 但是&#xff0c;忽视这些看似简单的事情的后果可能会导致伤害、蛀牙&#xff0c;或者在项目管理的情况下&#xff0c;导致项目…

GeoServer发布图层遇到的几个雷点及解决方案

目录 前言 一、图层编码导致图层预览无服务的异常问题 1、问题描述 2、问题分析 3、问题解决 二、图层空间参考投影设置的问题 1、问题描述 2、问题分析 3、问题解决 三、Qgis导出的SLD在GeoServer中发布预览报错的问题 1、问题描述 2、问题分析 3、问题解决 四、GeoS…

java 临床症状识别系统Myeclipse开发mysql数据库web结构jsp编程计算机网页项目

一、源码特点 JSP 临床症状识别系统 是一套完善的系统源码&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;以及相应配套的设计文档&#xff0c;系统主要采用B/S 模式开发。 研究的基本内容是基于Web的临床症状识别…

一文详解如何用GPU来运行Python代码/基于Python自制一个文件解压缩小工具

前几天捣鼓了一下Ubuntu&#xff0c;正是想用一下我旧电脑上的N卡&#xff0c;可以用GPU来跑代码&#xff0c;体验一下多核的快乐&#xff0c;感兴趣的小伙伴快跟随小编一起了解一下吧 简介 前几天捣鼓了一下Ubuntu&#xff0c;正是想用一下我旧电脑上的N卡&#xff0c;可以用…

Nova代码解析

1. 引言 前序博客有&#xff1a; Nova: Recursive Zero-Knowledge Arguments from Folding Schemes学习笔记基于cycle of curves的Nova证明系统&#xff08;1&#xff09;基于cycle of curves的Nova证明系统&#xff08;2&#xff09; 微软团队2021年论文 《Nova: Recursive…

警惕2本期刊被剔除!2023年6月EI目录已更新!(附全目录下载)

2023年6月EI期刊目录更新 爱思唯尔官网近日更新了EI期刊目录&#xff0c;此次更新是2023年6月1日&#xff0c;与上次更新&#xff08;2023年2月&#xff09;相比&#xff0c;有3本期刊名称在Serials&#xff08;连续出版&#xff09;列表中搜索不到&#xff0c;其中&#xff0…

【前端|HTML系列第2篇】HTML零基础入门之标签元素

大家好&#xff0c;欢迎来到前端入门系列的第二篇博客。在这个系列中&#xff0c;我们将一起学习前端开发的基础知识&#xff0c;从零开始构建网页和Web应用程序。本篇博客将为大家介绍HTML&#xff08;超文本标记语言&#xff09;常用标签元素&#xff0c;帮助零基础小白快速入…

Upload靶场通关笔记(更新中)

文章目录 一、Pass-011.抓包上传2.获取上传路径3.工具验证 二、Pass-02三、Pass-031.使用httpd.conf自定义后缀2.提取上传文件名3.工具测试4.注意点四、Pass-041.上传.htaccess2.上传图片3.工具测试 五、Pass-05六、Pass-061.空格.号绕过2.工具测试 七、Pass-07八、Pass-081.特…

电气工程师日常工作常遇到的41种接线方法(一)

01 电动机接线 一般常用三相交流电动机接线架上都引出6个接线柱&#xff0c;当电动机铭牌上标为Y形接法时&#xff0c;D6、D4、D5相连接&#xff0c;D1-D3接电源&#xff1b;为△形接法时&#xff0c;D6与D1连接&#xff0c;D4与D2连接&#xff0c;D5与D3连接&#xff0c;然后…

报表软件测评|如何选择一款简单易用的报表开发工具

报表业务的稳定性天生较差&#xff0c;随着业务的推进&#xff0c;会不断产生新的查询统计需求&#xff0c;报表也必须相应地不断增加、修改。这导致报表的数量似乎没有止境。通常情况下&#xff0c;企业信息系统的主要功能模块在上线后相对稳定&#xff0c;不再变化&#xff0…

文心一言 VS 讯飞星火 VS chatgpt (48)-- 算法导论6.1 6题

值为<23&#xff0c;17&#xff0c;14&#xff0c;6&#xff0c;13&#xff0c;10&#xff0c;1&#xff0c;5&#xff0c;7&#xff0c;12>的数组是一个最大堆吗? 文心一言&#xff1a; 是的&#xff0c;这个数组是一个最大堆。 最大堆是一种特殊的二叉树&#xff0…

第十六届CISCN复现MISC——国粹

国粹 不是我说&#xff0c;我当时比赛的时候&#xff0c;在那里叭叭叭的数的老用心了结果他是一道非常不常规的图片密码题&#xff0c;又是一种我没见过的题型 看了一些大佬的解题&#xff0c;知道他是一个坐标类型的图片拼凑 发现很多都提到了opencv&#xff0c;又是一个知识…