【问题标题】:Is this Command Pattern Thread-safe?这个命令模式是线程安全的吗?
【发布时间】:2013-10-23 03:32:36
【问题描述】:

您好 Stackoverflow 社区,

我有一个关于线程安全的问题。如果我有一个静态地图并用不同的对象填充它,这些对象线程安全吗?如果我只有他们不写的方法?

我创建一个小例子:这种情况下getCommand线程的返回值是否安全?

如何使用 JUnit 测试线程安全性?

控制器

public class CommandController {

    private static Map<String, Command> commandMap = initMap();


    public static Map<String, Command> initMap() {
         return new HashMap<String, Command>() {{
             put("A", new CommandA());
             put("B", new CommandB());
         }};
    }


    public Command getCommand(String key) {
        if(commandMap.containsKey(key)) {
            return commandMap.get(key);
        }
        return null;
    }

}

抽象类

public abstract class Command {

    public abstract int calc(int value);

}

命令 A

public class CommandB extends Command {
    @Override
    public int calc(int value) {
        value = value * 4;
        return value; 
    }
}

命令 B

public class CommandA extends Command {
    private int ab = 5;

    @Override
    public int calc(int value) {
        return value * ab;
    }
}

【问题讨论】:

  • HashMap 不是线程安全的。不可变对象是,但你应该使用final
  • 为什么不public Command getCommand(String key) { return commandMap.get(key); } ? (如果键不在地图中,则返回 null)
  • @assylias 这种方法会更简单,性能更高,但我认为它不会对线程安全产生任何影响(尤其是在使用 HashMap 时)。
  • 这里真正的问题是类加载/初始化是否会创建同步点、内存屏障或内存刷新点。如果线程 A 导致 CommandController.class 被加载并初始化其静态字段,线程 B 会看到 CommandController 的静态字段的最新值吗?特别是如果没有明确的同步点,例如“volatile”、“final”或“synchronized”。我不知道。
  • @MikeClark 确实如此。在类完全初始化之前发生的写入在类完全初始化后可见。

标签: java multithreading design-patterns


【解决方案1】:

这是线程安全的,原因有二。在这种情况下,两者都需要考虑才能获得纯线程安全

  1. 地图是不可变的(因为它是只读的)。
  2. 它是用类初始化的。 Since class initialization is thread-safe and guarantees publication, visibility is not an issue.

注意: Slaks 确实提出了一个很好的观点。您应该使用 final。通常,如果您担心线程安全并且该字段既不是 final 也不是 volatile,则可能有问题。尽管在这种情况下将其设为 final 并不会使其更加线程安全,但它只是防止将来发生线程不安全的事情(例如重新分配它)。

【讨论】:

    【解决方案2】:

    是的,这是线程安全的,因为类初始化保证对所有使用该类的线程可见,并且您的映射“实际上是不可变的”——它的状态在类初始化后不会改变。

    但是,如果您从程序在设置阶段显式调用的某个静态方法初始化映射,则必须实现自己的内存屏障以确保其他线程可以看到映射的正确状态。因此,请确保在类初始化期间完全初始化地图;这就是它的工作原理。

    【讨论】:

      【解决方案3】:

      是的。 Java 语言规范writes

      如果一个程序没有数据竞争,那么该程序的所有执行将看起来是顺序一致的。

      如果对同一个变量的两次冲突访问不在发生前的关系中,则会发生数据竞争,并且

      如果至少有一次访问是写入,则对同一变量的两次访问(读取或写入)称为冲突。

      对map的并发访问只读取共享状态,读取只能与写入冲突。因此,证明映射的初始化发生在并发线程访问之前就足够了。

      确实如此,因为初始化发生在静态字段初始化程序中,该初始化程序在类初始化期间进行处理。规范requires 是在调用它声明的方法之前初始化一个类,detailed initialization procedure 采用同步来确保初始化只发生一次,并且初始化线程与访问该类的所有其他线程同步,从而建立发生在之前。

      作为一种风格,您可能希望将字段声明为 final 以强调它仅在类加载时分配,并且对该字段的访问不需要进一步同步。

      【讨论】:

        【解决方案4】:

        这是线程安全的,因为没有人可以访问您的Map,因此无法对其进行变异。但是,您可能希望将其设为 private static final,以确保没有内存可见性问题。

        我一直在做这种事情(但不是使用 static 映射)- 我使用 Spring 填充 Map

        【讨论】:

        【解决方案5】:

        对我来说似乎线程安全。 CommandController 中的地图没有添加/删除任何内容。并且命令(CommandA 和 CommandB)没有修改的私有变量(它们仅用于计算。

        我猜这是一个(更)复杂情况的简单示例,因此当您的实际情况在 CommandController 中操纵地图时,或者当 Command 确实有修改的类变量时,您将遇到并发问题.

        【讨论】:

        • 在一般情况下,仅确保不添加或删除任何内容是不够的;您必须确保将一个线程所做的更改写入主内存,并且读取线程在初始化后至少清除一次缓存并从主内存读取一次。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-08-03
        • 1970-01-01
        • 2012-05-20
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多