如果其字段不可变,则外部同步的 ArrayList 线程安全吗?
Is externally synchronized ArrayList thread safe if its fields are not volatile?
让我们假设:
- 有一个ArrayList
- 列表被多个线程访问。线程可以添加元素并遍历所有元素。
- 所有访问都是外部同步的。所以不可能有两个线程同时访问列表。
查看 ArrayList 源代码,我们可以看到 size 和 elementData 字段不是易变的:
transient Object[] elementData; // non-private to simplify nested class access
private int size;
另外,我们来看添加方法:
/**
* This helper method split out from add(E) to keep method
* bytecode size under 35 (the -XX:MaxInlineSize default value),
* which helps when add(E) is called in a C1-compiled loop.
*/
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
会发生这样的事情吗?
- 假设列表有 4 个元素。
- 线程 A 添加新元素。大小更新为 5.
- 线程 B 添加新元素。大小被缓存,线程 B 看到它的旧值 (4)。因此,不是添加新元素,而是覆盖最后一个元素。
elementData 会出现类似的情况吗?
基于对你的问题的误解,我之前添加了另一个答案。抱歉。
关于 ArrayList 中大小字段的波动性,您的担心是有道理的。你描述的错误是可能的。这里是 an article describing exactly the same issue.
In case of volatile
reference object, it is ensured that the reference itself will be visible to other threads in a timely manner but the same is not true for its member variables. There is no guarantee that data contained within the object will be visible consistently if accessed individually.
对于来自 java.util
的标准 ArrayList,您无能为力,但您可以通过制作自己的 class 来非常轻松地解决这个问题,该 class 与 ArrayList 非常相似,但设置了实例变量的波动性适当地。这也可以为您提供一种更简单(或更易于阅读)的方式来同步列表,而不是从外部同步它。
TL;DR:如果同步正确,你描述的问题是不可能发生的,因为同步确保了操作的原子性和可见性。
JVM 执行 Java 代码的方式相当复杂。可以自由地对Java代码中的表达式和语句对应的指令进行重新排序,以便更有效地执行它们,前提是您无法判断某个线程对其操作进行了重新排序。
本质上就像老板说"I don't care how you get the work done, just have [the work] done by [some time]".
困难在于,虽然它说您不能看到线程内的重新排序,但它并没有说不同的线程不能看到彼此以不同的顺序做事。
这是令人头晕目眩的东西。简化的概念是 happens-before 的想法。您可以在两个线程中执行某些操作,以确保一个线程完成的事情在另一个线程尝试使用它们的结果时看起来已经发生了。从字面上看,一个线程中的事物在另一个线程中具有 "happened before" 事物。 (继续工作类比,这就像必须将您完成的工作交给同事,以便他们能够完成他们的工作:他们可以拿走您完成的工作并完成他们的工作,而不管 如何 你完成了)。
有许多众所周知的事物可以创建事前发生关系。与此问题相关的是:
- 写入易失性变量发生在读取同一变量之前。这通常用 "the data is always written to and read from main memory, instead of being cached by the thread".
来描述
- 退出同步块发生在进入具有相同监视器的同步块之前。换句话说,一个线程在同步块内发生的写入对其他执行同步代码的线程可见
因此,volatile 和 synchronized 都是创建 happens-before 的方法,这是保证一个线程完成的[某事]被另一个线程看到所必需的。
但是两者还是有区别的:
- Volatile 为您提供可见性:它确保写入可见。
- 同步给你可见性和原子性:它确保写入是可见的,但它另外确保没有其他人在同一时间做某事,只要一个举行特别班长。
在添加到 ArrayList
的情况下,需要原子性,因为您要做的不止一件事:增加大小 和 分配新的数组元素.
使变量也成为 volatile 在正确性方面没有任何意义,但它会使代码在模态情况下变慢,在这种情况下 ArrayList
只能从单个线程访问。
因此,只要您的代码 正确地 同步 - 也就是说,对列表的所有访问都在同一事物上同步,例如在列表本身上 - 您描述的情况不可能发生,因为同步的原子性和可见性属性。
让我们假设:
- 有一个ArrayList
- 列表被多个线程访问。线程可以添加元素并遍历所有元素。
- 所有访问都是外部同步的。所以不可能有两个线程同时访问列表。
查看 ArrayList 源代码,我们可以看到 size 和 elementData 字段不是易变的:
transient Object[] elementData; // non-private to simplify nested class access
private int size;
另外,我们来看添加方法:
/**
* This helper method split out from add(E) to keep method
* bytecode size under 35 (the -XX:MaxInlineSize default value),
* which helps when add(E) is called in a C1-compiled loop.
*/
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
会发生这样的事情吗?
- 假设列表有 4 个元素。
- 线程 A 添加新元素。大小更新为 5.
- 线程 B 添加新元素。大小被缓存,线程 B 看到它的旧值 (4)。因此,不是添加新元素,而是覆盖最后一个元素。
elementData 会出现类似的情况吗?
基于对你的问题的误解,我之前添加了另一个答案。抱歉。
关于 ArrayList 中大小字段的波动性,您的担心是有道理的。你描述的错误是可能的。这里是 an article describing exactly the same issue.
In case of
volatile
reference object, it is ensured that the reference itself will be visible to other threads in a timely manner but the same is not true for its member variables. There is no guarantee that data contained within the object will be visible consistently if accessed individually.
对于来自 java.util
的标准 ArrayList,您无能为力,但您可以通过制作自己的 class 来非常轻松地解决这个问题,该 class 与 ArrayList 非常相似,但设置了实例变量的波动性适当地。这也可以为您提供一种更简单(或更易于阅读)的方式来同步列表,而不是从外部同步它。
TL;DR:如果同步正确,你描述的问题是不可能发生的,因为同步确保了操作的原子性和可见性。
JVM 执行 Java 代码的方式相当复杂。可以自由地对Java代码中的表达式和语句对应的指令进行重新排序,以便更有效地执行它们,前提是您无法判断某个线程对其操作进行了重新排序。
本质上就像老板说"I don't care how you get the work done, just have [the work] done by [some time]".
困难在于,虽然它说您不能看到线程内的重新排序,但它并没有说不同的线程不能看到彼此以不同的顺序做事。
这是令人头晕目眩的东西。简化的概念是 happens-before 的想法。您可以在两个线程中执行某些操作,以确保一个线程完成的事情在另一个线程尝试使用它们的结果时看起来已经发生了。从字面上看,一个线程中的事物在另一个线程中具有 "happened before" 事物。 (继续工作类比,这就像必须将您完成的工作交给同事,以便他们能够完成他们的工作:他们可以拿走您完成的工作并完成他们的工作,而不管 如何 你完成了)。
有许多众所周知的事物可以创建事前发生关系。与此问题相关的是:
- 写入易失性变量发生在读取同一变量之前。这通常用 "the data is always written to and read from main memory, instead of being cached by the thread". 来描述
- 退出同步块发生在进入具有相同监视器的同步块之前。换句话说,一个线程在同步块内发生的写入对其他执行同步代码的线程可见
因此,volatile 和 synchronized 都是创建 happens-before 的方法,这是保证一个线程完成的[某事]被另一个线程看到所必需的。
但是两者还是有区别的:
- Volatile 为您提供可见性:它确保写入可见。
- 同步给你可见性和原子性:它确保写入是可见的,但它另外确保没有其他人在同一时间做某事,只要一个举行特别班长。
在添加到 ArrayList
的情况下,需要原子性,因为您要做的不止一件事:增加大小 和 分配新的数组元素.
使变量也成为 volatile 在正确性方面没有任何意义,但它会使代码在模态情况下变慢,在这种情况下 ArrayList
只能从单个线程访问。
因此,只要您的代码 正确地 同步 - 也就是说,对列表的所有访问都在同一事物上同步,例如在列表本身上 - 您描述的情况不可能发生,因为同步的原子性和可见性属性。