【问题标题】:Proper use of volatile variables and synchronized blocks正确使用易失变量和同步块
【发布时间】:2014-07-03 22:09:30
【问题描述】:

我正试图围绕 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 定义为volatiletimestamptimestampTimeZoneName 不应与另一个 timestamp 错误地匹配。

This 链接说

对于所有声明为 volatile 的变量,读写都是原子的 (包括 long 和 double 变量)

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

【问题讨论】:

  • 你打算使用SomeClass作为JPA实体吗?
  • 是的,我将其用作 JPA 实体
  • EntityManager@Entity 类不应暴露于来自多个线程的并发访问。检查Transactions and Concurrency chapter from Hibernate docs。总而言之,我建议将同步应用于您的业务逻辑。
  • 我想尝试从数据库中异步更新实体。就像工作线程定期刷新选择的实体一样。

标签: java multithreading synchronized volatile


【解决方案1】:
    // is synchronisation required?
    public BigDecimal getSomeValue() {
        return someValue;
    }

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

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

【讨论】:

    【解决方案2】:

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

    只要程序中的两个或多个字段相互一致很重要,您就需要synchronized。假设您有两个并行列表,并且您的代码取决于它们的长度相同。这被称为 invariant,因为这两个列表 invariably 长度相同。

    如何编写一个方法 append(x,y),在不暂时破坏不变量的情况下将一对新值添加到列表中?你不能。该方法必须将一个项目添加到第一个列表,打破不变量,然后将另一个项目添加到第二个列表,再次修复它。没有其他办法。

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

    解决方法是在某个对象上将 append(x,y) 设为 synchronized,并且(这是重要的部分)对于使用列表为 @987654323 的 所有其他方法 @在同一个对象上。由于在给定时间只有一个线程可以在给定对象上synchronized,因此任何其他线程都无法看到处于不一致状态的列表。

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

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

    祝你好运,玩得开心!

    【讨论】:

    • 这是一个很好的答案。但它非常很笼统,可能不是this问题的最佳答案。
    • @Cruncher,这是我对 mevtagezer 的第一句话的回答,“我正在努力解决线程安全问题......”
    【解决方案3】:

    要确定您是否需要同步,请尝试想象一个可以进行上下文切换的地方,它会破坏您的代码。

    在这种情况下,如果上下文切换发生在我放置评论的位置,那么在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));
    }
    

    就 volatile 而言,我很确定它们是必需的。你必须保证每个线程都得到一个变量的最新版本。

    这是volatile的合约。尽管它可能被同步块覆盖,并且 volatile 在这里实际上不是必需的,但无论如何编写都是好的。如果同步块已经完成了 volatile 的工作,则 VM 不会进行两次保证。这意味着 volatile 不会再花费你,它是一个非常好的闪光灯,它对程序员说:“我在多个线程中使用过”。


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

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

    【讨论】:

    • “易失性”怎么样?我需要它们作为时间戳和/或 timestampTimeZoneName 吗?以及获取和设置 someValue?
    • @mevtagezer 见最后一段
    • 因为 getter 给出了一个新的 ZonedDateTime 并且 setter 替换了私有字段。它“感觉”像 timestamp 和 timestampTimeZoneName 的 volatile 定义是不必要的。如果这些字段被用于类中的其他地方(getter/setter 除外),会不会是这样?
    • @mevtagezer 在这种情况下,这当然可能无关紧要。我不是 100% 了解细节。但是,如果这无关紧要,那么这意味着同步块完成了 volatile 变量的工作,因此 volatile 变量不会产生更多开销。这意味着您可以免费获得它。但是,在定义中包含 volatile 对程序员很有帮助。它告诉他们:“嘿,这可以跨多个线程访问!”。这是一件好事。
    • 你能把你最后的 cmets 添加到你的答案中吗?
    猜你喜欢
    • 2016-04-04
    • 1970-01-01
    • 2013-01-12
    • 2017-02-27
    • 2012-02-05
    • 2016-07-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多