求助 Java 大佬 synchronized 的问题

2022-02-26 08:25:34 +08:00
 gosidealone

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 ,这是为什么呢?

4471 次点击
所在节点    Java
33 条回复
ingin
2022-02-26 15:04:39 +08:00
@gosidealone #18 在同一个对象的情况下,你给出的结果:1 ,2 或者 2 ,1 或者 2 ,2 是怎么来的?我总觉得还有 1 ,1 这种情况,理由:i++不是原子操作
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
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
2022-02-26 16:07:32 +08:00
@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
2022-02-26 16:16:47 +08:00
输出 1,1 也是有可能的,i 没有保证线程可见性,i++ 也不是原子操作。
fly2mars
2022-02-26 16:18:20 +08:00
@JasonLaw 就是在这个并没有使用同一个 lock 场景下,
从结果上来看,21 和 22 都有,那"线程 1 看到线程 2 所做的改变"就是不确定的啊,
想了解下线程 1 是否决定去感知线程 2 的变化的,有无可量化的指标?或者有无关键字可去查询下
gosidealone
2022-02-26 20:50:26 +08:00
@ingin 额 我现在只跑得出 2 ,2 这种结果了
teem
2022-02-26 21:16:09 +08:00
看 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
2022-02-26 21:21:06 +08:00
#28 再添一个 getstatic (操作 G )可能更好理解一点
fly2mars
2022-02-26 22:22:51 +08:00
@teem 很清晰,请看你列出的 6 种情况中的 3,6 这两种步骤都是一样的,但为啥结果不一样呢(22 和 21)
teem
2022-02-26 23:33:57 +08:00
@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
2022-02-26 23:46:19 +08:00
更正 #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
2022-02-28 14:06:41 +08:00
你这都两对象了,, 加锁还有意义吗? 又不是静态方法

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/836521

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX