• 一次学习引发我对于 synchronized 的再理解


    背景

    我最近在学习 Java 并发编程,正好学习到 synchronized 锁这一块。在学习过程中由于对问题理解不够透彻产生了偏差,经过思考之后终于捋顺了,思考的过程可能有一些参考意义,希望能给大家一些启发。

    线程安全问题的例子

    话不多说,我们先看一段代码:

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

    简单介绍一下代码的逻辑,在主线程中开启两个线程对类的成员变量 count 分别进行自增和自减操作,等待两个线程都执行完毕,最后输出 count 的值。
    在不考虑并发的情况下,由于自增和自减的次数相同,最后的输出结果会是 0 。但是实际的执行结果却有以下三种可能 0、正数、负数。没错 0 的情况也是有可能出现的,不过概率很低。
    我们简单分析一下,count 的自增或自减操作不是一步完成的,而是分成好几步:

    1. 先获取到 count 的值,记做 x
    2. 然后进行自增(x+1)或者自减 (x-1),记为 y
    3. 最后将 y 回写到 count 中

    当两个线程同时对 count 进行操作时,有可能发生以下情况:

    1. 线程 A 获取到 count 的值为 x
    2. 线程 B 也获取到 count 的值为 x
    3. 线程 A 执行 x+1
    4. 线程 B 执行 x -1
    5. 线程 A 将 x+1 回写到 count 中
    6. 线程 B 将 x-1 回写到 count 中

    本来正常操作时 A 线程先读取 x 然后操作完 ,将 y 写回到 count 中。此时 B 线程再读取 count 的值 y,操作之后写会 count 中。结果经过上面的操作,线程 A,B 的两次操作,只有最后回写的线程(B)生效了,A的操作相当于作废了。因此对于多个线程同时操作共享资源,很容易出现线程安全问题。

    解决线程安全问题

    为了解决上面的问题我们需要对共享资源加锁,于是乎就有了下面的代码:

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

    我们利用 synchronized 对 count 的自增或者自减操作进行加锁,这样最后的结果就会和我们预想的一样为 0 了。我大概说一下其中的原理,这里 synchronizd 的作用就是给代码块里面的代码加上一把锁,这样就保证了 count++ 或者是 count-- 是一个完整的操作,也就是具有原子性(在很长的时期,原子都被认为是不可分的最小微观粒子,所以原子性就是整体性的意思)。学到这里的时候,我其实是处于一知半解,但是以为自己懂了的状态,模糊地觉得原子性嘛说明 count – 和 count++ 在执行的中途不会插入其他操作,也就不会出现线程安全问题了。

    能这么简单理解吗?

    先问个问题,如果只对 count++ 或者是 **count-- 加锁,会出现线程安全问题吗?
    显然会?为啥?因为如果加一个就能解决的话为啥要加两个?(ps:哈哈哈哈,整个活)
    我们正经分析一下,如果只对 count++ 加锁,两个线程同时运行,线程 A 在执行 count++ 的时候,由于 count-- 没有加锁,线程 B 还是可以执行
    count-- **,只要两个线程同时执行,就会出现线程安全问题,也就是互相覆盖的情况。
    我当时在疑惑什么呢?我在想, 给 count++ 加上 synchronized 关键字以后 count++ 就具有原子性了,原子性就代表中间不会存在其他操作,所以加上一个是不是也行?
    显然我的理解是有问题的,首先加锁并不等同于原子性,为什么这么说?举个例子:

       synchronized (Test1.class){
           count++;
       }
    
    • 1
    • 2
    • 3

    虽然多个线程执行上面的代码是一个线程一个线程去执行的,是原子性的,但是并不是说 count++ 这个操作就变成原子性的了,只是这段被 synchronized 包裹的代码是原子性的,多个线程不能同时执行这段代码,但是可以同时执行别的代码,就比如说 count++ 和 count-- ,如果只对其中一个加锁,那么他们就可以可以同时执行。
    其次,给操作共享资源的代码块加锁,并不等于给资源加锁。对于 count 这个资源,我只给 count++ 加锁并不能阻止其他的线程去同时运行 count–,所以说只给 count ++ 加锁是没有用的,必须要同时给两个操作都加锁,并且锁对象必须是一个。

    总结

    synchronized 实现原子性的原理是通过给同一个对象加锁,在多线程并发执行的情况下,都要先去同一个对象哪里先获取锁,然后才能执行 synchronized 代码块中的代码,由于同时只能有一个线程来获取到锁,所以同一时间只有一个线程执行代码块中的代码,保证了代码块中的代码是原子性的。但是对于共享资源来说,要想共享资源的线程安全,就需要保证所有对于共享资源的操作的原子性,则需要将所有对于共享资源的操作加上同一把锁,也就是如示例中的,在对 count++ 和 count-- 加锁时也要保证锁对象(Test1.class)是同一个。

  • 相关阅读:
    nacos协议
    Java集合List报错,java.lang.UnsupportedOperationException
    R语言确定聚类的最佳簇数:3种聚类优化方法
    osg学习-1《绘制基本单元》
    基于Matlab使用雷达资源管理有效跟踪多个机动目标仿真(附源码)
    2023华侨大学计算机考研信息汇总
    Python基础(二)
    “数智+绿色”驱动,宏工科技助力线缆线材稳定高品质生产
    Thread的使用、线程的几个重要操作和状态【JavaEE初阶】
    linux--系统文件I/O
  • 原文地址:https://blog.csdn.net/lzh_jk1b_xyxy/article/details/136222131