没有同步顺序的程序
Program without synchronization order
我正在查看我发现的最简单的示例之一,并开始推理 SO
(同步顺序)或更准确地说,缺少它。考虑以下示例:
int a, b; // two shared variables
Thread-X:
void threadX() {
synchronized(this) {
a = 1;
}
synchronized(this) {
b = 1;
}
}
还有一个reader线程,Thread-Y
:
void threadY() {
int r1 = b;
int r2 = a;
}
为简单起见,我们假设 Thread-Y
完全按照此顺序进行读取:它肯定会先读取 b
,然后再读取 a
(与写作相反) .
允许读取线程查看 [1, 0]
(例如 b=1
发生在 之前 a=1
)。我想我也明白为什么:因为两个动作之间有 no synchronization order,因此没有 happens-before 并且根据 JLS
这是数据竞争:
When a program contains two conflicting accesses that are not ordered by a happens-before relationship, it is said to contain a data race.
因此阅读 a
和 b
是两个活泼的阅读,所以看到 b=1
和 a=0
是允许和可能的。
现在这又允许 JVM 在 writer 中进行锁粗化,所以它变成:
void threadX() {
synchronized(this) {
a = 1;
b = 1;
}
}
我的问题是,如果 reader 最初是这样写的:
void threadY() {
synchronized(this) {
int r1 = b;
}
synchronized(this) {
int r2 = a;
}
}
是否仍允许粗化锁?我认为我知道答案,但我也想听听有根据的解释。
允许锁粗化(和重新排序),因为同步读取器在粗化锁之前或之后得到排序。他们将永远无法看到持有粗化锁时发生了什么,因此无法观察到锁定代码内的任何重新排序。
有关详细信息,请参阅:
https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#myth-barriers-are-sane
顺便问个好问题。前段时间我也在为这个特殊的例子苦苦挣扎:)
如果 JVM 可以证明后续的 synchronized
块将使用相同的对象,那么锁粗化总是可能的。即使是 reader
void threadY() {
synchronized(this) {
int r1 = b;
}
synchronized(this) {
int r2 = a;
}
}
也可能得到优化
void threadY() {
synchronized(this) {
int r1 = b;
int r2 = a;
}
}
但是如果两个方法都是同一个class的实例方法,也就是说,它们的this
指的是同一个对象,它们的执行不能重叠。因此,即使读取 and/or 和写入重新排序,结果 [1, 0]
也是不可能的。
甚至锁消除也是允许的,只要执行环境能保证结果[1, 0]
永远不会发生即可。一个众所周知的例子是这样一种情况,即该对象从未被其他线程看到。
是的,这是允许的。
这里简单解释一下。
记住 synchronized
块:
- 原子执行:
- 当另一个线程持有相同的锁时,一个线程无法进入
synchronized
块
- 当线程进入
synchronized
块时,它会立即看到在先前执行的 synchronized
块中完成的所有操作
- 全局顺序执行,与每个线程的程序顺序一致(你说的同步顺序)
换句话说,synchronized
块始终以全局顺序自动执行。
不同的执行在 synchronized
块的交错方式上可能有所不同,但情况总是如此:
- 来自
threadX()
的第一个 synchronized
块总是在第二个 之前执行
- 来自
threadY()
的 synchronized
块相同
有 6 种可能的交错:
threadX threadY threadX threadY threadX threadY
------------------------------- ------------------------------- -------------------------------
synchronized { | synchronized { | synchronized { |
a = 1; | a = 1; | a = 1; |
} | } | } |
synchronized { | | synchronized { | synchronized {
b = 1; | | int r1 = b; | int r1 = b;
} | | } | }
| synchronized { synchronized { | | synchronized {
| int r1 = b; b = 1; | | int r2 = a;
| } } | | }
| synchronized { | synchronized { synchronized { |
| int r2 = a; | int r2 = a; b = 1 |
| } | } } | }
(Case A) (Case B) (Case C)
threadX threadY threadX threadY threadX threadY
------------------------------- ------------------------------- -------------------------------
| synchronized { | synchronized { | synchronized {
| int r1 = b; | int r1 = b; | int r1 = b;
| } | } | }
| synchronized { synchronized { | synchronized { |
| int r2 = a; a = 1; | a = 1; |
| } } | } |
synchronized { | | synchronized { synchronized { |
a = 1; | | int r2 = a; b = 1; |
} | | } } |
synchronized { | synchronized { | | synchronized {
b = 1; | b = 1; | | int r2 = a;
} | } | | }
(Case D) (Case E) (Case F)
当您在 threadY()
中合并 synchronized
个块时:
void threadY() { void threadY() {
synchronized(this) { synchronized(this) {
int r1 = b; int r1 = b;
} => int r2 = a;
synchronized(this) { }
int r2 = a; }
}
}
然后你实际上只保留 threadY()
中的 synchronized
个块彼此相邻的情况:即情况 A、C 和 D。
既然这次优化后没有出现新的可能执行,那么这次优化是合法的。
要获得更严格和详细的解释,我建议:
- J. Manson's Ph.D. Thesis on JMM
中的“锁定粗化”章节
- 锁粗化example in an article by A. Shipilev as recommended in
我正在查看我发现的最简单的示例之一,并开始推理 SO
(同步顺序)或更准确地说,缺少它。考虑以下示例:
int a, b; // two shared variables
Thread-X:
void threadX() {
synchronized(this) {
a = 1;
}
synchronized(this) {
b = 1;
}
}
还有一个reader线程,Thread-Y
:
void threadY() {
int r1 = b;
int r2 = a;
}
为简单起见,我们假设 Thread-Y
完全按照此顺序进行读取:它肯定会先读取 b
,然后再读取 a
(与写作相反) .
允许读取线程查看 [1, 0]
(例如 b=1
发生在 之前 a=1
)。我想我也明白为什么:因为两个动作之间有 no synchronization order,因此没有 happens-before 并且根据 JLS
这是数据竞争:
When a program contains two conflicting accesses that are not ordered by a happens-before relationship, it is said to contain a data race.
因此阅读 a
和 b
是两个活泼的阅读,所以看到 b=1
和 a=0
是允许和可能的。
现在这又允许 JVM 在 writer 中进行锁粗化,所以它变成:
void threadX() {
synchronized(this) {
a = 1;
b = 1;
}
}
我的问题是,如果 reader 最初是这样写的:
void threadY() {
synchronized(this) {
int r1 = b;
}
synchronized(this) {
int r2 = a;
}
}
是否仍允许粗化锁?我认为我知道答案,但我也想听听有根据的解释。
允许锁粗化(和重新排序),因为同步读取器在粗化锁之前或之后得到排序。他们将永远无法看到持有粗化锁时发生了什么,因此无法观察到锁定代码内的任何重新排序。
有关详细信息,请参阅: https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#myth-barriers-are-sane
顺便问个好问题。前段时间我也在为这个特殊的例子苦苦挣扎:)
如果 JVM 可以证明后续的 synchronized
块将使用相同的对象,那么锁粗化总是可能的。即使是 reader
void threadY() {
synchronized(this) {
int r1 = b;
}
synchronized(this) {
int r2 = a;
}
}
也可能得到优化
void threadY() {
synchronized(this) {
int r1 = b;
int r2 = a;
}
}
但是如果两个方法都是同一个class的实例方法,也就是说,它们的this
指的是同一个对象,它们的执行不能重叠。因此,即使读取 and/or 和写入重新排序,结果 [1, 0]
也是不可能的。
甚至锁消除也是允许的,只要执行环境能保证结果[1, 0]
永远不会发生即可。一个众所周知的例子是这样一种情况,即该对象从未被其他线程看到。
是的,这是允许的。
这里简单解释一下。
记住 synchronized
块:
- 原子执行:
- 当另一个线程持有相同的锁时,一个线程无法进入
synchronized
块 - 当线程进入
synchronized
块时,它会立即看到在先前执行的synchronized
块中完成的所有操作
- 当另一个线程持有相同的锁时,一个线程无法进入
- 全局顺序执行,与每个线程的程序顺序一致(你说的同步顺序)
换句话说,synchronized
块始终以全局顺序自动执行。
不同的执行在 synchronized
块的交错方式上可能有所不同,但情况总是如此:
- 来自
threadX()
的第一个synchronized
块总是在第二个 之前执行
- 来自
threadY()
的
synchronized
块相同
有 6 种可能的交错:
threadX threadY threadX threadY threadX threadY
------------------------------- ------------------------------- -------------------------------
synchronized { | synchronized { | synchronized { |
a = 1; | a = 1; | a = 1; |
} | } | } |
synchronized { | | synchronized { | synchronized {
b = 1; | | int r1 = b; | int r1 = b;
} | | } | }
| synchronized { synchronized { | | synchronized {
| int r1 = b; b = 1; | | int r2 = a;
| } } | | }
| synchronized { | synchronized { synchronized { |
| int r2 = a; | int r2 = a; b = 1 |
| } | } } | }
(Case A) (Case B) (Case C)
threadX threadY threadX threadY threadX threadY
------------------------------- ------------------------------- -------------------------------
| synchronized { | synchronized { | synchronized {
| int r1 = b; | int r1 = b; | int r1 = b;
| } | } | }
| synchronized { synchronized { | synchronized { |
| int r2 = a; a = 1; | a = 1; |
| } } | } |
synchronized { | | synchronized { synchronized { |
a = 1; | | int r2 = a; b = 1; |
} | | } } |
synchronized { | synchronized { | | synchronized {
b = 1; | b = 1; | | int r2 = a;
} | } | | }
(Case D) (Case E) (Case F)
当您在 threadY()
中合并 synchronized
个块时:
void threadY() { void threadY() {
synchronized(this) { synchronized(this) {
int r1 = b; int r1 = b;
} => int r2 = a;
synchronized(this) { }
int r2 = a; }
}
}
然后你实际上只保留 threadY()
中的 synchronized
个块彼此相邻的情况:即情况 A、C 和 D。
既然这次优化后没有出现新的可能执行,那么这次优化是合法的。
要获得更严格和详细的解释,我建议:
- J. Manson's Ph.D. Thesis on JMM 中的“锁定粗化”章节
- 锁粗化example in an article by A. Shipilev as recommended in