唤醒一个线程而不冒被阻塞的风险

Waking up a thread without risking to get blocked

我有一个无限期工作线程运行,如果无事可做,它会休眠一分钟。有时,另一段代码产生了一些工作,想要立即唤醒工作线程。

所以我做了这样的事情(代码仅供说明):

class Worker {
    public void run() {
        while (!shuttingDown()) {
           step();
        }
    }

    private synchronized void step() {
        if (hasWork()) {
            doIt();
        } else {
            wait(60_000);
        }
    }

    public synchronized wakeMeUpInside() {
        notify();
    }
}

我不喜欢的是只为了唤醒某些东西而不得不进入监视器,这意味着通知线程可能会无缘无故地延迟。由于本机同步的选择有限,我想我会切换到 Condition,但它有 exactly the same problem:

An implementation may (and typically does) require that the current thread hold the lock associated with this Condition when this method is called.

这是一个基于信号量的解决方案:

class Worker {
    // If 0 there's no work available
    private workAvailableSem = new Semaphore(0);

    public void run() {
        while (!shuttingDown()) {
           step();
        }
    }

    private synchronized void step() {
        // Try to obtain a permit waiting up to 60 seconds to get one
        boolean hasWork = workAvailableSem.tryAquire(1, TimeUnit.MINUTES);
        if (hasWork) {
            doIt();
        }
    }

    public wakeMeUpInside() {
        workAvailableSem.release(1);
    }
}

我不能 100% 确定这是否满足您的需求。需要注意的几点:

  • 这将在每次调用 wakeMeUpInside 时添加一个许可证。因此,如果两个线程唤醒 Worker 它将 运行 doIt 两次而不会阻塞。您可以扩展示例以避免这种情况。
  • 这需要等待 60 秒才能完成工作。如果 none 可用,它将返回到 run 方法中,该方法将立即将其发送回 step 方法,后者将再次等待。我这样做是因为我假设您有某种原因想要每 60 秒 运行,即使没有工作。如果不是这种情况,请致电 aquire,您将无限期等待工作。

根据下面的评论,OP 只想 运行 一次。虽然您可以在这种情况下调用 drainPermits,但更简洁的解决方案是像这样使用 LockSupport

class Worker {
    // We need a reference to the thread to wake it
    private Thread workerThread = null;
    // Is there work available
    AtomicBoolean workAvailable = new AtomicBoolean(false);

    public void run() {
        workerThread = Thread.currentThread();
        while (!shuttingDown()) {
           step();
        }
    }

    private synchronized void step() {
        // Wait until work is available or 60 seconds have passed
        ThreadSupport.parkNanos(TimeUnit.MINUTES.toNanos(1));
        if (workAvailable.getAndSet(false)) {
            doIt();
        }
    }

    public wakeMeUpInside() {
        // NOTE: potential race here depending on desired semantics.
        // For example, if doIt() will do all work we don't want to
        // set workAvailable to true if the doIt loop is running.
        // There are ways to work around this but the desired
        // semantics need to be specified. 
        workAvailable.set(true);
        ThreadSupport.unpark(workerThread);
    }
}