Java线程同步

14
在下面的类中,方法getIt()是否线程安全?为什么?
public class X { 
  private long myVar; 
  public void setIt(long  var){ 
    myVar = var; 
   }  
   public long getIt() { 
     return myVar; 
  } 
}

这是作业吗?还是面试题? - finnw
3
无论您何时方便 :) 如果您没有做出贡献,有什么不同吗?您想让我更改标记吗? - Registered User
我只是好奇,因为在面试中我被问了一个类似的问题。不需要更改标签。 - finnw
8个回答

22

该代码不具备线程安全性。在Java中,类型为longdouble的变量被视为两个分别由32位组成的变量。一个线程可能会在另一个线程读取半个值之前写入并已经写入了半个值。在这种情况下,读者将看到一个本不应该存在的值。

要使其线程安全,您可以将myVar声明为volatile(Java 1.5或更高版本)或使setItgetItsynchronized

请注意,即使myVar是32位的int,您仍然可能遇到线程问题,其中一个线程可能正在读取另一个线程已更改的过时值。这可能是因为CPU缓存了该值。要解决此问题,您需要再次将myVar声明为volatile(Java 1.5或更高版本),或使setItgetItsynchronized

还值得注意的是,如果您在后续的setIt调用中使用getIt的结果,例如x.setIt(x.getIt() * 2),则可能需要跨两个调用进行synchronize

synchronized(x)
{
  x.setIt(x.getIt() * 2);
}

如果没有额外的同步,另一个线程可能会在getItsetIt调用之间更改该值,导致另一个线程的值丢失。


+1 这是大多数开发人员不知道的事实。你可以使用 java.util.concurrent 中的 Atomic* 类之一()来更好地表达你的意图,而不是使用 volatile。 - helpermethod

9
这段代码不是线程安全的。即使您的平台保证long的原子写入,缺少synchronized也可能导致一个线程调用了setIt(),即使此调用已经完成,另一个线程仍然可以调用getIt()并且此调用可能返回myVar的旧值。 synchronized关键字不仅提供了对块或方法的独占访问,还保证第二个线程知道变量的更改。
因此,您需要将两个方法都标记为synchronized,或者将成员myVar标记为volatile
这里有一个非常好的同步解释(链接)

原子操作不能交错执行,因此可以在不担心线程干扰的情况下使用它们。但是,这并不能消除所有同步原语的需求,因为仍然可能出现内存一致性错误。使用volatile变量可以降低内存一致性错误的风险,因为对volatile变量的任何写入都会与随后读取相同变量的操作建立happens-before关系。这意味着对volatile变量的更改始终对其他线程可见。此外,当线程读取volatile变量时,它不仅可以看到最新的变化,还可以看到导致变化的代码的副作用。


6
不是这样的,至少在缺乏原子64位内存访问的平台上不是这样的。
假设线程A调用了“setIt”,将32位复制到后备值所在的内存中,并且在复制另外32位之前被抢占了。
然后线程B调用“getIt”。

7
即使在提供原子64位内存访问的平台上,您仍需要同步获取和设置方法,或者将成员声明为volatile。有关更多详细信息,请参见我的答案。 - tangens
你的答案是正确的,但太具体了。有更一般的原因导致它不同步 - 请参考@tangens的答案。 - Yuval Adam

3

不是的,因为在Java中long类型不是原子性的,所以一个线程可以在setIt方法中写入long类型的32位,然后getIt方法读取该值,接着setIt方法可以设置另外的32位。

因此,最终结果是getIt返回了一个从未有效过的值。


2

通常情况下,应该是线程安全的,但不能保证线程安全。不同核心在CPU缓存中可能有不同版本的问题,或者对于所有架构来说,存储/检索可能不是原子性的。建议使用AtomicLong类。


0

getter 不是线程安全的,因为它没有受到任何保证最新可见性的机制的保护。你的选择有:

  • myVar 声明为 final(但这样就无法更改它)
  • myVar 声明为 volatile
  • 使用 synchronized 访问 myVar

0
据我所知,现代JVM不再拆分长整型和双精度操作。我不知道是否还有任何参考资料表明这仍然是一个问题。例如,查看AtomicLong,它在Sun的JVM中不使用同步。
假设您想确保这不是一个问题,那么可以同时对get()和set()进行同步。但是,如果您执行像add这样的操作,即set(get()+1),那么这种同步并没有带来太多好处,您仍然必须为整个操作同步对象。(更好的解决方法是使用一个add(n)的单个操作,该操作是同步的)
然而,更好的解决方案是使用AtomicLong。它支持原子操作,如get、set和add,并且不使用同步。

-4

由于这是一个只读方法,您应该同步set方法。

编辑:我明白为什么get方法也需要同步了。Phil Ross解释得很好,干得漂亮。


10
为了确保线程安全,这两种方法必须同步。否则,get方法可能会返回一个部分更改的版本。 - Michael Borgwardt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接