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

这篇具有很好参考价值的文章主要介绍了【并发编程】多线程安全问题,如何避免死锁。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

从今天开始阿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,所以俩线程会无限等待,造成死锁。

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

如何避免死锁?

产生死锁的四个必要条件

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

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

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

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

总结

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

到了这里,关于【并发编程】多线程安全问题,如何避免死锁的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【JavaEE】多线程之线程安全(synchronized篇),死锁问题

    线程安全问题 观察线程不安全 线程安全问题的原因  从原子性入手解决线程安全问题 ——synchronized synchronized的使用方法  synchronized的互斥性和可重入性 死锁 死锁的三个典型情况  死锁的四个必要条件  破除死锁 在前面的章节中,我们也了解到多线程为我们的程序带来了

    2024年02月01日
    浏览(57)
  • 【linux 多线程并发】多线程的控制,挂起线程暂停运行,直到唤醒线程,取消线程运行,可以设置合适的取消点属性避免不安全点被中止

    ​ 专栏内容 : 参天引擎内核架构 本专栏一起来聊聊参天引擎内核架构,以及如何实现多机的数据库节点的多读多写,与传统主备,MPP的区别,技术难点的分析,数据元数据同步,多主节点的情况下对故障容灾的支持。 手写数据库toadb 本专栏主要介绍如何从零开发,开发的

    2024年01月20日
    浏览(36)
  • 【Java并发编程】变量的线程安全分析

    1.成员变量和静态变量是否线程安全? 如果他们没有共享,则线程安全 如果被共享: 只有读操作,则线程安全 有写操作,则这段代码是临界区,需要考虑线程安全 2.局部变量是否线程安全 局部变量是线程安全的 当局部变量引用的对象则未必 如果给i对象没有逃离方法的作用

    2024年02月08日
    浏览(56)
  • 关于并发编程与线程安全的思考与实践

    作者:京东健康 张娜 并发编程的意义是充分的利用处理器的每一个核,以达到最高的处理性能,可以让程序运行的更快。而处理器也为了提高计算速率,作出了一系列优化,比如: 1、硬件升级:为平衡CPU 内高速存储器和内存之间数量级的速率差,提升整体性能,引入了多

    2024年02月03日
    浏览(63)
  • 关于并发编程与线程安全的思考与实践 | 京东云技术团队

    作者:京东健康 张娜 并发编程的意义是充分的利用处理器的每一个核,以达到最高的处理性能,可以让程序运行的更快。而处理器也为了提高计算速率,作出了一系列优化,比如: 1、硬件升级:为平衡CPU 内高速存储器和内存之间数量级的速率差,提升整体性能,引入了多

    2024年02月07日
    浏览(70)
  • JUC并发编程-集合不安全情况以及Callable线程创建方式

    1)List 不安全 ArrayList 在并发情况下是不安全的 解决方案 : 1.Vector 2.Collections.synchonizedList() 3. CopyOnWriteArrayList 核心思想 是,如果有 多个调用者(Callers)同时要求相同的资源 (如内存或者是磁盘上的数据存储),他们 会共同获取相同的指针指向相同的资源 , 直到某个调用者

    2024年01月23日
    浏览(49)
  • 什么条件下会出现死锁,如何避免?

    死锁,简单来说就是两个或者两个以上的线程在执行过程中,去争夺同一个共享资源导致相互等待的现象。如果没有外部干预,线程会一直处于阻塞状态,无法往下执行。这样一直等待处于阻塞状态的线程,被称为死锁线程。 产生死锁需要同时满足以下四个条件: 第一个:

    2024年02月12日
    浏览(55)
  • C++面试八股文:如何避免死锁?

    某日二师兄参加XXX科技公司的C++工程师开发岗位第31面: 面试官:什么是锁?有什么作用? 二师兄:在C++中,锁(Lock)是一种同步工具,用于保护共享资源,防止多个线程同时访问,从而避免数据竞争和不一致。 面试官:有哪些锁? 二师兄:从种类上分,可以分为普通锁、

    2024年02月12日
    浏览(50)
  • 阿里二面:如何定位&避免死锁?连着两个面试问到了!

    在面试过程中,死锁是必问的知识点,当然死锁也是我们日常开发中也会遇到的一个问题,同时一些业务场景例如库存扣减,银行转账等都需要去考虑如何避免死锁,一旦线上发生了死锁,那可能年终不保。。。。。下面我们就来聊一聊死锁如何定位,以及如何避免。 死锁(

    2024年03月13日
    浏览(45)
  • 【Java|多线程与高并发】线程安全问题以及synchronized使用实例

    Java多线程环境下,多个线程同时访问共享资源时可能出现的数据竞争和不一致的情况。 线程安全一直都是一个令人头疼的问题.为了解决这个问题,Java为我们提供了很多方式. synchronized、ReentrantLock类等。 使用线程安全的数据结构,例如ConcurrentHashMap、ConcurrentLinkedQueue等

    2024年02月09日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包