如果我有一个变量从多个线程读取并且只有一个线程写入,我是否需要锁定该变量?如果一个线程试图读取而另一个线程试图同时写入,它会崩溃吗?
答案 0 :(得分:8)
并发问题不是崩溃,而是您看到的数据版本。
如果共享变量是以原子方式编写的,当您认为(编写者)线程更新了变量时,一个(读取器)线程可能会读取过时的值。在这种情况下,您可以使用volatile关键字来防止读者线程读取陈旧值。
如果写入操作不是原子的(例如,如果它是某种类型的复合对象,并且您一次写入它的位,而其他线程理论上可以读取它),那么您的关注点将是也有些读者线程可以看到变量处于不一致状态。你可以通过在变量写入时锁定对变量的访问(慢速)或确保你是以原子方式编写来阻止这种情况。
写入某些类型的字段是原子的,但没有 发生在关系之前确保正确的内存排序(除非你使用{{ 1}});有关详细信息,请参阅this page。
答案 1 :(得分:4)
简单的答案是肯定的,你需要同步。
如果你曾经写过某个字段并在没有某种形式的同步的情况下从其他地方读取它,那么你的程序可能会看到不一致的状态并且可能是错误的。你的程序不会崩溃,但可以看到旧的或新的或(在长和双的情况下)半旧和半新数据。
当我说“某种形式的同步”时,我更准确地说是在写入和读取位置之间创建“先发生过”关系(也称为内存屏障)的东西。同步或java.util.concurrent.lock类是创建此类事物最明显的方法,但所有并发集合通常也提供类似的保证(检查javadoc是否确定)。例如,执行put和take并发队列将创建一个before-before关系。
将字段标记为volatile可防止您看到不一致的引用(长时间撕裂)并保证所有线程都“看到”写入。但是,易失性字段写入/读取不能与更大原子单元中的其他操作组合。 Atomic类处理常见的组合操作,如比较和设置或读取和增量。同步或其他java.util.concurrent同步器(CyclicBarrier等)或锁应该用于更大的排他性区域。
偏离简单的是,有些情况更“不,如果你真的知道你在做什么”。两个例子:
1)场地的特殊情况是最终的并且仅在施工期间写入。其中一个例子是当你填充预先计算的缓存时(想想一个Map,其中键是众所周知的值,值是预先计算的派生值)。如果你在构造之前在字段中构建它并且字段是final并且你以后永远不会写入它,那么构造函数的结尾将执行“final field freeze”并且后续读取不需要同步。
2)Effective Java中涵盖的“racy single check”模式的情况。规范示例位于java.lang.String.hashCode()中。 String有一个哈希字段,在你第一次调用hashCode()并缓存到本地字段时是懒惰计算的,该字段是未同步的。基本上,多个线程可能竞争计算此值并设置其他线程,但因为它由一个众所周知的sentinel(0)保护并始终计算相同的值(所以我们不关心哪个线程“获胜”或是否多个do),这实际上保证是好的。
较长的参考文献(由我撰写):http://refcardz.dzone.com/refcardz/core-java-concurrency
答案 2 :(得分:2)
请注意,volatile不是原子的,这意味着可以在不一致的状态下读取使用64位的double和long,其中32位是旧值,32位是新值。此外,易失性数组不会使数组条目易失。强烈建议使用java.util.concurrent中的类。