gpt4 book ai didi

java - 一次写入的数组/集合/映射内容的安全发布

转载 作者:行者123 更新时间:2023-12-01 16:43:00 25 4
gpt4 key购买 nike

主要问题:
在 Java 中安全发布数组、集合或映射内容的最佳方法是什么?

这是我尝试过的方法以及我的一些附带问题:

<小时/>

附带问题#1

在线程 1 上,我正在写信给 HashMap :

Map<K, V> map = new HashMap<>();
map.put(key, value);

我目前的理解是:

我错了吗?

<小时/>

附带问题#2

现在,我想安全地发布 HashMap 的内容在读取之前写入。我目前的理解是使用ConcurrentHashMap会导致性能损失,因此我宁愿避免在像这样的场景中使用它。

我想出了这个替代方案:

class MapSafePublication<K, V> {
private final Map<K, V> map = new HashMap<>();

private final ThreadLocal<Map<K, V>> safeMap = ThreadLocal.withInitial(() -> {
synchronized (MapSafePublication.this) {
return new HashMap<>(map);
}
});

synchronized void write(K key, V value) {
map.put(key, value);
}

V read(K key) {
return safeMap.get().get(key);
}
}

正确吗?有更好的方法吗?

<小时/>

附带问题#3

在线程 1 上,我正在写入一个 volatile 数组:

volatile Object[] array = new Object[size];
array[index] = value;

我目前的理解是从另一个线程读取array[index]是不安全的。为了确保安全,必须使用 AtomicReferenceArray .

这是 CopyOnWriteArrayList 的摘录:

final transient Object lock = new Object();
private transient volatile Object[] array;

public E set(int index, E element) {
synchronized (lock) {
Object[] es = getArray();
E oldValue = elementAt(es, index);

if (oldValue != element) {
es = es.clone();
es[index] = element;
}
// Ensure volatile write semantics even when oldvalue == element
setArray(es);
return oldValue;
}
}

public E get(int index) {
return elementAt(getArray(), index);
}

@SuppressWarnings("unchecked")
static <E> E elementAt(Object[] a, int index) {
return (E) a[index];
}

如何get()如果它没有获取 set() 使用的相同锁,则安全地从数组中读取?

这同样适用于 com.google.common.collect.ImmutableList.copyOf() 的结果,根据我目前的理解,这并不构成参数对象中存储或返回的引用的安全发布。我错了吗?

<小时/>

澄清#1

从内存可见性的角度来看,以下哪些是安全的?

class Test<K, V> {
private final Test1<K, V> test1;
private Test2<K, V> test2;

Test(Test1<K, V> test1, Test2<K, V> test2) {
this.test1 = test1;
this.test2 = test2;
}

void test(K key) {
System.out.println(test1.read(key));
System.out.println(test2.read(key));
}
}

class Test1<K, V> {
private Map<K, V> map = new HashMap<>();

void write(K key, V value) {
map.put(key, value);
}

V read(K key) {
return map.get(key);
}
}

class Test2<K, V> {
private final Map<K, V> map = new HashMap<>();

void write(K key, V value) {
map.put(key, value);
}

V read(K key) {
return map.get(key);
}
}

// Thread 1
Test1<K, V> test1 = new Test1<>();
test1.write(key, value);
Test2<K, V> test2 = new Test2<>();
test2.write(key, value);

// Thread 2
Test<K, V> test = new Test<>(test1, test2);
test.test(key);
<小时/>

澄清#2:

考虑以下示例:

// Thread 1
Object obj = new Object();
volatile Object object = obj;

// Thread 2
System.out.println(object);

// Thread 1 or 3
object = obj;

// Thread 2
System.out.println(object);

那么在后一种情况下,读取器和写入器线程之间没有发生之前的关系?

最佳答案

不存在“执行安全发布的最佳方式”,因为发布方式的决定取决于涉及发布的实际用例。

因此,说 Collections.unmodifyingMap(…) 不是安全发布是不正确的。该方法根本不是出版物。

当线程在对象发布后可能修改数据时,即当另一个线程可能已经处理数据时,根本不存在安全发布。

ConcurrentHashMap 可以解决此问题,不是因为它使映射的发布变得安全,而是因为每次修改本身都是安全的发布。如果使用线程可以处理这样的事实,即在处理映射时进行修改时不存在一致的映射整体状态,但只有单个映射是一致的,则这仍然只能使其使用线程安全。并且,每个键或值在发布后不得被修改。

当您遵守发布后不得修改对象的规则时,正确发布的可能性有很多。考虑:

HashMap<String, List<Integer>> map = new HashMap<>();
List<Integer> l = new ArrayList<>();
l.add(42);
map.put("foo", l);

Thread t = new Thread() {
@Override
public void run() {
System.out.println(map);
}
};
t.start();
System.out.println(map);
t.join();

调用t.start()与线程执行的任何操作之间存在发生之前关系。只要我们遵守“发布后不得修改”的规则,这足以安全发布 HashMap 和所包含的 ArrayList,无需任何额外的工作。两个线程可以同时读取 map 这一事实并不重要。

所以当你有这样的代码时

volatile Object[] array = new Object[size];
array[index] = value;

你是对的,它不安全,因为它违反了“发布后不得修改”的规则。

但是,当您仔细查看发布的代码时,CopyOnWriteArrayList 是不同的:

public E set(int index, E element) {
synchronized (lock) {
Object[] es = getArray();
E oldValue = elementAt(es, index);

if (oldValue != element) {
es = es.clone(); // <- create a local copy
es[index] = element; // <- modifies the copy that has not been published
}
// Ensure volatile write semantics even when oldvalue == element
setArray(es); // <- publishes the copy
return oldValue;
}
}

因此,并不违反“发布后不得修改”规则,因为修改始终在发布之前在本地副本上执行,并且全新数组引用的 volatile 写入正在建立一个 happens-before 与此引用的后续 volatile 读取的关系。 synchronized 的存在只是为了使多个修改保持一致。

请注意,您发布的代码在某些方面与 this OpenJDK implementation 有所不同:

public E set(int index, E element) {
synchronized (lock) {
Object[] es = getArray();
E oldValue = elementAt(es, index);

if (oldValue != element) {
es = es.clone();
es[index] = element;
setArray(es);
}
return oldValue;
}
}

虽然两者在正确使用时都可以正常工作,但区别在于

        // Ensure volatile write semantics even when oldvalue == element
setArray(es); // invoked even when oldvalue == element

显示出错误的心态,完全符合所讨论的规则。当oldvalue == element时,被设置的元素已经在列表中,换句话说,已经发布了。因此,如果在这个冗余的 set 调用之前修改了元素,它将违反“发布后不修改”规则,并且在此处执行另一个 volatile 写入不会解决这个问题。另一方面,如果不进行任何修改, volatile 写入将被废弃。因此,当oldvalue == element时,没有理由执行 volatile 写入。

这可能会帮助您评估您的 ThreadLocal 方法。为每个线程创建一个新副本是没有意义的。由于这些副本永远不会被修改,因此可以由任意数量的线程安全地读取它们。但由于这些快照永远不会被修改,因此任何线程都不会注意到创建快照后对原始映射所做的更改。如果更改很少发生并且您有很多读者,则与 CopyOnWriteArrayList 类似的方法会起作用。

关于java - 一次写入的数组/集合/映射内容的安全发布,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59646146/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com