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 ,这是为什么呢?
1
wangyu17455 2022-02-26 08:45:10 +08:00 via Android
会互斥,所以肯定输出 1 ,2 ,加了 sync 关键字同一时刻只有一个线程能执行这个方法,这是互斥性,方法结束所有的变量被写回主内存,这是可见性
|
2
dcsuibian 2022-02-26 08:47:20 +08:00
不太记得了,每个对象本身应该是有一个锁的。
比如 synchronized (this){ } 这种写法,方法上的 synchronized 应该是语法糖 但针对 synchronized static 这种写法,那个锁对象就是类的 class 实例 |
3
MakHoCheung 2022-02-26 08:48:39 +08:00
不会互斥,都两个 Main 对象了
|
4
wangyu17455 2022-02-26 08:59:29 +08:00 via Android
我瞎了,没看见是两个对象,一楼当我没说
|
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 ; 写的不对的地方请指正 |
6
cxshun 2022-02-26 09:11:00 +08:00
不会互斥,首先你的 synchronized 是加在实例方法上面,那么就只有同一个对象的才会被锁住,你这里是两个不同的 main 实例,完全没啥关系。
而至于输出 1,2 或者 2 ,1 或者 2 ,2 是因为原子性和可见性的问题,你可以尝试把 i 的类型换成 AtomicInteger 就可以实现你想要的效果了。当然前提还是同一个实例对象 |
7
sutra 2022-02-26 09:49:41 +08:00 via iPhone
上述讨论问题时注意被注释掉的那行代码,提问者可能再问被注释掉那行代码启用后会如何,而对于阅读者,那行会自动无视。
|
8
gosidealone OP @sutra 那倒没有 真的就是没有被修饰的情况,放在那里只是为了对比
|
9
gosidealone OP |
10
shadow1949 2022-02-26 10:57:01 +08:00
@Blanke
还有可能是 1 ,1 |
11
aviator 2022-02-26 11:10:51 +08:00
@wangyu17455 建议捐献 /dog
|
12
lueffy 2022-02-26 11:19:08 +08:00
不会互斥, 正好最近在看极客时间专栏 [Java 并发编程实战]
推荐看 3|互斥锁(上):解决原子性问题, 4|互斥锁(下):如何用一把锁保护多个资源 应该可以免费阅读 , 这两篇比较详细地介绍了 synchronized |
13
q1angch0u 2022-02-26 11:33:38 +08:00 via iPhone
可以在方法上加 static 或者在代码块中使用 synchronized (Main.class) {} 锁类哈~
|
14
ershierdu 2022-02-26 12:22:05 +08:00
最近在准备春招,这个也算是八股文里的经典题目了(虽然我是前几天才知道的。。)
|
15
fly2mars 2022-02-26 13:14:37 +08:00
2,1 是怎么来的,只要有 1 个线程的 i 变为 2 输出后,另一个线程只能输出 2 啊
|
17
jeffxjh 2022-02-26 13:39:30 +08:00
跑了一下这段代码 2,2
|
18
gosidealone OP @jeffxjh 多跑几次
|
19
JasonLaw 2022-02-26 14:49:42 +08:00
|
20
fly2mars 2022-02-26 14:57:56 +08:00
|
21
ingin 2022-02-26 15:04:39 +08:00
@gosidealone #18 在同一个对象的情况下,你给出的结果:1 ,2 或者 2 ,1 或者 2 ,2 是怎么来的?我总觉得还有 1 ,1 这种情况,理由:i++不是原子操作
|
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 |
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 如何或何时决定是否去读取共享的变量的 |
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.”就可以保证。
|
25
blackboom 2022-02-26 16:16:47 +08:00
输出 1,1 也是有可能的,i 没有保证线程可见性,i++ 也不是原子操作。
|
26
fly2mars 2022-02-26 16:18:20 +08:00
@JasonLaw 就是在这个并没有使用同一个 lock 场景下,
从结果上来看,21 和 22 都有,那"线程 1 看到线程 2 所做的改变"就是不确定的啊, 想了解下线程 1 是否决定去感知线程 2 的变化的,有无可量化的指标?或者有无关键字可去查询下 |
27
gosidealone OP @ingin 额 我现在只跑得出 2 ,2 这种结果了
|
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 若理解有误请指正,感谢。 |
29
teem 2022-02-26 21:21:06 +08:00
#28 再添一个 getstatic (操作 G )可能更好理解一点
|
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 |
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 |
33
Joker123456789 2022-02-28 14:06:41 +08:00
你这都两对象了,, 加锁还有意义吗? 又不是静态方法
|