• 深入理解线程安全



    引言:

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


    线程安全的根本原因

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

    1. public class Test {
    2. private static int count;
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t1 = new Thread(() -> {
    5. for (int i = 0; i < 100; i++) {
    6. count++;
    7. }
    8. });
    9. Thread t2 = new Thread(() -> {
    10. for (int i = 0; i < 100; i++) {
    11. count++;
    12. }
    13. });
    14. t1.start();
    15. t2.start();
    16. t1.join();
    17. t2.join();
    18. System.out.println(count);
    19. }
    20. }

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

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

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

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

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

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

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

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

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

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

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

    4. 内存可见性问题

    解决线程安全

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

    1. public class Test {
    2. private static int count;
    3. private static Object locker = new Object();
    4. public static void main(String[] args) throws InterruptedException {
    5. Thread t1 = new Thread(() -> {
    6. synchronized (locker) {
    7. for (int i = 0; i < 100; i++) {
    8. count++;
    9. }
    10. }
    11. });
    12. Thread t2 = new Thread(() -> {
    13. synchronized (locker) {
    14. for (int i = 0; i < 100; i++) {
    15. count++;
    16. }
    17. }
    18. });
    19. t1.start();
    20. t2.start();
    21. t1.join();
    22. t2.join();
    23. System.out.println(count);
    24. }
    25. }

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

    synchronized 的特性

    1. 互斥性

    2.可重入性

    互斥性

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

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

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

    可重入性

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

    1. class Counter {
    2. public int count = 0;
    3. synchronized void increase() {
    4. count++;
    5. }
    6. synchronized void increase2() {
    7. increase();
    8. }
    9. }

    死锁

    哲学家就餐问题:

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

    死锁的原因

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

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

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

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

    解决死锁

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

    我们只能调整:

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

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

    volatile关键字

    1. 保存内存可见性

    2. 不保证原子性

    保存内存可见性

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

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

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

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

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

  • 相关阅读:
    人工神经网络算法的应用,人工神经网络发展历史
    Java异常、继承结构、处理异常、自定义异常、SpringBoot中全局捕获处理异常
    解放你的双手----WIN7设置自动化任务
    Js手写面试题5-Promise
    Stable Diffusion科普文章【附升级gpt4.0秘笈】
    SpringCloud原理-OpenFeign篇(三、FeignClient的动态代理原理)
    解决idea启动tomcat控制台中文乱码
    Goland设置头注释
    前端导出下载文件后提示无法打开文件
    阿里云消息队列 Kafka-消息检索实践
  • 原文地址:https://blog.csdn.net/llt2997632602/article/details/132941459