正在运行的线程可以看到私有字段中的更改吗?

时间:2016-12-27 18:14:23

标签: java multithreading concurrency

我正在尝试实现一种线程池,当我启动一个Thread时,我自己的类扩展线程,它的run方法将永远运行或直到满足某些条件。

  1. 当线程正在运行时,它应该接收工作,如Callable接口。
  2. 当它检索值时,它应该通知Condition变量。
  3. private class Worker extends Thread {
        private Callable<T> toCall;
        private T result;
    
        @Override
        public void run(){
            try{
                while(true) {
                    if(toCall == null && /* Other condition accessing Outer class field */){
                        // Idle for too much time
                        break; // Finish thread run normally
                    }
    
                    if(toCall != null && /* Other condition accessing Outer class field */){
                        // Is this block reached when setCallable is invoked after this thread is running ? 
                        result = toCall.call();
                        hasResult.signal(); // Condition variable
                        toCall = null;
                    }
                }
            }catch(InterruptedException ie){
                // TODO 
            }
        }
    
        public void setCallable(Callable<T> toCall){
            this.toCall = toCall;
        }
    
        public void getResult(){
            return result;
        }
    }
    

    我的主要疑虑是:

    • 此主题是否看到toCall变量?
    • 中的更改
    • 使用此变量并将其设置为null后,对setCallable的另一次调用是否安全?
    • 最后,这是一个让线程始终运行并在需要时让它工作的好方法吗?发送工作的线程必须等待hasResult条件发出信号。

    我正在尝试实现我自己非常简单的ThreadPoolExecutor,我的方法是获得这些Worker的列表,根据需要创建它们,并重用那些没有做任何事情的列表。

    如果这种方法出错,那么获得此功能的最佳方法是什么?

    任何帮助将不胜感激,谢谢。

2 个答案:

答案 0 :(得分:2)

  

此线程是否看到toCall变量的变化?

就可见性而言,没有任何同步,答案不一定。

  

使用此变量并将其设置为null后,对setCallable的另一次调用是否安全?

这是实施可能不“安全”的地方。如果两个线程同时向工作者提交Callable会发生什么?鉴于上面的代码,它将只处理其中一个而忽略另一个。更好的选择可能是让工人轮询共享的任务队列。您可以使用类似BlockingQueue的内容来保存任务。其中一种方法允许您使用超时轮询队列。

  

最后,这是一个让线程始终运行并在需要时让它工作的好方法吗?

由于上述原因,我不这么认为。

答案 1 :(得分:1)

正如AR.3 in his answer已经指出的那样,线程可能不一定会看到toCall变量的变化。

将变量声明为volatile是使这一点安全的一种方法,但是(没有深入研究Java内存模型的细节),这可能没有必要。更准确地说:这里可能没有足够,因为显然存在涉及toCall变量的竞争条件:

   | Worker thread:                            |  External thread:
---| ------------------------------------------|--------------------------------
0: | while(true) {                             |
1: |                                           |  worker.setCallable(c);
2: |     if(toCall != null){                   |
3: |                                           |  worker.setCallable(null);
4: |         result = toCall.call(); // CRASH! |
5: |

因此,外部线程可能会将toCall变量设置为非null值。工作线程可能会找到toCall!=null,并输入if - 声明的正文。此时,外部线程可能会将toCall设置为null,当工作人员尝试执行NullPointerException时会导致toCall.call()

当然,这可以通过将对toCall变量的访问包装到同步块中来解决。然后,它不再是volatile - 这就是我上面提到的。

您的代码可能存在其他一些同步问题。您没有向我们提供所有细节,尤其是hasResult变量的作用。而且你没有说出为什么你想要自己实现这一点的特殊原因。

但总的来说:

有更简单,更优雅,更惯用的方式来达到你想要的目标。其中一条评论已经向您指出Future类,它提供了您似乎希望在那里实现的功能。根据同步的位置和方式(即等待,在这种情况下),应该有不同的选项。最简单的可能是执行者服务:

ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<T> future = executorService.submit(toCall);

// Whereever this should be consumed:
T t = future.get(); // Will wait until the result is computed

或者,CompletableFeature类提供添加“回调”的机制,当结果可用时将通知该回调。