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

举报
ruogu994 发表于 2024/02/22 14:54:29 2024/02/22
【摘要】 背景我最近在学习 Java 并发编程,正好学习到 synchronized 锁这一块。在学习过程中由于对问题理解不够透彻产生了偏差,经过思考之后终于捋顺了,思考的过程可能有一些参考意义,希望能给大家一些启发。 线程安全问题的例子话不多说,我们先看一段代码:``public class Test1 {static int count = 0;public static void main(S...

背景

我最近在学习 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);
}

}
``

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

先获取到 count 的值,记做 x
然后进行自增(x+1)或者自减 (x-1),记为 y
最后将 y 回写到 count 中
当两个线程同时对 count 进行操作时,有可能发生以下情况:

线程 A 获取到 count 的值为 x
线程 B 也获取到 count 的值为 x
线程 A 执行 x+1
线程 B 执行 x -1
线程 A 将 x+1 回写到 count 中
线程 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);
}

}
`
我们利用 synchronized 对 count 的自增或者自减操作进行加锁,这样最后的结果就会和我们预想的一样为 0 了。我大概说一下其中的原理,这里 synchronizd 的作用就是给代码块里面的代码加上一把锁,这样就保证了 count++ 或者是 count-- 是一个完整的操作,也就是具有原子性(在很长的时期,原子都被认为是不可分的最小微观粒子,所以原子性就是整体性的意思)。学到这里的时候,我其实是处于一知半解,但是以为自己懂了的状态,模糊地觉得原子性嘛说明 count – 和 count++ 在执行的中途不会插入其他操作,也就不会出现线程安全问题了。
能这么简单理解吗?
先问个问题,如果只对 count++ 或者是 **count-- 加锁,会出现线程安全问题吗?
显然会?为啥?因为如果加一个就能解决的话为啥要加两个?(ps:哈哈哈哈,整个活)
我们正经分析一下,如果只对 count++ 加锁,两个线程同时运行,线程 A 在执行 count++ 的时候,由于 count-- 没有加锁,线程 B 还是可以执行 count-- **,只要两个线程同时执行,就会出现线程安全问题,也就是互相覆盖的情况。
我当时在疑惑什么呢?我在想, 给 count++ 加上 synchronized 关键字以后 count++ 就具有原子性了,原子性就代表中间不会存在其他操作,所以加上一个是不是也行?
显然我的理解是有问题的,首先加锁并不等同于原子性,为什么这么说?举个例子:

synchronized (Test1.class){
count++;
}

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

总结

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

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。