簡體   English   中英

具有唯一項和線程池的線程安全 FIFO 隊列

[英]Thread-safe FIFO queue with unique items and thread pool

我必須管理系統中的計划文件復制。 文件復制由用戶安排,我需要限制復制期間使用的系統資源量。 每次復制可能花費的時間量未定義(即復制可能被安排為每 15 分鍾運行一次,而在下一次運行到期時上一次運行可能仍在運行)並且如果復制已經排隊,則不應排隊或運行。

我有一個調度程序,它會定期檢查到期的文件復制,並且對於每個復制,(1) 如果它沒有排隊也沒有運行,則將其添加到阻塞隊列中,或者 (2) 否則將其刪除。

private final Object scheduledReplicationsLock = new Object();
private final BlockingQueue<Replication> replicationQueue = new LinkedBlockingQueue<>();
private final Set<Long> queuedReplicationIds = new HashSet<>();
private final Set<Long> runningReplicationIds = new HashSet<>();

public boolean add(Replication replication) {

    synchronized (scheduledReplicationsLock) {
        // If the replication job is either still executing or is already queued, do not add it.
        if (queuedReplicationIds.contains(replication.id) || runningReplicationIds.contains(replication.id)) {
            return false;
        }
        replicationQueue.add(replication)
        queuedReplicationIds.add(replication.id);
    }

我還有一個線程池,等待隊列中有復制並執行它。 下面是線程池中各個線程的main方法:

public void run() {
    while (True) {
        Replication replication = null;
        synchronized (scheduledReplicationsLock) {
            // This will block until a replication job is ready to be run or the current thread is interrupted.
            replication = replicationQueue.take();

            // Move the ID value out of the queued set and into the active set
            Long replicationId = replication.getId();
            queuedReplicationIds.remove(replicationId);
            runningReplicationIds.add(replicationId);
        }
        executeReplication(replication)
    }
} 

此代碼陷入死鎖,因為線程輪詢中的第一個線程將獲得 scheduleLock 並阻止調度程序向隊列添加復制。 將 replicationQueue.take() 移出同步塊將消除死鎖,但隨后可能會從隊列中刪除元素並且散列集沒有用它進行原子更新,這可能導致復制被錯誤刪除。

如果隊列為空,我應該使用 BlockingQueue.poll() 並釋放鎖 + 睡眠而不是使用 BlockingQueue.take() 嗎?

歡迎對當前解決方案或滿足要求的其他解決方案進行修復。

等待/通知

保持相同的控制流,而不是在持有互斥鎖的同時BlockingQueue實例,您可以wait scheduledReplicationsLock通知,迫使工作線程釋放鎖並返回等待池。

這是您的生產商的簡化樣本:

private final List<Replication> replicationQueue = new LinkedList<>();
private final Set<Long> runningReplicationIds = new HashSet<>();

public boolean add(Replication replication) {
    synchronized (replicationQueue) {
        // If the replication job is either still executing or is already queued, do not add it.
        if (replicationQueue.contains(replication) || runningReplicationIds.contains(replication.id)) {
            return false;
        } else {
            replicationQueue.add(replication);
            replicationQueue.notifyAll();
        }
    }
}

工作Runnable將更新如下:

public void run() {
    synchronized (replicationQueue) {
        while (true) {
            if (replicationQueue.isEmpty()) {
                scheduledReplicationsLock.wait();
            }
            if (!replicationQueue.isEmpty()) {
                Replication replication = replicationQueue.poll();
                runningReplicationIds.add(replication.getId())
                executeReplication(replication);
            }
        }
    }
} 

阻塞隊列

通常,您最好使用BlockingQueue來協調您的生產者和復制工作池。

BlockingQueue是,顧名思義,自然阻塞,並會導致調用線程阻塞僅當項目無法拉/從/推到了隊列中。

同時,請注意,您將不得不更新您的運行/入隊狀態管理,因為您只會同步BlockingQueue項目,刪除任何約束。 這將取決於上下文,這是否可以接受。

這樣,您將刪除所有其他使用過的互斥鎖並在BlockingQueue上使用作為您的同步狀態:

private final BlockingQueue<Replication> replicationQueue = new LinkedBlockingQueue<>();

public boolean add(Replication replication) {
    // not sure if this is the proper invariant to check as at some point the replication would be neither queued nor running while still have been processed
    if (replicationQueue.contains(replication)) {
        return false;
    }
    // use `put` instead of `add` as this will block waiting for free space
    replicationQueue.put(replication);
    return true;
}

工人隨后將take從無限期BlockingQueue

public void run() {
    while (true) {
        Replication replication = replicationQueue.take();
        executeReplication(replication);
    }
} 

如果您使用 BlockingQueue,則無需使用任何額外的同步塊

引用自 docs ( https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/BlockingQueue.html )

BlockingQueue 實現是線程安全的 所有排隊方法都使用內部鎖或其他形式的並發控制以原子方式實現其效果。

只需使用這樣的東西

public void run() {
    try {
        while (replicationQueue.take()) { //Thread will be wait for the next element in the queue
          Long replicationId = replication.getId();
          queuedReplicationIds.remove(replicationId);
          runningReplicationIds.add(replicationId);
          executeReplication(replication);
        }
    } catch (InterruptedException ex) {
      //if interrupted while waiting next element
    }
}

}

查看 javadoc https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/LinkedBlockingQueue.html#take()

或者您可以將 BlockinQueue.pool() 與超時設置一起使用

UPD:經過討論,我使用兩個 ConcurrentHashSet 擴展 LinkedBlockingQueue 並添加方法 afterTake() 以刪除處理的副本。 您不需要隊列外的額外同步。 只需將副本放在第一個線程中並在另一個線程中獲取它,並在復制完成后調用 afterTake() 。 如果你想使用它,你需要覆蓋其他方法。

package ru.everytag;

import io.vertx.core.impl.ConcurrentHashSet;

import java.util.concurrent.LinkedBlockingQueue;

public class TwoPhaseBlockingQueue<E> extends LinkedBlockingQueue<E> {
  private ConcurrentHashSet<E> items = new ConcurrentHashSet<>();
  private ConcurrentHashSet<E> taken = new ConcurrentHashSet<>();

@Override
public void put(E e) throws InterruptedException {
    if (!items.contains(e)) {
        items.add(e);
        super.put(e);
    }
}

public E take() {
    E item = take();

    taken.add(item);
    items.remove(item);

    return item;
}

public void afterTake(E e) {
    if (taken.contains(e)) {
        taken.remove(e);
    } else if (items.contains(e)) {
        throw new IllegalArgumentException("Element still in the queue");
    }
}
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM