调用者会阻塞直到getFoo()方法返回一个值吗?

11

我有一个Java Thread,它公开了其他线程想要访问的属性:

class MyThread extends Thread {
   private Foo foo;
   ...
   Foo getFoo() {
     return foo;
   }
   ...
   public void run() { 
     ...
     foo = makeTheFoo();
     ...
   }
}
问题在于,从运行此代码到foo可用需要一些时间。调用者可能会在此之前调用getFoo()并获得null。我更希望他们简单地阻塞、等待并在初始化完成后获取该值。(foo之后不再更改。)它准备就绪需要毫秒级的时间,因此我对这种方法感到满意。
现在,我可以使用wait()notifyAll()来实现这一点,有95%的机会我会做得正确。但我想知道你们会如何做;是否在java.util.concurrent中有一个原语可以做到这一点,而我却忽略了?
或者,你会怎样构建它?是的,使foo成为易失性变量。是的,在内部锁对象上同步,并将检查放在while循环中,直到它不为null。我有什么遗漏吗?
9个回答

16

如果foo仅被初始化一次,那么CountDownLatch是一个很好的选择。

class MyThread extends Thread {

  private final CountDownLatch latch = new CountDownLatch(1);

  ...

  Foo getFoo() throws InterruptedException
  {
    latch.await(); /* Or use overload with timeout parameter. */
    return foo;
  }

  @Override
  public void run() {
    foo = makeTheFoo()
    latch.countDown();
  }

}

锁提供与volatile关键字相同的可见性行为,这意味着读取线程将看到由线程分配的foo的值,即使foo没有声明为volatile


3
通常情况下,您需要使用notify()和notifyAll()方法。如果只有一个项目创建Foo并且许多线程可能等待它,则notify()方法是危险的。但我认为这里还有其他问题。
我不会把Thread作为存储Foo的地方。这样做意味着在创建Foo之后必须保留一个线程。为什么不创建另一个对象来存储Foo,并让创建线程写入它?
然后,我会让getFoo()测试foo,并且只在它非空时等待(别忘了将其与自身和foo setter同步)。

同意,但我希望不是这样。在这种情况下,我真的必须在run()内部创建对象。这是一个有奇怪需求的GUI应用程序。 - Sean Owen

1
尝试使用 CountDownLatch:
class MyThread extends Thread {
   private volatile CountDownLatch latch;
   private Foo foo;
   MyThread(){
      latch = new CountDownLatch(1);
   }
   ...
   Foo getFoo() {
     latch.await(); // waits until foo is ready
     return foo;
   }
   ...
   public void run() { 
     ...
     foo = makeTheFoo();
     latch.countDown();// signals that foo is ready
     ...
   }
}

我认为wait/notifyAll不会起作用,因为每个wait都需要一个notify。你想要通知一次,然后再也不用担心通知,那么任何调用getFoo的其他线程都将阻塞,直到foo被初始化或者如果它已经被初始化,则只需获取foo


实际上,这里有一个微妙的错误:由于latch不是finalvolatile,因此不能保证其他线程能够“看到”它。在实践中可能不是问题,但是这种保证并不会带来任何成本。 - erickson

1

我会使用 java.util.concurrent 中的任何一个 BlockingQueue,更具体地说,如果有一个线程在等待 Foo 并且有一个线程正在生产它,我会使用 SynchronousQueue。在存在更多的生产者和/或消费者的情况下,我的默认选项是 LinkedBlockingQueue,但其他实现可能更适合您的应用程序。您的代码将变成:

class MyThread extends Thread {
   private SynchronousQueue<Foo> queue = new SynchronousQueue<Foo>();
   ...
   Foo getFoo() {
     Foo foo;
     try {
        foo = queue.take();
     }
     catch (InteruptedException ex) {
        ...stuff ...
     }
     return foo;
   }
   ...
   public void run() { 
     ...
     foo = makeTheFoo();
     try {
        queue.put(foo);
     }
     catch (InteruptedException ex) {
        ...stuff ...
     }
     ...
   }
}

1
SynchronousQueue 在许多情况下都是一个方便的构造,但它并不非常适合这里的要求:foo 只被初始化一次,并且可能被读取多次。这将只允许 foo 被读取一次。尝试读取该属性的其他尝试将永远阻塞。 - erickson
是的,你说得对,我漏掉了“只初始化一次”的部分。我以为这更像是生产者/消费者的情况。 - jilles de wit

0
也许可以尝试一下我那个高级的 FutureValue 类...
import java.util.concurrent.CountDownLatch;

public class FutureValue<T> {
private CountDownLatch latch = new CountDownLatch(1);
private T value;

public void set(T value) throws InterruptedException, IllegalStateException {
    if (latch.getCount() == 0) {
        throw new IllegalStateException("Value has been already set.");
    }
    latch.countDown();
    this.value = value;
}

/**
 * Returns the value stored in this container. Waits if value is not available.
 * 
 * @return
 * @throws InterruptedException
 */
public T get() throws InterruptedException {
    latch.await();
    return value;
}

}

// Usage example
class MyExampleClass {
@SuppressWarnings("unused")
private static void usageExample() throws InterruptedException {
    FutureValue<String> futureValue = new FutureValue<>();

    // the thread that will produce the value somewhere
    new Thread(new Runnable() {

        @Override
        public void run() {
            try {
                futureValue.set("this is future");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).run();

    String valueProducedSomewhereElse = futureValue.get();
}
}

0

懒加载是一个选项吗?

synchronized Foo getFoo() {
    if (foo == null)
        foo = makeFoo();
    }
    return foo;
}

不是在这种情况下,因为foo必须在另一个线程中创建。 - Sean Owen

0

当然,我可以手动编写一个解决方案。但我认为这个问题略微复杂 -- 字段应该是“volatile”的,并且我们需要使用notifyAll()来启动 -- 所以我希望得到关于我还需要什么或者一个明确正确的预制解决方案的评论。 - Sean Owen
@Sean 在这种情况下,我会选择DJClayworth的建议。 - Robben_Ford_Fan_boy

0
据我所知,并发编程是显式创建的,不需要等待并可以立即执行你想要的操作——但如果可能的话。在你的情况下,你需要等待直到某些东西可用,所以你唯一的选择是,嗯,wait()。简而言之,似乎你描述的方式是唯一正确的方式。

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接