Java并发场景 - 我需要同步还是不同步? [英] Java concurrency scenario -- do I need synchronization or not?

查看:100
本文介绍了Java并发场景 - 我需要同步还是不同步?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这是协议。我有一个哈希映射包含数据我称为程序代码,它住在一个对象,如此:

 类元数据
{
private HashMap validProgramCodes;
public HashMap getValidProgramCodes(){return validProgramCodes; }
public void setValidProgramCodes(HashMap h){validProgramCodes = h; }
}



我有很多很多读者线程,每个线程都调用getValidProgramCodes )一次,然后使用该hashmap作为只读资源。



到目前为止很好。这里是我们感兴趣的地方。



我想放一个计时器,每隔一段时间生成一个新的有效程序代码列表(不管怎么样),并调用setValidProgramCodes。



我的理论 - 我需要帮助来验证 - 是我可以继续使用代码,而不进行显式同步。它如下所示:
在更新validProgramCodes时,validProgramCodes的值总是好的 - 它是一个指向新的或旧的哈希的指针。 这是一个假设,一切都在那里。读者谁有老的哈希是好的;他可以继续使用旧的值,因为它不会被垃圾收集,直到他释放它。每个读者都是短暂的;它会很快就会死亡,并被一个新的人取而代之以获得新的价值。



这是水吗?我的主要目标是避免昂贵的同步和阻塞在绝大多数情况下,没有更新发生。

解决方案

使用挥发性物质



这是一个线程关心另一个线程的情况吗?然后 JMM常见问题答案:


大多数时候,一个线程并不关心别人在做什么。但是当
时,这就是同步
的用途。


OP的代码是安全的,因为,考虑这是:没有什么在Java的内存模型,保证当一个新的线程启动时,该字段将被刷新到主内存。此外,JVM可以自由重新排序操作,只要更改在线程中不可检测。



理论上来说,读者线程不能保证看到对validProgramCodes的写入。在实践中,他们最终会,但你不能确定什么时候。



我建议将validProgramCodes成员声明为volatile。速度差异将是可以忽略的,它将保证您的代码现在和未来的安全性,无论JVM优化可能引入。



这里有一个具体的建议: p>

  import java.util.Collections; 

类元数据{

私有volatile映射validProgramCodes = Collections.emptyMap();

public Map getValidProgramCodes(){
return validProgramCodes;
}

public void setValidProgramCodes(Map h){
if(h == null)
throw new NullPointerException(validProgramCodes == null);
validProgramCodes = Collections.unmodifiableMap(new HashMap(h));
}

}



不可变性



除了用 unmodifiableMap 包装它,我复制了地图( new HashMap(h) code>)。这使得快照不会改变,即使设置器的调用者继续更新映射h。



依赖于界面



在风格笔记上,通常最好使用诸如 List Map 之类的抽象类型来声明API,而不是像 ArrayList HashMap。如果具体类型需要改变,

缓存



将h分配给validProgramCodes的结果可能只是写入处理器的缓存。即使新线程启动,h对新线程也不可见,除非它已被刷新到共享内存。除非必要,否则良好的运行时将避免刷新,并且使用 volatile 是表示有必要的一种方法。



重新排序



假设以下代码:

  HashMap codes = new HashMap (); 
codes.putAll(source);
meta.setValidProgramCodes(codes);

如果 setValidCodes c $ c> validProgramCodes = h; ,编译器可以对代码重新排序,如下所示:

  1:meta.validProgramCodes = codes = new HashMap(); 
2:codes.putAll(source);

假设执行写入器行1后,读取器线程开始运行此代码:

  1:Map codes = meta.getValidProgramCodes(); 
2:Iterator i = codes.entrySet()。iterator();
3:while(i.hasNext()){
4:Map.Entry e =(Map.Entry)i.next();
5://用e做事情。
6:}


$ b $ p

现在假设写线程在地图上调用putAll读者的第2行和第3行。迭代器底下的映射经历了并行修改,并抛出一个运行时异常,这是一个恶意间歇的,看似无法解释的运行时异常,在测试期间从未产生过。



并发编程



每当有一个线程关心另一个线程正在做什么时,您必须以确保一个线程的动作对另一个线程可见。如果一个线程中的某个事件必须发生在另一个线程中的某个事件之前,您必须明确指明该事件。没有其他保证。实际上,这意味着 volatile 同步



不要吝啬。不正确的程序无法完成其工作的速度并不重要。这里显示的示例是简单的和设计的,但是放心,他们说明真实世界的并发错误,由于它们的不可预测性和平台敏感性,难以识别和解决。



< h2>其他资源


Here's the deal. I have a hash map containing data I call "program codes", it lives in an object, like so:

Class Metadata
{
    private HashMap validProgramCodes;
    public HashMap getValidProgramCodes() { return validProgramCodes; }
    public void setValidProgramCodes(HashMap h) { validProgramCodes = h; }
}

I have lots and lots of reader threads each of which will call getValidProgramCodes() once and then use that hashmap as a read-only resource.

So far so good. Here's where we get interesting.

I want to put in a timer which every so often generates a new list of valid program codes (never mind how), and calls setValidProgramCodes.

My theory -- which I need help to validate -- is that I can continue using the code as is, without putting in explicit synchronization. It goes like this: At the time that validProgramCodes are updated, the value of validProgramCodes is always good -- it is a pointer to either the new or the old hashmap. This is the assumption upon which everything hinges. A reader who has the old hashmap is okay; he can continue to use the old value, as it will not be garbage collected until he releases it. Each reader is transient; it will die soon and be replaced by a new one who will pick up the new value.

Does this hold water? My main goal is to avoid costly synchronization and blocking in the overwhelming majority of cases where no update is happening. We only update once per hour or so, and readers are constantly flickering in and out.

解决方案

Use Volatile

Is this a case where one thread cares what another is doing? Then the JMM FAQ has the answer:

Most of the time, one thread doesn't care what the other is doing. But when it does, that's what synchronization is for.

In response to those who say that the OP's code is safe as-is, consider this: There is nothing in Java's memory model that guarantees that this field will be flushed to main memory when a new thread is started. Furthermore, a JVM is free to reorder operations as long as the changes aren't detectable within the thread.

Theoretically speaking, the reader threads are not guaranteed to see the "write" to validProgramCodes. In practice, they eventually will, but you can't be sure when.

I recommend declaring the validProgramCodes member as "volatile". The speed difference will be negligible, and it will guarantee the safety of your code now and in future, whatever JVM optimizations might be introduced.

Here's a concrete recommendation:

import java.util.Collections;

class Metadata {

    private volatile Map validProgramCodes = Collections.emptyMap();

    public Map getValidProgramCodes() { 
      return validProgramCodes; 
    }

    public void setValidProgramCodes(Map h) { 
      if (h == null)
        throw new NullPointerException("validProgramCodes == null");
      validProgramCodes = Collections.unmodifiableMap(new HashMap(h));
    }

}

Immutability

In addition to wrapping it with unmodifiableMap, I'm copying the map (new HashMap(h)). This makes a snapshot that won't change even if the caller of setter continues to update the map "h". For example, they might clear the map and add fresh entries.

Depend on Interfaces

On a stylistic note, it's often better to declare APIs with abstract types like List and Map, rather than a concrete types like ArrayList and HashMap. This gives flexibility in the future if concrete types need to change (as I did here).

Caching

The result of assigning "h" to "validProgramCodes" may simply be a write to the processor's cache. Even when a new thread starts, "h" will not be visible to a new thread unless it has been flushed to shared memory. A good runtime will avoid flushing unless it's necessary, and using volatile is one way to indicate that it's necessary.

Reordering

Assume the following code:

HashMap codes = new HashMap();
codes.putAll(source);
meta.setValidProgramCodes(codes);

If setValidCodes is simply the OP's validProgramCodes = h;, the compiler is free to reorder the code something like this:

 1: meta.validProgramCodes = codes = new HashMap();
 2: codes.putAll(source);

Suppose after execution of writer line 1, a reader thread starts running this code:

 1: Map codes = meta.getValidProgramCodes();
 2: Iterator i = codes.entrySet().iterator();
 3: while (i.hasNext()) {
 4:   Map.Entry e = (Map.Entry) i.next();
 5:   // Do something with e.
 6: }

Now suppose that the writer thread calls "putAll" on the map between the reader's line 2 and line 3. The map underlying the Iterator has experienced a concurrent modification, and throws a runtime exception—a devilishly intermittent, seemingly inexplicable runtime exception that was never produced during testing.

Concurrent Programming

Any time you have one thread that cares what another thread is doing, you must have some sort of memory barrier to ensure that actions of one thread are visible to the other. If an event in one thread must happen before an event in another thread, you must indicate that explicitly. There are no guarantees otherwise. In practice, this means volatile or synchronized.

Don't skimp. It doesn't matter how fast an incorrect program fails to do its job. The examples shown here are simple and contrived, but rest assured, they illustrate real-world concurrency bugs that are incredibly difficult to identify and resolve due to their unpredictability and platform-sensitivity.

Additional Resources

这篇关于Java并发场景 - 我需要同步还是不同步?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆