停车线程在服务中

时间:2018-05-16 12:07:41

标签: java multithreading concurrency parking

我正在试验线程停车并决定建立某种服务。这是它的样子:

public class TestService {
    private static final Logger logger = LoggerFactory.getLogger(TestService.class); // logback I think this logger causes some troubles

    private final CountDownLatch stopLatch;
    private final Object parkBlocker = new Object();
    private volatile boolean stopped;
    private final Thread[] workers;

    public TestService(int parallelizm) {
        stopLatch = new CountDownLatch(parallelizm);
        workers = new Thread[parallelizm];
        for (int i = 0; i < parallelizm; i++) {
            workers[i] = new Thread(() -> {
                try {
                    while (!stopped) {
                        logger.debug("Parking " + Thread.currentThread().getName());
                        LockSupport.park(parkBlocker);
                        logger.debug(Thread.currentThread().getName() + " unparked");
                    }
                } finally {
                    stopLatch.countDown();
                }
            });
        }
    }

    public void start() {
        Arrays.stream(workers).forEach(t -> {
            t.start();
            logger.debug(t.getName() + " started");
        });
    }

    public boolean stop(long timeout, TimeUnit unit) throws InterruptedException {
        boolean stoppedSuccefully = false;
        this.stopped = true;
        unparkWorkers();
        if (stopLatch.await(timeout, unit)) {
            stoppedSuccefully = true;
        }
        return stoppedSuccefully;
    }

    private void unparkWorkers() {
        Arrays.stream(workers).forEach(w -> {
            LockSupport.unpark(w);
            logger.debug("Un-park call is done on " + w.getName());
        });
    }
}

我面临的问题是,如果我按如下方式测试此服务:

public static void main(String[] args) = {
  while(true) {
    TestService service = new TestService(2);
    service.start();
    if (!service.stop(10000, TimeUnit.MILLISECONDS))
      throw new RuntimeException();
  }
}

我有时会遇到以下行为:

14:58:55.226 [main] DEBUG com.pack.age.TestService - Thread-648 started
14:58:55.227 [Thread-648] DEBUG com.pack.age.TestService - Parking Thread-648
14:58:55.227 [main] DEBUG com.pack.age.TestService - Thread-649 started
14:58:55.227 [main] DEBUG com.pack.age.TestService - Un-park call is done on Thread-648
14:58:55.227 [Thread-648] DEBUG com.pack.age.TestService - Thread-648 unparked
14:58:55.227 [main] DEBUG com.pack.age.TestService - Un-park call is done on Thread-649
14:58:55.227 [Thread-649] DEBUG com.pack.age.TestService - Parking Thread-649
Exception in thread "main" java.lang.RuntimeException
    at com.pack.age.Test$.main(Test.scala:12)
    at com.pack.age.Test.main(Test.scala)

线程停在停车场上:

"Thread-649" #659 prio=5 os_prio=0 tid=0x00007efe4433f000 nid=0x7691 waiting on condition [0x00007efe211c8000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x0000000720739a68> (a java.lang.Object)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at com.pack.age.TestService.lambda$new$0(TestService.java:27)
    at com.pack.age.TestService$$Lambda$1/1327763628.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)

我没有在停车场看到任何比赛。此外,如果在unpark之前调用park,则park保证不会阻止(这就是javadoc所说的)。

也许我误用了LockSupport::park。你能建议任何修复吗?

1 个答案:

答案 0 :(得分:3)

这与记录器无关,尽管它的用法使问题浮出水面。就这么简单,您就有了比赛条件。在解释种族状况之前,您需要首先了解LockSupport::unpark文档中的一些知识:

使给定线程的许可(如果尚不可用)可用。如果线程在驻留时被阻止,则它将取消阻止。 否则,它的下一个停泊呼叫保证不会阻塞。

第一点是解释here。简短的版本是:如果您已经启动了thread,但尚未 调用park,并且在这段时间内(介于{{1 }}和start),其他一些线程在第一个调用park:该线程根本不会驻留。许可证将立即可用。也许这张小图可以使它更清晰:

unpark

请注意,(ThreadA) start ------------------ park --------- .... (ThreadB) start ----- unpark ----- ThreadB呼叫unpark(ThreadA)ThreadA的期间之间如何呼叫start。这样,当park到达ThreadA时:保证不会阻塞,就像文档中所说的一样。

同一文档中的第二点是:

如果未启动给定线程,则不能保证此操作完全无效。

让我们通过绘图来看看:

park

Thread B calls unpark(ThreadA) --- Thread A starts --- Thread A calls park 调用ThreadA之后,它将永远挂起,因为park再也不会对其调用ThreadB。请注意,对unpark的调用是在 unpark开始之前进行的(与前面的示例不同)。

这正是您的情况:

(来自ThreadA

LockSupport.unpark(w);unparkWorkers之前被称为 t.start();。用简单的话来说-您的代码在public void start(){...}之前都在unpark上都调用了workers,因此当它们最终到达park时,它们被卡住了,没有人被发现能够unpark。您用logger而不是System::out看到此事实的事实很可能与您在println时遇到的情况有关–幕后有一个synchronized方法。


事实上,LockSupport恰好提供了证明这一点的语义。为此,我们需要(为简单起见:SOProblem service = new SOProblem(1);

static class ParkBlocker {

    private volatile int x;

    public ParkBlocker(int x) {
        this.x = x;
    }

    public int getX() {
        return x;
    }
}

现在我们需要将其插入适当的方法中。首先标记我们称为unpark的事实:

private void unparkWorkers() {
    Arrays.stream(workers).forEach(w -> {
        LockSupport.unpark(w);
        logger.debug("Un-park call is done on " + w.getName());
    });
    /*
     * add "1" to whatever there is already in pb.x, meaning
     * we have done unparking _also_
     */
    int y = pb.x;
    y = y + 1;
    pb.x = y;
}

然后在周期结束后重置标志:

public boolean stop(long timeout, TimeUnit unit) throws InterruptedException {
    boolean stoppedSuccefully = false;
    stopped = true;
    unparkWorkers();
    if (stopLatch.await(timeout, unit)) {
        stoppedSuccefully = true;
        // reset the flag
        pb.x = 0;
    }
    return stoppedSuccefully;
}

然后将构造函数更改为标记线程已开始:

  .....
  while (!stopped) {
       logger.debug("Parking " + Thread.currentThread().getName());
       // flag the fact that thread has started. add "2", meaning
       // thread has started
       int y = pb.x;
       y = y + 2;
       pb.x = y;
       LockSupport.park(pb);
       logger.debug(Thread.currentThread().getName() + " unparked");
  }

然后,当线程冻结时,您需要检查标志:

 public static void main(String[] args) throws InterruptedException {
    while (true) {
        SOProblem service = new SOProblem(1); // <-- notice a single worker, for simplicity
        service.start();
        if (!service.stop(10000, TimeUnit.MILLISECONDS)) {
            service.debug();
            throw new RuntimeException();
        }
    }
}

其中debug方法是:

public void debug() {
    Arrays.stream(workers)
          .forEach(x -> {
              ParkBlocker pb = (ParkBlocker) LockSupport.getBlocker(x);
              if (pb != null) {
                  System.out.println("x = " + pb.getX());
              }
          });
}

问题再次出现时,您在呼叫unpark之前先呼叫park ,这是在x = 3作为输出时发生的。