V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
gosidealone
V2EX  ›  Java

求助 Java 大佬 synchronized 的问题

  •  
  •   gosidealone · 169 天前 · 3577 次点击
    这是一个创建于 169 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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  
       169 天前 via Android
    会互斥,所以肯定输出 1 ,2 ,加了 sync 关键字同一时刻只有一个线程能执行这个方法,这是互斥性,方法结束所有的变量被写回主内存,这是可见性
    dcsuibian
        2
    dcsuibian  
       169 天前
    不太记得了,每个对象本身应该是有一个锁的。
    比如
    synchronized (this){
    }
    这种写法,方法上的 synchronized 应该是语法糖
    但针对 synchronized static 这种写法,那个锁对象就是类的 class 实例
    MakHoCheung
        3
    MakHoCheung  
       169 天前
    不会互斥,都两个 Main 对象了
    wangyu17455
        4
    wangyu17455  
       169 天前 via Android
    我瞎了,没看见是两个对象,一楼当我没说
    Blanke
        5
    Blanke  
       169 天前   ❤️ 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  
       169 天前
    不会互斥,首先你的 synchronized 是加在实例方法上面,那么就只有同一个对象的才会被锁住,你这里是两个不同的 main 实例,完全没啥关系。

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

    请问线程 1 有没有看见线程 2 的改变,是在 System.out.println(i)这步决定的吗
    ingin
        21
    ingin  
       168 天前
    @gosidealone #18 在同一个对象的情况下,你给出的结果:1 ,2 或者 2 ,1 或者 2 ,2 是怎么来的?我总觉得还有 1 ,1 这种情况,理由:i++不是原子操作
    JasonLaw
        22
    JasonLaw  
       168 天前
    @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  
       168 天前
    @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  
       168 天前 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  
       168 天前
    输出 1,1 也是有可能的,i 没有保证线程可见性,i++ 也不是原子操作。
    fly2mars
        26
    fly2mars  
       168 天前
    @JasonLaw 就是在这个并没有使用同一个 lock 场景下,
    从结果上来看,21 和 22 都有,那"线程 1 看到线程 2 所做的改变"就是不确定的啊,
    想了解下线程 1 是否决定去感知线程 2 的变化的,有无可量化的指标?或者有无关键字可去查询下
    gosidealone
        27
    gosidealone  
    OP
       168 天前
    @ingin 额 我现在只跑得出 2 ,2 这种结果了
    teem
        28
    teem  
       168 天前   ❤️ 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  
       168 天前
    #28 再添一个 getstatic (操作 G )可能更好理解一点
    fly2mars
        30
    fly2mars  
       168 天前
    @teem 很清晰,请看你列出的 6 种情况中的 3,6 这两种步骤都是一样的,但为啥结果不一样呢(22 和 21)
    teem
        31
    teem  
       168 天前   ❤️ 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  
       168 天前   ❤️ 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  
       166 天前
    你这都两对象了,, 加锁还有意义吗? 又不是静态方法
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2309 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 03:21 · PVG 11:21 · LAX 20:21 · JFK 23:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.