深入理解线程安全

简介: 在多线程编程中,线程安全是一个至关重要的概念。线程安全可能到导致数据不一致,应用程序崩溃和其他不可预测的后果。本文将深入探讨线程安全问题的根本原因,并通过Java代码示例演示如何解决这些问题。

引言:

在多线程编程中,线程安全是一个至关重要的概念。线程安全可能到导致数据不一致,应用程序崩溃和其他不可预测的后果。本文将深入探讨线程安全问题的根本原因,并通过Java代码示例演示如何解决这些问题。


线程安全的根本原因

这里先观察一个线程不安全的例子:

public class Test {
    private static int count;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

这里我们想要得到 count 的值为200,但是运行的结果却不是。


这里的 count++ 实际操作分为3步:


1. load把数据从内存,读到 cpu 寄存器中(可能先t1,也可能先t2)。


2. add 将寄存器中的数据进行 +1。


3. save 把寄存器中的数据,保存到内存中 。


因为线程的调度是随机的,所以在 count++ 时会出现下面等很多不同的结果

739daeee17787ea1baf189d1a3b6a47d_3519e022063d4e1ba11ecabeb5222f12.png



针对以上我们可以知道产生线程安全的原因:


1. 操作系统中,线程的调度是随机的


2. 两个线程针对同一个变量进行修改


3. 修改操作,不是原子性的


   此处count++是 非原子 的操作(先读,再修改)


4. 内存可见性问题


解决线程安全

使用synchronized关键字可以将代码块或方法标记为同步的,这样只有一个线程可以访问它。这可以防止多个线程同时访问共享数据。


public class Test {
    private static int count;
    private static Object locker = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                for (int i = 0; i < 100; i++) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                for (int i = 0; i < 100; i++) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

这串代码,对其进行 synchroniezd 加锁后,答案就能是200了

synchronized 的特性

1. 互斥性

2.可重入性

互斥性

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待。


进入 synchronized 修饰的代码块, 相当于 加锁


退出 synchronized 修饰的代码块, 相当于 解锁


可重入性

在下面的代码中,increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前 对象加锁的. 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释 放, 相当于连续加两次锁)

class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
    }
    synchronized void increase2() {
        increase();
    }
}

死锁

哲学家就餐问题:


假设有几个哲学家,围着一个桌子(上有一盘菜),每两个人之间有1个筷子,哲学家吃饭的的时间是随机的,吃饭的时候会拿起左右两边的筷子(先拿左边的,没拿起右手的不会放下左手的),正常情况下是可以吃的,但是如果同时拿起左边的筷子,就 " 卡 "到这了 。

f49f912a4196023f40a02dd48f7784ca_340a809d32d84d8bbef73bf7e7f99d85.png


死锁的原因

1. 互斥使用,当一个线程持有一把锁时,另一个也想要得到锁,就必须阻塞等待(锁的基本特性)


2. 不可抢占,当锁已经被线程1拿到后,线程2只能等待1主动释放(锁的基本特性)


3. 请求保持,一个线程尝试获取多把锁(代码结构:下面例子)

4fd745fce696365ef3b8826d10d02b2a_b74c2cd1ce5d44509de0eb4833661084.png



4. 循环等待(代码结构:哲学家就餐问题)


解决死锁

所以解决死锁1,和 2是锁的本质不能改变


我们只能调整:


对于3来说,改变代码结构


对于4来说,约定加锁的顺序


volatile关键字

1. 保存内存可见性


2. 不保证原子性


保存内存可见性

讨论内存可见性的话,就不得不谈论一下下面的知识点:


计算机cpu访问数据(存在内存中)的时候,会先读出来,放到寄存器中,再进行运算。cpu读内存的操作相对于其他操作来说是比较慢的。所以编译器为了提高效率,把本来要读内存的操作,优化成了读寄存器,减少了读内存的操作。eg:

public class Demo17 {
    private static volatile int isQuit = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (isQuit == 0) {
                // 循环体里啥都没干.
                // 此时意味着这个循环, 一秒钟就会执行很多很多次.
            }
            System.out.println("t1 退出!");
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            System.out.println("请输入 isQuit: ");
            Scanner scanner = new Scanner(System.in);
            // 一旦用户输入的值, 不为 0, 此时就会使 t1 线程执行结束.
            isQuit = scanner.nextInt();
        });
        t2.start();
    }
}

这里即使你输入的不是0,也会一直循环!!!


因此为解决这个问题,就可以在变量的前面加上volatile!!!


目录
相关文章
|
Unix Shell Linux
如何使用 Awk 打印文件中的字段和列
如何使用 Awk 打印文件中的字段和列
|
7月前
|
存储 SQL 算法
阿里面试:每天新增100w订单,如何的分库分表?这份答案让我当场拿了offer
例如,在一个有 10 个节点的系统中,增加一个新节点,只会影响到该新节点在哈希环上相邻的部分数据,其他大部分数据仍然可以保持在原节点,大大减少了数据迁移的工作量和对系统的影响。狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由”。在 3 - 5 年的中期阶段,随着业务的稳定发展和市场份额的进一步扩大,订单数据的增长速度可能会有所放缓,但仍然会保持在每年 20% - 30% 的水平。
阿里面试:每天新增100w订单,如何的分库分表?这份答案让我当场拿了offer
|
7月前
|
人工智能
云工开物合作动态丨中央美术学院与阿里云签约,推动人工智能和艺术与设计学科融合发展
2024年12月8日,中央美术学院与阿里云在厦门签署合作协议,双方将结合艺术与技术优势,在人工智能与艺术交叉学科的课程共建、学生实践等方面展开合作。阿里云通过“云工开物”计划提供算力资源和PAI ArtLab平台,助力师生高效创作,推动艺术与设计类人才培养新模式的探索。
|
9月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
252 5
Java 并发编程——volatile 关键字解析
|
8月前
|
安全 Android开发 数据安全/隐私保护
《鸿蒙Next原生应用的独特用户体验之旅》
鸿蒙Next在界面设计、操作逻辑、动效体验等方面与iOS类似,强调简洁一致性,悬浮效果提升空间感。其操作便捷,动效流畅,性能优化使流畅度提升30%,媲美iOS。智能交互方面,鸿蒙Next的小艺助手和跨设备互联功能表现出色,支持识屏对话等深度交互。安全隐私保护机制细致,应用体积小,节省流量和存储空间。相比安卓和iOS,鸿蒙Next在用户体验上展现出独特优势,为用户带来更优质、便捷和安全的使用感受。
523 9
|
安全 机器人 测试技术
宇树Unitree Z1机械臂使用教程
本文是宇树Unitree Z1机械臂的使用教程,包括建立机械臂通信、基本运行demo、ROS Gazebo仿真demo、键盘控制demo、手柄控制demo、moveit真实机械臂demo以及其他高级控制demo的详细步骤和注意事项。教程涵盖了软件安装、环境配置、代码下载、编译运行等内容,并提供了机械臂操作的实用技巧。
1280 1
|
9月前
|
缓存 自然语言处理 API
Ascend推理组件MindIE LLM
MindIE LLM是基于昇腾硬件的大语言模型推理组件,提供高性能的多并发请求调度与优化技术,如Continuous Batching、PageAttention等,支持Python和C++ API,适用于高效能推理需求。其架构包括深度定制优化的模型模块、文本生成器和任务调度管理器,支持多种模型框架和量化方式,旨在提升大规模语言模型的推理效率和性能。
|
10月前
|
编解码 vr&ar 图形学
Unity下如何实现低延迟的全景RTMP|RTSP流渲染
随着虚拟现实技术的发展,全景视频逐渐成为新的媒体形式。本文详细介绍了如何在Unity中实现低延迟的全景RTMP或RTSP流渲染,包括环境准备、引入依赖、初始化客户端、解码与渲染、优化低延迟等步骤,并提供了具体的代码示例。适用于远程教育、虚拟旅游等实时交互场景。
274 2
|
10月前
|
监控 网络协议 安全
员工网络监控软件:PowerShell 在网络监控自动化中的应用
在数字化办公环境中,企业对员工网络活动的监控需求日益增长。PowerShell 作为一种强大的脚本语言,能够有效实现员工网络监控自动化。本文介绍了如何使用 PowerShell 获取网络连接信息、监控特定网址的访问情况,并生成自动化报告,帮助企业高效管理员工网络活动,确保网络安全和合规性。
232 0
|
XML 缓存 自然语言处理
项目中常用到的缓存中间件场景
数据缓存是指数据库查询缓存,每次访问页面的时候,都会先检测相应的缓存数据是否存在,如果不存在,就连接数据库,得到数据,并把查询结果序列化后保存到文件中,以后同样的查询结果就直接从缓存表或文件中获得。
527 90