Java: ExecutorService chặn khi gửi sau một kích thước hàng đợi nhất định


82

Tôi đang cố gắng viết mã một giải pháp trong đó một luồng duy nhất tạo ra các tác vụ chuyên sâu I / O có thể được thực hiện song song. Mỗi tác vụ có dữ liệu trong bộ nhớ quan trọng. Vì vậy, tôi muốn có thể giới hạn số lượng nhiệm vụ đang chờ xử lý tại một thời điểm.

Nếu tôi tạo ThreadPoolExecutor như thế này:

    ThreadPoolExecutor executor = new ThreadPoolExecutor(numWorkerThreads, numWorkerThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(maxQueue));

Sau đó, executor.submit(callable)ném RejectedExecutionExceptionkhi hàng đợi đầy và tất cả các luồng đã bận.

Tôi có thể làm gì để tạo executor.submit(callable)khối khi hàng đợi đã đầy và tất cả các luồng đang bận?

CHỈNH SỬA : Tôi đã thử cái này :

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

Và nó phần nào đạt được hiệu quả mà tôi muốn đạt được nhưng theo một cách không phù hợp (về cơ bản các luồng bị từ chối được chạy trong luồng đang gọi, vì vậy điều này chặn luồng gọi gửi thêm).

CHỈNH SỬA: (5 năm sau khi đặt câu hỏi)

Đối với bất kỳ ai đang đọc câu hỏi này và câu trả lời của nó, vui lòng không coi câu trả lời được chấp nhận là một giải pháp đúng. Vui lòng đọc qua tất cả các câu trả lời và nhận xét.



1
Tôi đã sử dụng Semaphore trước đây để làm chính xác điều đó, giống như trong câu trả lời cho câu hỏi tương tự mà @axtavt được liên kết với.
Stephen Denne

2
@TomWolk Đối với một điều, bạn nhận được nhiều tác vụ thực thi song song hơn numWorkerThreadskhi luồng người gọi cũng đang thực thi một tác vụ. Tuy nhiên, vấn đề quan trọng hơn là nếu luồng người gọi nhận được một tác vụ đang chạy lâu, các luồng khác có thể chờ tác vụ tiếp theo.
Tahir Akhtar

2
@TahirAkhtar, đúng; Hàng đợi phải đủ dài để nó không bị khô khi người gọi phải thực thi tác vụ herselfe. Nhưng tôi nghĩ đó là một lợi thế nếu thêm một luồng nữa, luồng người gọi, có thể được sử dụng để thực thi các tác vụ. Nếu người gọi chỉ chặn, luồng của người gọi sẽ không hoạt động. Tôi sử dụng CallerRunsPolicy với một hàng đợi gấp ba lần mức bình thường của threadpool và nó hoạt động tốt và trơn tru. So với giải pháp này, tôi sẽ cân nhắc việc ủ với việc khai thác quá mức khuôn khổ.
TomWolk vào

1
@TomWalk +1 Điểm tốt. Có vẻ như một sự khác biệt khác là nếu tác vụ bị từ chối khỏi hàng đợi và được chạy bởi luồng người gọi, thì luồng người gọi sẽ bắt đầu xử lý một yêu cầu không theo thứ tự vì nó không đợi đến lượt trong hàng đợi. Chắc chắn, nếu bạn đã chọn sử dụng các luồng thì bạn phải xử lý mọi phụ thuộc đúng cách, nhưng chỉ cần lưu ý một số điều.
rimsky

Câu trả lời:


63

Tôi đã làm điều này tương tự. Bí quyết là tạo một BlockingQueue nơi phương thức offer () thực sự là một put (). (bạn có thể sử dụng bất kỳ cơ sở BlockingQueue nào bạn muốn).

public class LimitedQueue<E> extends LinkedBlockingQueue<E> 
{
    public LimitedQueue(int maxSize)
    {
        super(maxSize);
    }

    @Override
    public boolean offer(E e)
    {
        // turn offer() and add() into a blocking calls (unless interrupted)
        try {
            put(e);
            return true;
        } catch(InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
        return false;
    }

}

Lưu ý rằng điều này chỉ hoạt động cho nhóm chủ đề, corePoolSize==maxPoolSizevì vậy hãy cẩn thận ở đó (xem nhận xét).


2
hoặc bạn có thể mở rộng SynchronousQueue để ngăn chặn việc lưu vào bộ đệm, chỉ cho phép xử lý trực tiếp.
brendon

Thanh lịch và trực tiếp giải quyết vấn đề. offer () trở thành put (), và put () có nghĩa là "... chờ nếu cần thiết để có chỗ trống"
Trenton

5
Tôi không nghĩ đây là một ý tưởng hay vì nó thay đổi giao thức của phương thức chào hàng. Phương thức ưu đãi phải là một cuộc gọi không chặn.
Mingjiang Shi

6
Tôi không đồng ý - điều này thay đổi hành vi của ThreadPoolExecutor.execute để nếu bạn có corePoolSize <maxPoolSize, logic ThreadPoolExecutor sẽ không bao giờ thêm nhân viên bổ sung ngoài lõi.
Krease

5
Để làm rõ - giải pháp của bạn chỉ hoạt động miễn là bạn duy trì hạn chế ở đâu corePoolSize==maxPoolSize. Nếu không có điều đó, nó không còn cho phép ThreadPoolExecutor có hành vi được thiết kế. Tôi đang tìm kiếm một giải pháp cho vấn đề này đã không có hạn chế đó; xem câu trả lời thay thế của tôi bên dưới để biết cách tiếp cận mà chúng tôi đã thực hiện.
Krease

15

Đây là cách tôi giải quyết vấn đề này cuối cùng của tôi:

(lưu ý: giải pháp này chặn luồng gửi Callable, vì vậy nó ngăn RejectedExecutionException được ném)

public class BoundedExecutor extends ThreadPoolExecutor{

    private final Semaphore semaphore;

    public BoundedExecutor(int bound) {
        super(bound, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
        semaphore = new Semaphore(bound);
    }

    /**Submits task to execution pool, but blocks while number of running threads 
     * has reached the bound limit
     */
    public <T> Future<T> submitButBlockIfFull(final Callable<T> task) throws InterruptedException{

        semaphore.acquire();            
        return submit(task);                    
    }


    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);

        semaphore.release();
    }
}

1
Tôi cho rằng điều này không hoạt động tốt cho những trường hợp corePoolSize < maxPoolSize...: |
rogerdpack

1
Nó hoạt động cho trường hợp ở đâu corePoolSize < maxPoolSize. Trong những trường hợp đó, semaphore sẽ có sẵn, nhưng sẽ không có một luồng và SynchronousQueuesẽ trả về false. Sau ThreadPoolExecutorđó sẽ quay một chủ đề mới. Vấn đề của giải pháp này là nó có một điều kiện chủng tộc . Sau semaphore.release(), nhưng trước khi luồng kết thúc execute, submit () sẽ nhận được giấy phép semaphore. NẾU super.submit () được chạy trước khi execute()kết thúc, công việc sẽ bị từ chối.
Luís Guilherme

@ LuísGuilherme Nhưng semaphore.release () sẽ không bao giờ được gọi trước khi luồng kết thúc thực thi. Vì cuộc gọi này được thực hiện trong phương thức after Execute (...). Tôi có thiếu một cái gì đó trong kịch bản bạn đang mô tả?
cvacca

1
afterExecute được gọi bởi cùng một luồng chạy tác vụ, vì vậy nó vẫn chưa kết thúc. Tự làm bài kiểm tra. Thực hiện giải pháp đó và ném một lượng lớn công việc vào người thực thi, ném nếu công việc bị từ chối. Bạn sẽ nhận thấy rằng có, điều này có một điều kiện chủng tộc, và không khó để tái tạo nó.
Luís Guilherme

1
Đi tới ThreadPoolExecutor và kiểm tra phương thức runWorker (Worker w). Bạn sẽ thấy rằng mọi thứ sẽ xảy ra sau khi afterExecute kết thúc, bao gồm việc mở khóa worker và tăng số lượng nhiệm vụ đã hoàn thành. Vì vậy, bạn đã cho phép các tác vụ đi vào (bằng cách giải phóng semaphore) mà không cần băng thông để xử lý chúng (bằng cách gọi processWorkerExit).
Luís Guilherme

14

Câu trả lời hiện được chấp nhận có một vấn đề tiềm ẩn nghiêm trọng - nó thay đổi hành vi của ThreadPoolExecutor.execute sao cho nếu bạn có corePoolSize < maxPoolSize, logic ThreadPoolExecutor sẽ không bao giờ thêm nhân viên bổ sung ngoài lõi.

Từ ThreadPoolExecutor .execute (Runnable):

    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);

Cụ thể, khối 'else' cuối cùng đó sẽ không bao giờ bị đánh.

Một giải pháp thay thế tốt hơn là làm điều gì đó tương tự như những gì OP đang làm - sử dụng RejectedExecutionHandler để thực hiện cùng một putlogic:

public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    try {
        if (!executor.isShutdown()) {
            executor.getQueue().put(r);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RejectedExecutionException("Executor was interrupted while the task was waiting to put on work queue", e);
    }
}

Có một số điều cần chú ý với cách tiếp cận này, như được chỉ ra trong các nhận xét (đề cập đến câu trả lời này ):

  1. Nếu corePoolSize==0, sau đó có một điều kiện đua mà tất cả các luồng trong nhóm có thể chết trước khi nhiệm vụ được hiển thị
  2. Việc sử dụng một triển khai bao bọc các tác vụ hàng đợi (không áp dụng cho ThreadPoolExecutor) sẽ dẫn đến sự cố trừ khi trình xử lý cũng kết thúc nó theo cách tương tự.

Hãy ghi nhớ những vấn đề đó, giải pháp này sẽ hoạt động đối với hầu hết các ThreadPoolExecutor điển hình và sẽ xử lý đúng trường hợp corePoolSize < maxPoolSize.


Đối với bất kỳ ai đã phản đối - bạn có thể cung cấp một số thông tin chi tiết? Có điều gì đó không chính xác / gây hiểu lầm / nguy hiểm trong câu trả lời này? Tôi muốn có cơ hội để giải quyết mối quan tâm của bạn.
Krease

2
Tôi không downvote, nhưng nó dường như là một ý tưởng rất xấu
vanOekel

@vanOekel - cảm ơn vì liên kết - câu trả lời đó nêu ra một số trường hợp hợp lệ cần biết nếu sử dụng phương pháp này, nhưng IMO không biến nó thành "ý tưởng tồi" - nó vẫn giải quyết một vấn đề có trong câu trả lời hiện được chấp nhận. Tôi đã cập nhật câu trả lời của mình với những cảnh báo đó.
Krease

Nếu kích thước nhóm lõi là 0 và nếu nhiệm vụ được gửi cho trình thực thi, trình thực thi sẽ bắt đầu tạo / s luồng nếu hàng đợi đã đầy để xử lý tác vụ. Thế thì tại sao nó dễ bị bế tắc. Không hiểu ý bạn. Bạn có thể nói rõ hơn.?
Shirgill Farhan

@ShirgillFarhanAnsari - đó là trường hợp được nêu ra trong nhận xét trước. Điều này có thể xảy ra vì việc thêm trực tiếp vào hàng đợi không kích hoạt tạo luồng / công nhân bắt đầu. Đó là một trường hợp cạnh / điều kiện chủng tộc có thể được giảm nhẹ bằng việc có một khác không lõi kích thước hồ bơi
Krease

4

Tôi biết đây là một câu hỏi cũ nhưng có một vấn đề tương tự là tạo tác vụ mới rất nhanh và nếu có quá nhiều OutOfMemoryError xảy ra vì tác vụ hiện tại không được hoàn thành đủ nhanh.

Trong trường hợp của tôi đã Callablesđược gửi và tôi cần kết quả do đó tôi cần lưu trữ tất cả những gì được Futurestrả lại executor.submit(). Giải pháp của tôi là đặt Futuresvào một BlockingQueuevới kích thước tối đa. Khi hàng đợi đó đã đầy, không có tác vụ nào được tạo thêm cho đến khi một số tác vụ được hoàn thành (các phần tử bị xóa khỏi hàng đợi). Trong mã giả:

final ExecutorService executor = Executors.newFixedThreadPool(numWorkerThreads);
final LinkedBlockingQueue<Future> futures = new LinkedBlockingQueue<>(maxQueueSize);
try {   
    Thread taskGenerator = new Thread() {
        @Override
        public void run() {
            while (reader.hasNext) {
                Callable task = generateTask(reader.next());
                Future future = executor.submit(task);
                try {
                    // if queue is full blocks until a task
                    // is completed and hence no future tasks are submitted.
                    futures.put(compoundFuture);
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();         
                }
            }
        executor.shutdown();
        }
    }
    taskGenerator.start();

    // read from queue as long as task are being generated
    // or while Queue has elements in it
    while (taskGenerator.isAlive()
                    || !futures.isEmpty()) {
        Future compoundFuture = futures.take();
        // do something
    }
} catch (InterruptedException ex) {
    Thread.currentThread().interrupt();     
} catch (ExecutionException ex) {
    throw new MyException(ex);
} finally {
    executor.shutdownNow();
}

2

Tôi đã gặp vấn đề tương tự và tôi đã thực hiện điều đó bằng cách sử dụng các beforeExecute/afterExecutehook từ ThreadPoolExecutor:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Blocks current task execution if there is not enough resources for it.
 * Maximum task count usage controlled by maxTaskCount property.
 */
public class BlockingThreadPoolExecutor extends ThreadPoolExecutor {

    private final ReentrantLock taskLock = new ReentrantLock();
    private final Condition unpaused = taskLock.newCondition();
    private final int maxTaskCount;

    private volatile int currentTaskCount;

    public BlockingThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
            long keepAliveTime, TimeUnit unit,
            BlockingQueue<Runnable> workQueue, int maxTaskCount) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        this.maxTaskCount = maxTaskCount;
    }

    /**
     * Executes task if there is enough system resources for it. Otherwise
     * waits.
     */
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        taskLock.lock();
        try {
            // Spin while we will not have enough capacity for this job
            while (maxTaskCount < currentTaskCount) {
                try {
                    unpaused.await();
                } catch (InterruptedException e) {
                    t.interrupt();
                }
            }
            currentTaskCount++;
        } finally {
            taskLock.unlock();
        }
    }

    /**
     * Signalling that one more task is welcome
     */
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        taskLock.lock();
        try {
            currentTaskCount--;
            unpaused.signalAll();
        } finally {
            taskLock.unlock();
        }
    }
}

Điều này sẽ đủ tốt cho bạn. Btw, việc triển khai ban đầu dựa trên kích thước nhiệm vụ vì một nhiệm vụ có thể lớn hơn 100 lần so với nhiệm vụ khác và việc gửi hai nhiệm vụ lớn sẽ giết chết hộp, nhưng chạy một nhiệm vụ lớn và nhiều nhiệm vụ nhỏ thì được. Nếu các tác vụ chuyên sâu vào I / O của bạn có cùng kích thước, bạn có thể sử dụng lớp này, nếu không, chỉ cần cho tôi biết và tôi sẽ đăng cách triển khai dựa trên kích thước.

PS Bạn muốn kiểm tra ThreadPoolExecutorjavadoc. Đó là hướng dẫn sử dụng thực sự tuyệt vời của Doug Lea về cách nó có thể dễ dàng tùy chỉnh.


1
Tôi đang tự hỏi điều gì sẽ xảy ra khi một Thread đang giữ khóa beforeExecute () và thấy điều đó maxTaskCount < currentTaskCountvà bắt đầu chờ đợi với unpausedđiều kiện. Đồng thời, một luồng khác cố gắng có được khóa trong afterExecute () để báo hiệu hoàn thành một tác vụ. Nó sẽ không phải là một bế tắc?
Tahir Akhtar

1
Tôi cũng nhận thấy rằng giải pháp này sẽ không chặn luồng gửi nhiệm vụ khi hàng đợi đầy. Vì vậy, RejectedExecutionExceptionvẫn có thể.
Tahir Akhtar

1
Ngữ nghĩa của các lớp ReentrantLock / Condition tương tự như những gì được đồng bộ hóa & chờ / thông báo cung cấp. Khi các phương thức chờ điều kiện được gọi là khóa được giải phóng, do đó sẽ không có bế tắc.
Petro Semeniuk

Đúng, ExecutorService này chặn các tác vụ khi gửi mà không chặn luồng người gọi. Công việc chỉ được nộp và sẽ được xử lý không đồng bộ khi có đủ tài nguyên hệ thống cho nó.
Petro Semeniuk

2

Tôi đã triển khai một giải pháp theo mẫu trang trí và sử dụng semaphore để kiểm soát số lượng tác vụ được thực thi. Bạn có thể sử dụng nó với bất kỳ Executorvà:

  • Chỉ định tối đa các nhiệm vụ đang diễn ra
  • Chỉ định thời gian chờ tối đa để đợi giấy phép thực thi tác vụ (nếu thời gian chờ trôi qua và không có giấy phép nào được cấp, a RejectedExecutionExceptionsẽ được ném)
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;

import javax.annotation.Nonnull;

public class BlockingOnFullQueueExecutorDecorator implements Executor {

    private static final class PermitReleasingDecorator implements Runnable {

        @Nonnull
        private final Runnable delegate;

        @Nonnull
        private final Semaphore semaphore;

        private PermitReleasingDecorator(@Nonnull final Runnable task, @Nonnull final Semaphore semaphoreToRelease) {
            this.delegate = task;
            this.semaphore = semaphoreToRelease;
        }

        @Override
        public void run() {
            try {
                this.delegate.run();
            }
            finally {
                // however execution goes, release permit for next task
                this.semaphore.release();
            }
        }

        @Override
        public final String toString() {
            return String.format("%s[delegate='%s']", getClass().getSimpleName(), this.delegate);
        }
    }

    @Nonnull
    private final Semaphore taskLimit;

    @Nonnull
    private final Duration timeout;

    @Nonnull
    private final Executor delegate;

    public BlockingOnFullQueueExecutorDecorator(@Nonnull final Executor executor, final int maximumTaskNumber, @Nonnull final Duration maximumTimeout) {
        this.delegate = Objects.requireNonNull(executor, "'executor' must not be null");
        if (maximumTaskNumber < 1) {
            throw new IllegalArgumentException(String.format("At least one task must be permitted, not '%d'", maximumTaskNumber));
        }
        this.timeout = Objects.requireNonNull(maximumTimeout, "'maximumTimeout' must not be null");
        if (this.timeout.isNegative()) {
            throw new IllegalArgumentException("'maximumTimeout' must not be negative");
        }
        this.taskLimit = new Semaphore(maximumTaskNumber);
    }

    @Override
    public final void execute(final Runnable command) {
        Objects.requireNonNull(command, "'command' must not be null");
        try {
            // attempt to acquire permit for task execution
            if (!this.taskLimit.tryAcquire(this.timeout.toMillis(), MILLISECONDS)) {
                throw new RejectedExecutionException(String.format("Executor '%s' busy", this.delegate));
            }
        }
        catch (final InterruptedException e) {
            // restore interrupt status
            Thread.currentThread().interrupt();
            throw new IllegalStateException(e);
        }

        this.delegate.execute(new PermitReleasingDecorator(command, this.taskLimit));
    }

    @Override
    public final String toString() {
        return String.format("%s[availablePermits='%s',timeout='%s',delegate='%s']", getClass().getSimpleName(), this.taskLimit.availablePermits(),
                this.timeout, this.delegate);
    }
}

1

Tôi nghĩ rằng nó đơn giản như sử dụng một ArrayBlockingQueuethay vì aa LinkedBlockingQueue.

Bỏ qua cho tôi ... điều đó hoàn toàn sai. ThreadPoolExecutorcuộc gọi Queue#offerkhông putcó hiệu lực mà bạn yêu cầu.

Bạn có thể mở rộng ThreadPoolExecutorvà cung cấp triển khai các execute(Runnable)cuộc gọi đó putthay cho offer.

Tôi e rằng đó không phải là một câu trả lời hoàn toàn thỏa đáng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.