正确使用volatile变量和synchronized块

时间:2014-05-15 12:45:31

标签: java multithreading synchronized volatile

我试图在java(或一般)中围绕线程安全。我有这个类(我希望符合POJO的定义),它也需要与JPA提供程序兼容:

    public class SomeClass {

        private Object timestampLock = new Object();
        // are "volatile"s necessary?
        private volatile java.sql.Timestamp timestamp;
        private volatile String timestampTimeZoneName;

        private volatile BigDecimal someValue;

        public ZonedDateTime getTimestamp() {
            // is synchronisation necessary here? is this the correct usage?
            synchronized (timestampLock) {
                return ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneId.of(timestampTimeZoneName));
            }
        }

        public void setTimestamp(ZonedDateTime dateTime) {
            // is this the correct usage?
            synchronized (timestampLock) {
                this.timestamp = java.sql.Timestamp.from(dateTime.toInstant());
                this.timestampTimeZoneName = dateTime.getZone().getId();
            }
        }

        // is synchronisation required?
        public BigDecimal getSomeValue() {
            return someValue;
        }

        // is synchronisation required?
        public void setSomeValue(BigDecimal val) {
            someValue = val;
        }
    }

如代码中注释的行中所述,是否有必要将timestamptimestampTimeZoneName定义为volatile,并且应该使用synchronized块?或者我应该只使用synchronized块,而不是将timestamptimestampTimeZoneName定义为volatiletimestampTimeZoneName的{​​{1}}不应与其他timestamp错误匹配。

This链接说

  

对于声明为volatile的所有变量,读取和写入都是原子的   (包括长变量和双变量)

我是否应该理解,由于timestamp定义,通过setter / getter对此代码中的someValue的访问是线程安全的?如果是这样,有没有更好的(我不知道“更好”在这里可能意味着什么)实现这一目标的方法?

3 个答案:

答案 0 :(得分:1)

要确定是否需要同步,请尝试设想一个可能会导致代码中断的上下文切换的位置。

在这种情况下,如果上下文切换发生在我放置注释的位置,那么在getTimestamp()中,您将从每个时间戳类型中读取不同的值。

此外,虽然分配是原子的,但这个表达式java.sql.Timestamp.from(dateTime.toInstant());当然不是,因此您可以在dateTime.toInstant()from之间进行上下文切换。简而言之,你肯定需要同步块。

synchronized (timestampLock) {
    this.timestamp = java.sql.Timestamp.from(dateTime.toInstant());
    //CONTEXT SWITCH HERE
    this.timestampTimeZoneName = dateTime.getZone().getId();
}

synchronized (timestampLock) {
    return ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneId.of(timestampTimeZoneName));
}

就易变性而言,我很确定它们是必需的。您必须保证每个线程肯定会获得变量的最新版本。

这是挥之不去的合约。虽然它可能被同步块覆盖,并且在这里实际上并不需要,但无论如何都要写。如果synchronized块已经执行了volatile的工作,则VM将不会执行两次保证。这意味着不再需要挥发性物质,这是一个非常好的闪光灯,对程序员说:“我已经在多个线程中使用了”。


对于someValue:如果此处没有同步块,那么volatile肯定是必要的。如果在一个线程中调用一个集合,则另一个线程没有队列告诉它可能已在此线程之外更新。因此它可能使用旧的缓存值。如果它采用单线程,JIT可以做很多有趣的优化。可以简单地破坏你的程序。

现在我不完全确定此处是否同步 。我的猜测是否定的。无论如何我会添加它以保证安全。或者你可以让java担心同步并使用http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicInteger.html

答案 1 :(得分:1)

这里没什么新东西,这只是@Cruncher已经说过的更明确的版本:

只要程序中的两个或多个字段彼此保持一致,您就需要synchronized。假设您有两个并行列表,并且您的代码依赖于它们两者的长度相同。这就是所谓的不变,两个列表总是相同的长度。

如何编写一个方法,追加(x,y),为列表添加一对新值而不会暂时破坏不变量?你不能。方法必须将一个项目添加到第一个列表,打破不变量,然后将另一个项目添加到第二个列表,再次修复它。别无他法。

在单线程程序中,临时中断状态没有问题,因为当append(x,y)正在运行时,没有其他方法可以使用列表。在多线程程序中,这已不再适用。在最坏的情况下,append(x,y)可以将x添加到x列表,然后调度程序可以在该确切时刻挂起线程以允许其他线程运行。在附加(x,y)完成作业并使列表再次正确之前,CPU可以执行数百万条指令。在所有这段时间内,其他线程会看到破坏的不变量,并可能导致数据损坏或导致程序崩溃。

修复是针对某些对象追加(x,y)为synchronized,并且(这是重要的部分),使用列表的每个其他方法 同一个对象上的synchronized。由于在给定时间给定对象上只有一个线程可以synchronized,因此任何其他线程都不可能看到处于不一致状态的列表。

因此,如果线程A调用append(x,y),并且线程B尝试同时查看列表"",则线程B将看到列表的内容之后线程A完成了它的工作?这称为数据竞赛。只有我到目前为止所描述的同步,没有办法知道哪个线程会赢。到目前为止我们所做的只是保证一个特定的不变量。

如果哪个线程赢得比赛很重要,那么这意味着有一些更高级别的不变量也需要保护。您还必须添加更多同步以保护该同步。 "线程安全" - 用两个小词来命名一个既宽又深的主题。

祝你好运,玩得开心!

答案 2 :(得分:0)

    // is synchronisation required?
    public BigDecimal getSomeValue() {
        return someValue;
    }

    // is synchronisation required?
    public void setSomeValue(BigDecimal val) {
        someValue = val;
    }

我认为您需要放置同步块,因为考虑一个示例,其中一个线程正在设置该值,同时其他线程正在尝试从getter方法读取{{{ 3}}你会看到同步块。所以,如果你把变量放在方法中,那么你必须要求同步块。