如何在Java中创建线程安全的一次性read-many值?

时间:2010-03-11 21:14:01

标签: java multithreading

这是我在处理更复杂系统时经常遇到的问题,而且我从来没有想过要解决的好方法。它通常涉及共享对象主题的变体,其构造和初始化必然是两个不同的步骤。这通常是因为体系结构要求,类似于applet,因此建议我合并构造和初始化的答案是没有用的。系统最迟必须以Java 4为目标,因此建议仅在以后的JVM中提供支持的答案也没有用。

举个例子,假设我有一个结构化的类,以适应这样的应用程序框架:

public class MyClass
{

private /*ideally-final*/ SomeObject someObject;

MyClass() {
    someObject=null;
    }

public void startup() {
    someObject=new SomeObject(...arguments from environment which are not available until startup is called...);
    }

public void shutdown() {
    someObject=null; // this is not necessary, I am just expressing the intended scope of someObject explicitly
    }
}

我无法使someObject成为final,因为在调用startup()之前无法设置它。但我真的希望它反映它的一次写入语义,并能够从多个线程直接访问它,最好避免同步。

我的想法是表达并强制执行一定程度的终结,我猜想我可以创建一个通用的容器,就像这样( UPDATE - 更正此类的线程化代码):

public class WormRef<T>
{
private volatile T                      reference;                              // wrapped reference

public WormRef() {
    reference=null;
    }

public WormRef<T> init(T val) {
    if(reference!=null) { throw new IllegalStateException("The WormRef container is already initialized"); }
    reference=val;
    return this;
    }

public T get() {
    if(reference==null) { throw new IllegalStateException("The WormRef container is not initialized"); }
    return reference;
    }

}

然后在MyClass,上面,执行:

private final WormRef<SomeObject> someObject;

MyClass() {
    someObject=new WormRef<SomeObject>();
    }

public void startup() {
    someObject.init(new SomeObject(...));
    }

public void sometimeLater() {
    someObject.get().doSomething();
    }

这为我提出了一些问题:

  1. 有更好的方法,还是现有的Java对象(必须在Java 4中提供)?
  2. 其次,就线程安全而言:

    1. 此线程安全 提供 ,在调用someObject.get()之前,没有其他线程访问set()。其他线程只会在startup()和shutdown()之间调用MyClass上的方法 - 框架保证这一点。
    2. 鉴于完全不同步的WormReference容器,在任一JMM下都可以看到object的值既不是null也不是对SomeObject的引用?换句话说,JMM是否始终保证没有线程可以观察到对象的内存是分配对象时堆上发生的任何值。我认为答案是肯定的,因为分配显式地将分配的内存归零 - 但CPU缓存是否会导致在给定的内存位置观察到其他内容?
    3. 是否足以使WormRef.reference为volatile以确保正确的多线程语义?
    4. 请注意,此问题的主要内容是如何表达并强制执行someObject的最终性,而无法对其进行实际标记final;次要是线程安全所必需的。也就是说,不要过于担心线程安全问题。

12 个答案:

答案 0 :(得分:1)

理论上,如下所述重写startup()就足够了:

public synchronized void startup() {
    if (someObject == null) someObject = new SomeObject();
}

顺便说一下,虽然WoRmObject是最终的,但线程仍然可以多次调用set()。你真的需要添加一些同步。

更新:我玩了一下它并创建了一个SSCCE,您可能会觉得用它来玩它有用:)

package com.stackoverflow.q2428725;

import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Test {

    public static void main(String... args) throws Exception {
        Bean bean = new Bean();
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(4);
        executor.schedule(new StartupTask(bean), 2, TimeUnit.SECONDS);
        executor.schedule(new StartupTask(bean), 2, TimeUnit.SECONDS);
        Future<String> result1 = executor.submit(new GetTask(bean));
        Future<String> result2 = executor.submit(new GetTask(bean));
        System.out.println("Result1: " + result1.get());
        System.out.println("Result2: " + result2.get());
        executor.shutdown();
    }

}

class Bean {

    private String property;
    private CountDownLatch latch = new CountDownLatch(1);

    public synchronized void startup() {
        if (property == null) {
            System.out.println("Setting property.");
            property = "foo";
            latch.countDown();
        } else {
            System.out.println("Property already set!");
        }
    }   

    public String get() {
        try {
            latch.await();
        } catch (InterruptedException e) {
            // handle.
        }
        return property;
    }

}

class StartupTask implements Runnable {

    private Bean bean;

    public StartupTask(Bean bean) {
        this.bean = bean;
    }

    public void run() {
        System.out.println("Starting up bean...");
        bean.startup();
        System.out.println("Bean started!");
    }

}

class GetTask implements Callable<String> {

    private Bean bean;

    public GetTask(Bean bean) {
        this.bean = bean;
    }

    public String call() {
        System.out.println("Getting bean property...");
        String property = bean.get();
        System.out.println("Bean property got!");
        return property;
    }

}

CountDownLatch将导致所有await()次来电被阻止,直至倒计时到达零。

答案 1 :(得分:1)

我首先宣布您的someObject volatile

private volatile SomeObject someObject;

Volatile关键字会创建一个内存屏障,这意味着在引用someObject时,单独的线程将始终看到更新的内存。

在您当前的实现中,即使在调用null之后,某些线程仍可能将someObject视为startup

实际上,volatile包中声明的集合会大量使用这种java.util.concurrent技术。

正如其他一些海报在此提出的那样,如果所有其他海报失败都会回归到完全同步。

答案 2 :(得分:1)

我会删除WoRmObject中的setter方法,并提供一个抛出异常if (object != null)的同步init()方法

答案 3 :(得分:1)

考虑在您尝试创建的此对象容器中使用AtomicReference作为委托。例如:

public class Foo<Bar> {
private final AtomicReference<Bar> myBar = new AtomicReference<Bar>();
 public Bar get() {
  if (myBar.get()==null) myBar.compareAndSet(null,init());
  return myBar.get();
 }

 Bar init() { /* ... */ }
 //...
}

EDITED:用一些延迟初始化方法设置一次。它不适合阻止对(可能是昂贵的)init()的多次调用,但可能会更糟。您可以将myBar的实例化粘贴到构造函数中,然后添加一个允许赋值的构造函数,如果提供了正确的信息。

有一些关于线程安全,单例实例化(与你的问题非常相似)的一般讨论,例如this site

答案 4 :(得分:1)

根据您对框架的描述,它很可能是线程安全的。在调用myobj.startup()和使myobj可用于其他线程之间某处存在内存障碍。这保证了startup()中的写入对其他线程可见。因此,您不必担心线程安全,因为框架会这样做。虽然没有免费的午餐;每当另一个线程通过框架获得myobj的访问权限时,它必须涉及同步或易失性读取。

如果您查看框架并列出路径中的代码,您将在适当的位置看到sync / volatile,使您的代码线程安全。也就是说,如果框架正确实现。

让我们检查一个典型的swing示例,其中一个工作线程进行一些计算,将结果保存在全局变量x中,然后发送一个重绘事件。接收重绘事件时的GUI线程,从全局变量x读取结果,并相应地重新绘制。

工作线程和重绘代码都不会对任何内容进行任何同步或易失性读/写操作。必须有成千上万的这样的实现。幸运的是,即使程序员没有特别注意,他们都是线程安全的。为什么?因为事件队列是同步的;我们有一个很好的发生前链:

write x - insert event - read event - read x

因此,通过事件框架隐式地写​​入x和读取x已正确同步。

答案 5 :(得分:0)

  1. 同步怎么样?
  2. 不,它不是线程安全的。如果没有同步,变量的新状态可能永远不会传递给其他线程。
  3. 是的,据我所知,引用是原子的,所以你会看到null或引用。请注意,引用对象的状态是完全不同的故事

答案 6 :(得分:0)

你能使用一个只允许每个线程的值设置一次的ThreadLocal吗?

答案 7 :(得分:0)

有很多错误的方法可以进行延迟实例化,尤其是在Java中。

简而言之,天真的方法是创建一个私有对象,一个公共同步的init方法,以及一个公共的非同步get方法,它对您的对象执行空检查,并在必要时调用init。问题的复杂性在于以线程安全的方式执行空检查。

本文应该有用:http://en.wikipedia.org/wiki/Double-checked_locking

在Java中,这个特定的主题在Doug Lea的“Java中的并发编程”中得到了深入的讨论,它有些过时,并且在Lea和其他人的共同撰写的“Java Concurrency in Practice”中进行了讨论。特别是,CPJ是在Java 5发布之前发布的,它大大改进了Java的并发控制。

当我回到家并可以访问所述书籍时,我可以发布更多细节。

答案 8 :(得分:0)

这是我的最终答案,Regis 1

/**
 * Provides a simple write-one, read-many wrapper for an object reference for those situations
 * where you have an instance variable which you would like to declare as final but can't because
 * the instance initialization extends beyond construction.
 * <p>
 * An example would be <code>java.awt.Applet</code> with its constructor, <code>init()</code> and
 * <code>start()</code> methods.
 * <p>
 * Threading Design : [ ] Single Threaded  [x] Threadsafe  [ ] Immutable  [ ] Isolated
 *
 * @since           Build 2010.0311.1923
 */

public class WormRef<T>
extends Object
{

private volatile T                      reference;                              // wrapped reference

public WormRef() {
    super();

    reference=null;
    }

public WormRef<T> init(T val) {
    // Use synchronization to prevent a race-condition whereby the following interation could happen between three threads
    //
    //  Thread 1        Thread 2        Thread 3
    //  --------------- --------------- ---------------
    //  init-read null
    //                  init-read null
    //  init-write A
    //                                  get A
    //                  init-write B
    //                                  get B
    //
    // whereby Thread 3 sees A on the first get and B on subsequent gets.
    synchronized(this) {
        if(reference!=null) { throw new IllegalStateException("The WormRef container is already initialized"); }
        reference=val;
        }
    return this;
    }

public T get() {
    if(reference==null) { throw new IllegalStateException("The WormRef container is not initialized"); }
    return reference;
    }

} // END PUBLIC CLASS

(1)由Regis Philburn主持的游戏节目“所以你想成为百万富翁”。

答案 9 :(得分:0)

基于AtomicReference的我的小版本。它可能不是最好的,但我相信它干净且易于使用:

public static class ImmutableReference<V> {
    private AtomicReference<V> ref = new AtomicReference<V>(null);

    public boolean trySet(V v)
    {
        if(v == null)
            throw new IllegalArgumentException("ImmutableReference cannot hold null values");

        return ref.compareAndSet(null, v);
    }

    public void set(V v)
    {
        if(!trySet(v)) throw new IllegalStateException("Trying to modify an immutable reference");
    }

    public V get()
    {
        V v = ref.get();
        if(v == null)
            throw new IllegalStateException("Not initialized immutable reference.");

        return v;
    }

    public V tryGet()
    {
        return ref.get();
    }
}

答案 10 :(得分:0)

第一个问题:为什么你不能只启动私有方法,在构造函数中调用,然后它可以是最终的。这将在调用构造函数之后确保线程安全,因为它在之前是不可见的,并且仅在构造函数返回后读取。或者重新考虑类结构,以便启动方法可以创建MyClass对象作为其构造函数的一部分。在可能的情况下,这个特殊情况看起来像一个结构很差的情况,你真的只想让它成为最终的和不可改变的。

简单方法,如果类是不可变的,并且只有在创建后才读取,然后将它从guava包装在一个不可变列表中。您还可以创建自己的不可变包装器,在要求返回引用时进行防御性复制,这样可以防止客户端更改引用。如果它在内部是不可变的,则不需要进一步的同步,并且允许不同步的读取。您可以根据请求将您的包装器设置为防御性地复制,因此即使尝试写入它也会干净地失败(它们只是不做任何事情)。您可能需要一个内存屏障,或者您可以进行延迟初始化,但请注意延迟初始化可能需要进一步同步,因为在构造对象时可能会获得几个不同步的读取请求。

稍微涉及的方法将涉及使用枚举。由于枚举是保证单一的,因此一旦创建枚举,它就会永久固定。您仍然必须确保该对象在内部是不可变的,但它确实保证其单例状态。没有多少努力。

答案 11 :(得分:-1)

以下课程可以回答您的问题。通过在提供的泛型中使用volatile中间变量和final值守门员来实现一些线程安全性。您可以考虑使用synchronized setter / getter进一步增加它。希望它有所帮助。

https://stackoverflow.com/a/38290652/6519864