求助 Java 大佬 synchronized 的问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
gosidealone
V2EX    Java

求助 Java 大佬 synchronized 的问题

  •  
  •   gosidealone 2022-02-26 08:25:34 +08:00 4863 次点击
    这是一个创建于 1328 天前的主题,其中的信息可能已经有所发展或是发生改变。

    synchronized 加在方法上锁的是对象的实例吗?

    public class Test { public static void main(String[] args) { //Main main = new Main(); new Thread(()->{ Main main = new Main(); main.get(); }).start(); new Thread(()->{ Main main = new Main(); main.get(); }).start(); } } class Main{ private static int i = 0; public synchronized void get(){ i++; System.out.println(i); } } 

    这两个线程执行 get 函数的时候会互斥吗?如果是同一个 Main 对象肯定是输出 1 ,2 ,如果不是同一个对象输出的是 2 ,2 或者 1 ,2 或者 2 ,1 ,这是为什么呢?

    33 条回复    2022-02-28 14:06:41 +08:00
    wangyu17455
        1
    wangyu17455  
       2022-02-26 08:45:10 +08:00 via Android
    会互斥,所以肯定输出 1 ,2 ,加了 sync 关键字同一时刻只有一个线程能执行这个方法,这是互斥性,方法结束所有的变量被写回主内存,这是可见性
    dcsuibian
        2
    dcsuibian  
       2022-02-26 08:47:20 +08:00
    不太记得了,每个对象本身应该是有一个锁的。
    比如
    synchronized (this){
    }
    这种写法,方法上的 synchronized 应该是语法糖
    但针对 synchronized static 这种写法,那个锁对象就是类的 class 实例
    MakHoCheung
        3
    MakHoCheung  
       2022-02-26 08:48:39 +08:00
    不会互斥,都两个 Main 对象了
    wangyu17455
        4
    wangyu17455  
       2022-02-26 08:59:29 +08:00 via Android
    我瞎了,没看见是两个对象,一楼当我没说
    Blanke
        5
    Blanke  
       2022-02-26 09:04:37 +08:00   5
    1. 在 get 方法加锁,锁住的是实例对象,因为 get 方法不是 static
    2. 两个线程里如果是同一个 Main 对象,第一个线程会先拿到锁,所以输出会是 1,2 不变
    3. 两个线程里如果不是同一个 Main 对象,因为是实例锁,两线程不会互斥。如果 i 不是 static ,那么输出都会是 1 ,这里 i 是 static ,所以两个线程并发的时候,都可能先执行 i++,和输出 i ,所以结果可能是 1,2 、2,2 、2,1 三种情况。
    具体说明 3 中的输出顺序:
    ( 1 )输出 1 ,2
    线程 1: 执行 i++;
    线程 1: 输出 i ,也就是 1 ;
    线程 2: 执行 i++;
    线程 2: 输出 i ,也就是 2 ;
    ( 2 )输出 2 ,2
    线程 1: 执行 i++;
    线程 2: 执行 i++;
    线程 1: 输出 i ,也就是 2 ;
    线程 2: 输出 i ,也就是 2 ;
    ( 3 )输出 2 ,1
    线程 2: 执行 i++;
    线程 2: 输出 i ,也就是 1 ;
    线程 1: 执行 i++;
    线程 1: 输出 i ,也就是 2 ;

    写的不对的地方请指正
    cxshun
        6
    cxshun  
       2022-02-26 09:11:00 +08:00
    不会互斥,首先你的 synchronized 是加在实例方法上面,那么就只有同一个对象的才会被锁住,你这里是两个不同的 main 实例,完全没啥关系。

    而至于输出 1,2 或者 2 ,1 或者 2 ,2 是因为原子性和可见性的问题,你可以尝试把 i 的类型换成 AtomicInteger 就可以实现你想要的效果了。当然前提还是同一个实例对象
    sutra
        7
    sutra  
       2022-02-26 09:49:41 +08:00 via iPhone
    上述讨论问题时注意被注释掉的那行代码,提问者可能再问被注释掉那行代码启用后会如何,而对于阅读者,那行会自动无视。
    gosidealone
        8
    gosidealone  
    OP
       2022-02-26 10:20:54 +08:00 via iPhone
    @sutra 那倒没有 真的就是没有被修饰的情况,放在那里只是为了对比
    gosidealone
        9
    gosidealone  
    OP
       2022-02-26 10:21:36 +08:00 via iPhone
    @Blanke
    @MakHoCheung
    @wangyu17455
    谢谢大家的回复
    shadow1949
        10
    shadow1949  
       2022-02-26 10:57:01 +08:00
    @Blanke
    还有可能是 1 ,1
    aviator
        11
    aviator  
       2022-02-26 11:10:51 +08:00
    @wangyu17455 建议捐献 /dog
    lueffy
        12
    lueffy  
       2022-02-26 11:19:08 +08:00
    不会互斥, 正好最近在看极客时间专栏 [Java 并发编程实战]
    推荐看 3|互斥锁(上):解决原子性问题, 4|互斥锁(下):如何用一把锁保护多个资源
    应该可以免费阅读 , 这两篇比较详细地介绍了 synchronized
    ryanbuu
        13
    ryanbuu  
       2022-02-26 11:33:38 +08:00 via iPhone
    可以在方法上加 static 或者在代码块中使用 synchronized (Main.class) {} 锁类哈~
    ershierdu
        14
    ershierdu  
       2022-02-26 12:22:05 +08:00
    最近在准备春招,这个也算是八股文里的经典题目了(虽然我是前几天才知道的。。)
    fly2mars
        15
    fly2mars  
       2022-02-26 13:14:37 +08:00
    2,1 是怎么来的,只要有 1 个线程的 i 变为 2 输出后,另一个线程只能输出 2 啊
    fanxasy
        16
    fanxasy  
       2022-02-26 13:34:25 +08:00
    @fly2mars 原始值是 0..
    jeffxjh
        17
    jeffxjh  
       2022-02-26 13:39:30 +08:00
    跑了一下这段代码 2,2
    gosidealone
        18
    gosidealone  
    OP
       2022-02-26 14:47:03 +08:00
    @jeffxjh 多跑几次
    JasonLaw
        19
    JasonLaw  
       2022-02-26 14:49:42 +08:00
    @Blanke #5
    @fly2mars #15

    Q:为什么会出现先输出 2 ,再输出 1 呢?
    A:Main.i 是共享可变状态,但是两个线程所使用的 lock 是不一样的。因此会出现“线程 1 先执行了 i++,线程 2 也看到了线程 1 所做的改变,线程 2 此时所看见的 i 是 1 ,然后线程 2 执行了 i++和 System.out.println(i),先输出了 2 。但是线程 1 并没有看见线程 2 所做的改变,它所看见的 i 还是 1 ,然后执行 System.out.println(i)输出了 1”这种情况。
    fly2mars
        20
    fly2mars  
       2022-02-26 14:57:56 +08:00
    @JasonLaw
    那当线程 1 看见了线程 2 所做的改变,所以输出 2,2 了是吧

    请问线程 1 有没有看见线程 2 的改变,是在 System.out.println(i)这步决定的吗
    ingin
        21
    ingin  
       2022-02-26 15:04:39 +08:00
    @gosidealone #18 在同一个对象的情况下,你给出的结果:1 ,2 或者 2 ,1 或者 2 ,2 是怎么来的?我总觉得还有 1 ,1 这种情况,理由:i++不是原子操作
    JasonLaw
        22
    JasonLaw  
       2022-02-26 15:19:08 +08:00
    @fly2mars #20

    Q:那当线程 1 看见了线程 2 所做的改变,所以输出 2,2 了是吧
    A:对

    Q:线程 1 有没有看见线程 2 的改变,是在 System.out.println(i)这步决定的吗
    A:也不能说是 System.out.println(i)决定了是否看见别的线程所做的改变。因为两个线程所使用的 lock 不是同一个,也就没有不能保证这个线程是否能够看到另外个线程所做的改变。更多细节可以看一下 https://stackoverflow.com/questions/16213443/instruction-reordering-happens-before-relationship
    fly2mars
        23
    fly2mars  
       2022-02-26 15:41:21 +08:00
    @JasonLaw 看了你的连接,是我没表达清楚,再问下
    因 i 是个共享的变量,那当线程 2 此时的 i 已经是 2,输出也是 2 时.

    线程 1 此时的 i
    1.看见线程 2 所做的改变,输出是 2.结果 2,2
    2.还没看见线程 2 所做的改变,输出还是 1,结果 2,1

    问题:线程 1 是否看见线程 2 所做的改变,即是否会读取共享的变量 i,是随机的吗?如果不是随机的,线程 1 如何或何时决定是否去读取共享的变量的
    JasonLaw
        24
    JasonLaw  
       2022-02-26 16:07:32 +08:00 via iPhone
    @fly2mars #23 不是随机的,只能说 Java 不会保证“线程 1 看到线程 2 所做的改变”。如果想让线程 1 看到线程 2 所做的改变,都使用同一个 lock 就行了。“ Monitor lock rule. An unlock on a monitor lock happens before every subsequent lock on that same monitor lock.”就可以保证。
    blackboom
        25
    blackboom  
       2022-02-26 16:16:47 +08:00
    输出 1,1 也是有可能的,i 没有保证线程可见性,i++ 也不是原子操作。
    fly2mars
        26
    fly2mars  
       2022-02-26 16:18:20 +08:00
    @JasonLaw 就是在这个并没有使用同一个 lock 场景下,
    从结果上来看,21 和 22 都有,那"线程 1 看到线程 2 所做的改变"就是不确定的啊,
    想了解下线程 1 是否决定去感知线程 2 的变化的,有无可量化的指标?或者有无关键字可去查询下
    gosidealone
        27
    gosidealone  
    OP
       2022-02-26 20:50:26 +08:00
    @ingin 额 我现在只跑得出 2 ,2 这种结果了
    teem
        28
    teem  
       2022-02-26 21:16:09 +08:00   2
    看 Main.class 字节码很清晰:

    ~ % javap -c Main.class
    Compiled from "Test.java"
    class com.test.sync.Main {
    com.test.sync.Main();
    Code:
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return

    public synchronized void get();
    Code:
    0: getstatic #2 // Field i:I
    3: iconst_1
    4: iadd
    5: putstatic #2 // Field i:I
    8: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
    11: getstatic #2 // Field i:I
    14: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
    17: return

    static {};
    Code:
    0: iconst_0
    1: putstatic #2 // Field i:I
    4: return
    }

    注意 i++ 操作非原子操作,先定义 iconst_x (操作 C ),再计算 iadd (操作 A ),再赋值 putstatic (操作 P ),这是 3 部操作。再加上打印操作 PRINT ,把两个线程 4 个操作步骤互相穿插,逻辑上来讲是可能出现 4 种结果 和 6 种情况:
    设两个线程分别为「线程 1 」 和「线程 2 」,逻辑上来讲是可能出现 6 种情况:

    1 、C1 、A1 、P1 、PRINT1 、C2 、A2 、P2 、PRINT2 ,结果:1 2
    2 、C1 、A1 、P1 、C2 、A2 、P2 、PRINT1 、PRINT2 ,结果:2 2
    3 、C1 、A1 、P1 、C2 、A2 、P2 、PRINT2 、PRINT1 ,结果:2 2
    4 、C1 、C2 、A1 、A2 、P1 、P2 、PRINT1 、PRINT2 ,结果:1 1
    5 、C1 、C2 、A1 、A2 、P1 、P2 、PRINT2 、PRINT1 ,结果:1 1
    6 、C1 、A1 、P1 、C2 、A2 、P2 、PRINT2 、PRINT1 ,结果:2 1

    总结结果 4 种:
    1 2
    2 2
    1 1
    2 1

    若理解有误请指正,感谢。
    teem
        29
    teem  
       2022-02-26 21:21:06 +08:00
    #28 再添一个 getstatic (操作 G )可能更好理解一点
    fly2mars
        30
    fly2mars  
       2022-02-26 22:22:51 +08:00
    @teem 很清晰,请看你列出的 6 种情况中的 3,6 这两种步骤都是一样的,但为啥结果不一样呢(22 和 21)
    teem
        31
    teem  
       2022-02-26 23:33:57 +08:00   1
    @fly2mars 再添一个 getstatic (操作 G )可能更好理解一点:
    3 、C1 、A1 、P1 、C2 、A2 、P2 、G1 、G2 、PRINT2 、PRINT1 ,结果:2 2
    6 、C1 、A1 、P1 、C2 、A2 、P2 、PRINT2 、G2 、PRINT1 、G1 ,结果:2 1
    teem
        32
    teem  
       2022-02-26 23:46:19 +08:00   1
    更正 #31
    3 、C1 、A1 、P1 、C2 、A2 、P2 、G1 、G2 、PRINT2 、PRINT1 ,结果:2 2
    6 、C1 、A1 、P1 、C2 、A2 、P2 、G2 、PRINT2 、PRINT1 、G1 ,结果:2 1
    Joker123456789
        33
    Joker123456789  
       2022-02-28 14:06:41 +08:00
    你这都两对象了,, 加锁还有意义吗? 又不是静态方法
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2886 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 14:10 PVG 22:10 LAX 07:10 JFK 10:10
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86