线程的安全问题

目录

导言:

正文:

1.共享资源:

2.非原子操作:

3.执行顺序不确定:

4.可见性:

5.死锁和饥饿:

6.指令重排序:

总结:


导言:

线程安全是并发编程中的一个重要概念,它指的是在多线程环境下,对共享数据的访问和修改不会导致数据的不一致或其他不可预料的结果。在Java中,线程安全问题通常涉及到共享变量的访问和修改,以及多线程间的同步和协作。

正文:

1.共享资源

多线程程序中,多个线程可能同时访问并修改共享的数据结构、对象或变量。如果没有适当的同步机制,就会导致数据竞争问题。许多操作,如自增(++)、自减(--)、赋值等,虽然看起来是简单的操作,实际上在底层可能包含多个步骤(如读取值、修改值、写回值)。如果这些步骤在执行过程中被其他线程中断,就可能导致最终的值不符合预期。

代码实例:

public class test {
   private static int count;

    public static void main(String[] args) throws InterruptedException {
        //创建线程t1
        Thread t1 = new Thread(() -> {
           for (int i = 0; i < 50000; i++)
               count++;
        });
        //创建线程t2
        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 50000; i++)
                count++;
        });
        //启动两个线程
        t1.start();
        t2.start();
        //保证两个线程能运行完
        t1.join();
        t2.join();
        //预期结果:10w
        System.out.println("count = " + count);
    }
}

这段代码的目的是让两个线程t1t2各自对静态变量count进行50000次自增操作,预期的最终结果是count的值变为100000。然而,这段代码存在线程安全问题,导致最终输出的count值每次都不一样。

问题的根源在于count++操作不是原子的。这个操作实际上包含了三个独立的步骤:

  1. 读取count当前的值。
  2. 增加该值。
  3. 将新值写回count

当多个线程并发执行count++操作时,可能会出现以下情况:

  • 线程A读取了count的值(假设为0)。
  • 线程B也读取了count的值(同样为0)。
  • 线程A增加其count的值到1,并写回内存。
  • 线程B增加其count的值到1,并写回内存。

在这种情况下,尽管两个线程都执行了count++操作,但count的最终值只增加了1,而不是2。这是因为两个线程可能读取到了相同的初始值,并且在增加和写回值的过程中没有适当的同步。

解决方法:

使用synchronized关键字,synchronized 关键字是 Java 中用于处理并发问题的同步机制之一。它可以确保同一时间只有一个线程能够访问被 synchronized 修饰的代码块或方法,从而解决多线程并发访问共享资源时的线程安全问题。

public class Test {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 创建线程t1
        Thread t1 = new Thread(() -> {
            synchronized (countLock) {
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
        });
        // 创建线程t2
        Thread t2 = new Thread(() -> {
            synchronized (countLock) {
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
        });
        // countLock 是一个用来同步的静态对象
        private static final Object countLock = new Object();

        // 启动两个线程
        t1.start();
        t2.start();
        // 保证两个线程能运行完
        t1.join();
        t2.join();
        // 预期结果:10w
        System.out.println("count = " + count);
    }
}

在这个修改后的代码中,我们引入了一个静态对象 countLock 作为同步锁。两个线程在修改 count 变量时都会尝试获取这个锁对象的锁。当一个线程持有锁时,其他线程必须等待直到锁被释放。这样就保证了 count++ 的正确性。

2.非原子操作

非原子性操作指的是那些在执行过程中可以被其他线程中断的操作。在多线程环境中,非原子性操作可能导致竞态条件、数据不一致和其他线程安全问题。某些操作不是原子性的,即不能一次性完成所有操作。在多线程环境下,一个操作可能被多个线程交错执行,导致意外结果。

一条 java 语句不一定是原子的,也不一定只是一条指令

public class RaceConditionExample {
    private int sharedState = 0;

    public void increment() {
        sharedState++; // 非原子性操作
    }
}

在上面的例子中,increment 方法看起来是简单的自增操作,但实际上它包含三个独立的步骤:读取 sharedState 的值、增加值、写回新的值。如果有多个线程并发调用 increment 方法,sharedState 的值可能不会按预期递增。

解决办法同样是使用锁

public class SynchronizedSolution {
    private final Object lock = new Object();
    private int sharedState = 0;

    public void increment() {
        synchronized (lock) {
            sharedState++;
        }
    }
}

非原子性操作是多线程编程中常见的线程安全问题来源。解决这类问题的关键是通过使用 synchronized 关键字。

3.执行顺序不确定

多线程程序的执行顺序是不确定的,线程的调度是由操作系统和JVM控制的。线程的调度是随机的,这是线程安全问题的罪魁祸首。由于线程调度的随机性,即使是相同的程序在不同的执行环境下,或者在同一环境下不同的运行次数,都可能产生不同的结果。如果多个线程对共享资源的访问顺序不一致,就会产生不确定的结果。

解决这个问题的关键在于使用适当的同步机制来控制线程的执行顺序和访问共享资源的方式。通过使用 synchronized 关键字、原子类、并发集合类和其他并发工具,可以有效地避免由于执行顺序不确定性导致的线程问题。开发者应该在设计和实现多线程程序时充分考虑这些潜在问题,并采取适当的同步策略来确保程序的正确性和性能。

4.可见性

可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到。现代计算机系统中,每个CPU核心都有自己的缓存,这可能导致不同核心之间的数据不一致。当一个线程修改了共享变量的值,其他线程可能无法立即看到这个修改。下面进行更详细的说明:

先给出一幅图:

1.线程之间的共享变量存在 主内存 (Main Memory)。

2.每一个线程都有自己的 "工作内存" (Working Memory) 。

3.当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据。

4.当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 "副本"。此时修改线程1 的工作内存中的值, 线程2 的工作内存不一定会及时变化。此时代码就会出现问题。同样使用锁即可解决这种问题。

5.死锁和饥饿

死锁是指多个线程或进程因争夺资源而造成的一种僵局,每个线程都在等待其他线程释放资源,导致所有线程都无法继续执行。死锁通常包含四个必要条件:互斥条件、请求与保持条件、不剥夺条件和循环等待条件。

饥饿是指一个或多个线程由于某种原因无法获取所需的资源,导致无法继续执行的情况。造成饥饿的原因可能包括优先级反转、资源竞争等。

public class DeadlockExample {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 acquired lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("Thread 1 acquired lock2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 acquired lock2");
                synchronized (lock1) {
                    System.out.println("Thread 2 acquired lock1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

在上面的代码中,我们创建了两个线程thread1thread2,分别尝试获取lock1lock2,但它们的获取顺序不同,导致了死锁的发生。每个线程获取了一个锁,同时申请另一个锁导致两个进程永无止境的等待下去。

解决死锁问题的方法包括:

  • 预防死锁:设计良好的资源分配策略,破坏死锁的四个必要条件。
  • 避免死锁:通过安全序列算法等方法在运行时避免发生死锁。
  • 检测和恢复:通过检测死锁的发生,采取相应的措施打破死锁。

以下是对上述死锁问题的代码进行修改,通过调整获取锁的顺序来避免死锁的发生:

public class DeadlockSolution {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 acquired lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("Thread 1 acquired lock2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 2 acquired lock1");
                synchronized (lock2) {
                    System.out.println("Thread 2 acquired lock2");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

解决饥饿问题的方法包括:

  • 公平性:设计公平的资源分配策略,确保每个线程都有机会获取资源。
  • 优先级调度:通过优先级调度算法确保高优先级的线程能够及时获得所需的资源。
  • 资源复用:尽量减少资源的持有时间,避免资源长时间被占用而导致其他线程饥饿。

对于饥饿问题,可以通过设置线程的优先级或使用公平的锁来解决。以下是一个简单的代码,

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class StarvationSolution {
    private static final Lock fairLock = new ReentrantLock(true); // 使用公平锁

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    fairLock.lock();
                    System.out.println(Thread.currentThread().getName() + " acquired the fair lock");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    fairLock.unlock();
                }
            }).start();
        }
    }
}

在上述代码中,我们使用ReentrantLock来创建一个公平锁,并在创建线程时指定使用公平锁。这样可以保证等待时间最长的线程会最先获取到锁,避免了饥饿问题的发生。 

6.指令重排序

指令重排序是现代处理器为了提高性能而采取的一种优化手段,它可以改变程序中指令的执行顺序,但不会改变程序的最终结果。然而,指令重排序可能会导致多线程程序出现一些意想不到的问题,如内存可见性问题、数据竞争等。

代码实例:

import java.util.Scanner;

public class test {
    static class Counter {
        public int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while (counter.flag == 0) {
            }
            System.out.println("循环结束!");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入一个整数:");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

在这个示例中,t1线程中不断地检查counter.flag是否为0,而t2线程负责从标准输入中读取一个整数并赋值给counter.flag。预期当用户输入非 0 的值的时候, t1 线程结束。实际上当用户输入非0值时, t1 线程循环不会结束 。

JVM可能会对指令进行重排序,导致t2线程中的赋值操作在t1线程看来发生在读取操作之前,从而t1线程永远无法看到t2线程修改的counter.flag值。

为了解决这个问题,可以通过以下方式进行修复:

  1. 使用volatile关键字修饰Counter类中的flag变量,确保线程之间的内存可见性。
  2. 使用synchronized关键字或Lock来保护共享变量的读写操作,确保线程安全。
  3. 使用wait()notify()等方法实现线程间的通信,避免忙等待的方式。

修复后的代码示例:

import java.util.Scanner;

public class test {
    static class Counter {
        public volatile int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while (counter.flag == 0) {
            }
            System.out.println("循环结束!");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入一个整数:");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

通过将flag变量设置为volatile,可以确保线程之间对flag变量的可见性,避免出现数据不一致的情况。 

需要注意的是

指令重排序并不是一定会发生的。指令重排序是编译器或处理器为了提高程序执行性能而采取的一种优化手段,它可以在不影响单线程程序正确性的前提下,对指令的执行顺序进行调整。然而,这种优化并不是在所有情况下都会发生,而是根据具体的程序代码、编译器实现以及处理器特性来决定的。

编译器或处理器在进行指令重排序时,会遵循一定的规则和限制,以确保程序的正确性不受影响。例如,存在数据依赖关系的指令通常不会被重排序,因为这样做可能会改变程序的执行结果。此外,即使在允许重排序的情况下,编译器或处理器也可能会根据当前的执行环境和优化策略,选择不进行重排序。

总结:

线程安全问题是指在多线程环境下,当多个线程同时访问共享资源时可能导致的数据不一致、竞态条件、死锁等问题。为了解决线程安全问题,可以使用同步机制(如synchronized关键字、ReentrantLock等)来保护共享资源的访问,或者使用volatile关键字来确保共享变量的可见性。通过合理的设计和编码,可以有效地避免线程安全问题,确保多线程程序的正确性和稳定性。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

文献阅读:使用 CellChat 推理和分析细胞-细胞通信

文献介绍 「文献题目」 Inference and analysis of cell-cell communication using CellChat 「研究团队」 聂青&#xff08;加利福尼亚大学欧文分校&#xff09; 「发表时间」 2021-02-17 「发表期刊」 Nature Communications 「影响因子」 16.6 「DOI」 10.1038/s41467-0…

Vue3 使用 v-bind 动态绑定 CSS 样式

在 Vue3 中&#xff0c;可以通过 v-bind 动态绑定 CSS 样式。 语法格式&#xff1a; color: v-bind(数据); 基础使用&#xff1a; <template><h3 class"title">我是父组件</h3><button click"state !state">按钮</button>…

解析CUDA FATBIN格式

参考文档&#xff1a; https://pdfs.semanticscholar.org/5096/25785304410039297b741ad2007e7ce0636b.pdf CUDA Pro Tip: Understand Fat Binaries and JIT Caching | NVIDIA Technical Blog cuda二进制文件中到底有什么 - 知乎 NVIDIA CUDA Compiler Driver NVIDIA CUDA…

HSP_04章_扩展: 进制、位运算

文章目录 10. 扩展: 进制11. 位运算11.1 二进制在运算中的说明11.2 原码 反码 补码11.3位运算符11.3.1 ~按位取反11.3.2 &按位与11.3.3 ^按位异或11.3.4 |按位或11.3.5 << 左移11.3.6 >> 右移 10. 扩展: 进制 进制介绍 进制的转换 2.1 其他进制转十进制 二进…

(八)目标跟踪中参数估计(似然、贝叶斯估计)理论知识

目录 前言 一、统计学基础知识 &#xff08;一&#xff09;随机变量 &#xff08;二&#xff09;全概率公式 &#xff08;三&#xff09;高斯分布及其性质 二、似然是什么&#xff1f; &#xff08;一&#xff09;概率和似然 &#xff08;二&#xff09;极大似然估计 …

国内顶级大牛整理:分布式消息中间件实践笔记+分布式核心原理解析

XMPP JMS RabbitMQ 简介 工程实例 Java 访问RabbitMQ实例 Spring 整合RabbitMQ 基于RabbitMQ的异步处理 基于RabbitMQ的消息推送 RabbitMQ实践建议 虚拟主机 消息保存 消息确认模式 消费者应答 流控机制 通道 总结 ActiveMQ 简介 工程实例 Java 访问ActiveMQ实例…

机器人寻路算法双向A*(Bidirectional A*)算法的实现C++、Python、Matlab语言

机器人寻路算法双向A*&#xff08;Bidirectional A*&#xff09;算法的实现C、Python、Matlab语言 最近好久没更新&#xff0c;在搞华为的软件挑战赛&#xff08;软挑&#xff09;&#xff0c;好卷只能说。去年还能混进32强&#xff0c;今年就比较迷糊了&#xff0c;这东西对我…

EasyRecovery2024汉化精简版,无需注册

EasyRecovery2024是世界著名数据恢复公司 Ontrack 的技术杰作&#xff0c;它是一个威力非常强大的硬盘数据恢复软件。能够帮你恢复丢失的数据以及重建文件系统。 EasyRecovery不会向你的原始驱动器写入任何东东&#xff0c;它主要是在内存中重建文件分区表使数据能够安全地传输…

FL Studio21.2.3中文版软件新功能介绍及下载安装步骤教程

FL Studio21.2中文版的适用人群非常广泛&#xff0c;主要包括以下几类&#xff1a; FL Studio 21 Win-安装包下载如下: https://wm.makeding.com/iclk/?zoneid55981 FL Studio 21 Mac-安装包下载如下: https://wm.makeding.com/iclk/?zoneid55982 音乐制作人&#xff1a…

C#/BS手麻系统源码 手术麻醉管理系统源码 商业项目源码

C#/BS手麻系统源码 手术麻醉管理系统源码 商业项目源码 手麻系统从麻醉医生实际工作环境和流程需求方面设计&#xff0c;与HIS&#xff0c;LIS&#xff0c;PACS&#xff0c;EMR无缝连接&#xff0c;方便查看患者的信息;实现术前、术中、术后手术麻醉信息全记录;减少麻醉医师在…

Spring Boot配置⽂件的格式

1、Spring Boot 配置⽂件有以下三种&#xff1a; &#xff08;1&#xff09;application.properties &#xff08;2&#xff09;application.yml &#xff08;3&#xff09;application.yaml 2、yml 为yaml的简写, 实际开发中出现频率最⾼. yaml 和yml 的使⽤⽅式⼀样 3、 4…

Vue + .NetCore前后端分离,不一样的快速发开框架

摘要&#xff1a; 随着前端技术的快速发展&#xff0c;Vue.NetCore框架已成为前后端分离开发中的热门选择。本文将深入探讨Vue.NetCore前后端分离的快速开发框架&#xff0c;以及它如何助力开发人员提高效率、降低开发复杂度。文章将从基础功能、核心优势、适用范围、依赖环境等…

软考之零碎片段记录(一)

2023上半年选择题 一、流水线周期 注&#xff1a;&#xff08;n-1) * 流水线周期 &#xff08;取址时间分析时间执行时间&#xff09; 注&#xff1a;流水线周期&#xff1a;指令中最耗时的部分&#xff08;取址或者分析或者执行&#xff09; # 耗时最高的部分 * &#xff0…

单例设计模式(3)

单例模式&#xff08;3&#xff09; 实现集群环境下的分布式单例类 如何理解单例模式中的唯一性&#xff1f; 单例模式创建的对象是进程唯一的。以springboot应用程序为例&#xff0c;他是一个进程&#xff0c;可能包含多个线程&#xff0c;单例代表在这个进程的某个类是唯一…

Unity 基于Rigidbody2D模块的角色移动

制作好站立和移动的动画后 控制器设计 站立 移动 角色移动代码如下&#xff1a; using System.Collections; using System.Collections.Generic; using Unity.VisualScripting; using UnityEngine;public class p1_c : MonoBehaviour {// 获取动画组件private Animator …

LeetCode Python - 84. 柱状图中最大的矩形

目录 题目描述解法方法一方法二 运行结果方法一方法二 题目描述 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 示例 1: 输入&#xff1a;heights …

《最小阻力之路》利用最小阻力路径,采用创造性思维模式,更有效地实现个人愿景和目标 - 三余书屋 3ysw.net

最小阻力之路 大家好&#xff0c;今天我们分享《最小阻力之路》。我们时常听到有人感叹&#xff0c;明明懂得那么多道理&#xff0c;为何生活过得不如意呢&#xff1f;这本书从某种角度回应了这个疑问&#xff0c;作者分析了我们在人生旅途中屡次失败的原因&#xff0c;提出了…

图像分割论文阅读:Automatic Polyp Segmentation via Multi-scale Subtraction Network

这篇论文的主要内容是介绍了一种名为多尺度差值网络&#xff08;MSNet&#xff09;的自动息肉分割方法。 1&#xff0c;模型整体结构 整体结构包括编码器&#xff0c;解码器&#xff0c;编码器和解码器之间是多尺度差值模块模块&#xff08;MSM&#xff09;&#xff0c;以及一…

使用Python实现ID3决策树中特征选择的先后顺序,字节跳动面试真题

def empty1(pri_data): hair [] #[‘长’, ‘短’, ‘短’, ‘长’, ‘短’, ‘短’, ‘长’, ‘长’] voice [] #[‘粗’, ‘粗’, ‘粗’, ‘细’, ‘细’, ‘粗’, ‘粗’, ‘粗’] sex [] #[‘男’, ‘男’, ‘男’, ‘女’, ‘女’, ‘女’, ‘女’, ‘女’] for o…

刷题日记——国家名称排序

7.国家名称排序 分析 一开始打算用二维的字符数组来操作&#xff0c;但是数组指针玩不太明白&#xff0c;于是改用结构体&#xff0c;结构体country里面仅一个成员name&#xff08;字符数组&#xff09;&#xff0c;这样就有两种解题方法&#xff1a; 方法一&#xff1a;使用…