线程的安全问题

news2024/11/29 0:35:21

目录

导言:

正文:

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关键字来确保共享变量的可见性。通过合理的设计和编码,可以有效地避免线程安全问题,确保多线程程序的正确性和稳定性。

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

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

相关文章

Vue——案例01(查询用户)

一、案例实现页面 二、案例实现效果 1. 查询效果 2. 年龄升序 3. 年龄降序 4. 原顺序 三、案例实现思路 1. 定义界面所需标签样式 <div id"app"><h2>查询用户:</h2><input type"text" placeholder"请输入名字"/><b…

Unity类银河恶魔城学习记录11-8 p110 Enemy modifiers源代码

Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释&#xff0c;可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili EnemyStat.cs using System.Collections; using System.Collections.Gener…

docker中配置交互式的JupyterLab环境的问题

【报错1】 Could not determine jupyter lab build status without nodejs 【解决措施】安装nodejs(利用conda进行安装/从官网下载进行安装&#xff09; 1、conda安装 conda install -c anaconda nodejs 安装后出现其他报错&#xff1a;Please install nodejs 5 and npm bef…

Vite为什么比Webpack快得多?

Vite为什么比Webpack快得多&#xff1f; 在前端开发中&#xff0c;构建工具扮演着至关重要的角色&#xff0c;而Vite和Webpack无疑是两个备受关注的工具。然而&#xff0c;众多开发者纷纷赞誉Vite的速度之快&#xff0c;本文将深入探讨Vite相较于Webpack为何更快的原因&#xf…

备战蓝桥杯---树学初步1

LCA&#xff08;最近公共祖先&#xff09; 定义&#xff1a;有根树的两个节点u,v&#xff0c;他们的LCA是一个节点x,其中x是他们的公共祖先并且X的深度尽可能大。 法1---Tarjan算法&#xff1a; 核心&#xff1a;DFS并查集 在并查集中建立仅有u的集合&#xff0c;设该集合祖…

每日一练 找无重复字符的最长子串

我们来看下这个题目&#xff0c;我们要统计的是不重复的子串&#xff0c;我们可以使用“滑动窗口法”&#xff0c;其实我们很容易就能想到思路。 我们的左窗代表我们目前遍历的开始&#xff0c;即我们遍历的子串的开头&#xff0c;右窗从左窗开始进行遍历&#xff0c;每次遍历…

安装部署MariaDB数据库管理系统

目录 一、初始化MariaDB服务 1、安装、启动数据库服务程序、将服务加入开机启动项中。 2、为保证数据库安全性和正常运转&#xff0c;需要对数据库程序进行初始化操作。 3、配置防火墙&#xff0c;放行对数据库服务程序的访问请求&#xff0c;允许管理员root能远程访问数据…

AIGC-Stable Diffusion发展及原理总结

目录 一. AIGC介绍 1. 介绍 2. AIGC商业化方向 3. AIGC是技术集合 4. AIGC发展三要素 4.1 数据 4.2 算力 4.3 算法 4.3.1 多模态模型CLIP 4.3.2 图像生成模型 二. Stable Diffusion 稳定扩散模型 1. 介绍 1.1 文生图功能&#xff08;Txt2Img) 1.2 图生图功能&…

Java复习第十二天学习笔记(JDBC),附有道云笔记链接

【有道云笔记】十二 3.28 JDBC https://note.youdao.com/s/HsgmqRMw 一、JDBC简介 面向接口编程 在JDBC里面Java这个公司只是提供了一套接口Connection、Statement、ResultSet&#xff0c;每个数据库厂商实现了这套接口&#xff0c;例如MySql公司实现了&#xff1a;MySql驱动…

python实现两个Excel表格数据对比、补充、交叉验证

业务背景 业务中需要用到类似企查查一类的数据平台进行数据导出&#xff0c;但企查查数据不一定精准&#xff0c;所以想采用另一个官方数据平台进行数据对比核验&#xff0c;企查查数据缺少的则补充&#xff0c;数据一致的保留企查查数据&#xff0c;不一致的进行颜色标注。 …

Jmeter 配置说明之线程组

一、线程组介绍&#xff1a; 线程组元件是任何一个测试计划的开始点。在一个测试计划中的所有元件都必须在某个线程组下。所有的任务都是基于线程组&#xff1a; 通俗理解&#xff1a; 线程组&#xff1a;就是一个线程组&#xff0c;里面有若干个请求&#xff1b; 线程&am…

ZNC3罗德与施瓦茨ZNC3网络分析仪

181/2461/8938产品概述&#xff1a; 罗德与施瓦茨 ZNC3 网络分析仪的工作频率范围为 9 kHz 至 3 GHz&#xff0c;面向移动无线电和电子产品行业的应用。它具有双向测试装置&#xff0c;用于测量有源和无源 DUT 的所有四个 S 参数。此外&#xff0c;它还提供适合开发和生产中各…

SOC子模块---存储器

存储器子系统简介 多层存储结构概念 该结构的核心是解决容量,速度,价格之间的矛盾,建立其的一种多层次存储架构;在金字塔越顶部其容量越小,价格越昂贵,但是其速度越快; Cache的出现主要解决了cpu 与主存之间速度的差异; 主存和辅存的层次结构解决了计算机对大容量,…

生态鱼缸智能养殖,系统架构与框图

功能&#xff1a;自动投食&#xff08;推拉式电磁铁&#xff09;&#xff0c;温度显示的改进&#xff08;传感器已经有了&#xff09;&#xff0c;控制灯光&#xff08;白天亮晚上灭&#xff09;&#xff0c;自动补养/水循环&#xff08;水氧监测太贵了&#xff0c;根据水温&am…

学习笔记】java项目—苍穹外卖day05

文章目录 苍穹外卖-day05课程内容1. Redis入门1.1 Redis简介1.2 Redis下载与安装1.2.1 Redis下载1.2.2 Redis安装 1.3 Redis服务启动与停止1.3.1 服务启动命令1.3.2 客户端连接命令1.3.3 修改Redis配置文件1.3.4 Redis客户端图形工具 2. Redis数据类型2.1 五种常用数据类型介绍…

containerd系统级学习大纲

文章目录 1 理解Containerd的背景和架构 &#x1f4da;1.1 历史背景 &#x1f570;️1.2 架构细节 &#x1f527; 2 设置开发环境 &#x1f6e0;️3 源码分析 &#x1f50d;4 深入核心功能4.1 容器生命周期管理4.2 镜像管理4.3 网络管理4.4 存储与快照4.5 插件系统 5 贡献代码6…

2024/3/29打卡 填充——贪心

目录 题目 思路 代码 先来说下什么时候使用贪心和动态规划&#xff1a; 一个题目当寻找答案的过程中有大约 2的指数级&#xff08; 2^n&#xff09;个方案的时候&#xff0c;可以考虑用贪心和动态规划问题&#xff08;其实&#xff0c;我现在还不知道什么时候用 dfs 和 动态…

投稿指南【NO.15】SCI期刊推荐

写在前面&#xff1a;SCI论文发表是我国高校机构与基地评估、成果评奖、项目立项、人才培养等方面的重要考核指标&#xff0c;更不要要说关系到我们个人的毕业、晋升和前途。SCI含金量之高&#xff0c;要是读研读博科研工作期间能发表&#xff0c;将对直博、找工作、国奖申请、…

node.js的常用命令

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

c++----list模拟实现

目录 1. list的基本介绍 2. list的基本使用 2.1 list的构造 用法示例 2.2 list迭代器 用法示例 2.3. list容量&#xff08;capacity&#xff09;与访问&#xff08;access) 用法示例 2.4 list modifiers 用法示例 2.5 list的迭代器失效 3.list的模拟实现 3.1…