【问题标题】:Java instance members and concurrencyJava 实例成员和并发
【发布时间】:2015-01-13 14:44:06
【问题描述】:

鉴于我对 java 中并发的理解,似乎只有当线程访问给定对象(例如 servlet)的同一实例时,对实例成员的共享访问才能处理多线程访问。请参见此处: Why instance variable in Servlet is not thread-safe

由于并非所有应用程序都是基于 servlet,你如何确定哪些对象需要适应多线程访问?例如,在一个大型的、非基于 servlet 的企业应用程序中,考虑到类的绝对数量,您如何从设计的角度确定哪些对象在运行时将只有一个实例在多个线程之间共享?我能想到的唯一情况就是单身。

在 Java 的 EL API 中,javax.el.BeanELResolver 有一个私有内部类,它使用同步来序列化对其成员之一的访问。除非我遗漏了什么,否则 BeanELResolver 看起来并不像单例,因此每个线程都应该有自己的 BeanELResolver 实例。同步其成员之一背后的设计考虑因素是什么?

【问题讨论】:

  • 同步是一种安全措施。如果实例的 reference 转义,您应该在多线程环境中使用它。
  • 在编写类之前你应该知道实例是否会被并发使用。这是编码人员必须在其实现中允许的应用程序设计选择。
  • 这是一个复杂的主题 - 最好的建议是阅读 Brian Goetz 的 Java Concurrency in Practice - 所有你需要知道的都在里面。

标签: java multithreading concurrency


【解决方案1】:

在很多情况下,一个类的状态可以在多个线程之间共享,而不仅仅是单例。例如,您可以让一个类或方法创建对象(某种工厂)并在所有创建的对象中注入相同的依赖项。注入的依赖将在所有调用工厂方法的线程之间共享。依赖关系可以是任何东西:计数器、数据库访问类等。

例如:

class ThreadSafeCounter{
  /* constructor omitted */
  private final String name;
  private final AtomicInteger i = new AtomicInteger();
  int increment() { return i.incrementAndGet(); }
}

class SheepTracker {
  public SheepTracker(ThreadSafeCounter c) { sheepCounter = c;}
  private final ThreadSafeCounter sheepCounter;
  public int addSheep() { return c.increment(); }
}

class SheepTrackerFactory {
  private final ThreadSafeCounter c;
  public SheepTracker newSheepAdder() {
    return new SheepTracker(c);
  }
}

在上面,SheepTrackerFactory 可以被许多需要做同样事情的线程使用,即跟踪绵羊。所有线程中的绵羊数量都保存在一个全局状态变量ThreadSafeCounter 中(在这个例子中它可能只是一个AtomicInteger,但请耐心等待,你可以想象这个类如何包含额外的状态/操作)。现在每个SheepTracker 都可以是一个轻量级的类,执行其他不需要同步的操作,但是当它们需要增加羊的数量时,它们会以线程安全的方式进行。

【讨论】:

    【解决方案2】:

    你问的问题很笼统,所以我会试着用一个笼统的答案来回答。早在您深入学习类之前,您的设计必须考虑的第一件事就是应用程序线程的设计。在这一步中,您将考虑手头的任务,以及如何最好地利用必须解决它的硬件。基于此,您可以为您的应用程序选择最佳的线程设计。

    例如 - 应用程序是否执行密集计算?如果是这样,部分计算是否可以并行化以更好地利用多核 CPU?如果是这样,请确保在不同的内核上并行设计多个线程计算。

    您的应用程序是否执行大量 I/O 操作?如果是这样,最好将它们并行化,以便多个线程可以处理输入/输出(这很慢并且需要大量等待外部设备),而其他线程继续处理自己的任务。这就是 servlet 由多个线程并行执行的原因。

    一旦确定了要并行化的任务和希望在单个线程中执行的任务,您就可以开始设计类本身了。现在很清楚您的软件的哪些部分必须是线程安全的,哪些不是。您有一个负责 I/O 的线程池正在访问的数据结构?它必须是线程安全的。您有一个由执行维护任务的单个线程访问的对象?不一定。

    无论如何,这与单例无关。 Singlton 是一种设计模式,意味着只能创建某个对象的单个实例。它没有说明访问它或其成员的线程数。

    【讨论】:

      【解决方案3】:

      任何实例都可以在线程之间共享,而不仅仅是单例。

      这就是为什么很难想出一种设计,让开发团队中的任何人都可以立即看到哪些类型或实例将在线程之间共享,哪些不会。完全不可能阻止共享某些实例。所以解决方案必须在其他地方。阅读“memory barriers”了解详情。

      同步用于两个目的:

      1. 定义内存屏障(即何时更改对其他线程可见)
      2. 确保复杂的数据结构可以在线程之间共享(即锁定)。

      由于无法阻止人们在不同线程之间共享单个 BeanELResolver 实例,因此他们可能需要确保并发访问不会破坏某些复杂结构(可能是 Map)。

      【讨论】:

      • 没有String 可以是一个单身。根据定义,单例是其类的唯一实例。即使在最简单的 Java 程序中,也有数千个不同的 String 实例。 (例如,每个类名、字段名和方法名都是一个字符串。)
      • @jameslarge:我提到了String,因为有些人将不可变或值类型与单例混淆了。通常即使public final static String 常量也不是单例(如果您从外部类引用javac 会创建一个副本,例如在注释中)。
      • 我认为我们对“单例”的含义并不一致。如果我有一个方便的副本,我会选择“设计模式”的定义,但我很确定维基百科的定义是等价的:en.wikipedia.org/wiki/Singleton_pattern 根据这个定义,说“通常...... ":没有 Java String 可以永远成为单例。
      • @jameslarge:您可以像任何其他对象一样创建 String 的单个实例,并像任何其他单例一样传递它。例如在 Spring 中:@Bean String public foo() { return "foo"; } 将创建一个单例。
      • 您的示例中的方法 foo() 不会创建单例:它返回一个字符串文字。每次调用它都会返回 same 字符串,但这不是“单例”的意思。 @Bean 注解将导致 Spring 框架在某个时候调用 foo(),并且 Spring 会将字符串文字合并到一个新的 bean 中。我不知道bean是否是单例,因为我不知道Spring,但字符串本身不是单例。它不能是单例,因为它不是其类的唯一实例。
      猜你喜欢
      • 1970-01-01
      • 2014-07-19
      • 1970-01-01
      • 1970-01-01
      • 2015-08-23
      • 1970-01-01
      • 1970-01-01
      • 2017-07-26
      • 1970-01-01
      相关资源
      最近更新 更多