Làm cách nào để ThreadPoolExecutor tăng chủ đề lên tối đa trước khi xếp hàng?


99

Tôi đã thất vọng một thời gian với hành vi mặc định ThreadPoolExecutorhỗ trợ các nhóm ExecutorServiceluồng mà rất nhiều người trong chúng ta sử dụng. Để trích dẫn từ Javadocs:

Nếu có nhiều luồng corePoolSize nhưng ít hơn luồng MaximumPoolSize đang chạy, một luồng mới sẽ chỉ được tạo khi hàng đợi đầy .

Điều này có nghĩa là nếu bạn xác định một nhóm luồng bằng mã sau, nó sẽ không bao giờ bắt đầu luồng thứ 2 vì LinkedBlockingQueuekhông bị ràng buộc.

ExecutorService threadPool =
   new ThreadPoolExecutor(1 /*core*/, 50 /*max*/, 60 /*timeout*/,
      TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(/* unlimited queue */));

Chỉ khi bạn có một hàng đợi bị giới hạn và hàng đợi đã đầy thì bất kỳ luồng nào trên số lõi mới được bắt đầu. Tôi nghi ngờ rằng một số lượng lớn các lập trình viên đa luồng Java cơ sở không biết về hành vi này của ThreadPoolExecutor.

Bây giờ tôi có trường hợp sử dụng cụ thể, nơi điều này không phải là tối ưu. Tôi đang tìm cách, mà không cần viết lớp TPE của riêng mình, để giải quyết vấn đề đó.

Yêu cầu của tôi là đối với một dịch vụ web đang thực hiện cuộc gọi lại cho một bên thứ ba có thể không đáng tin cậy.

  • Tôi không muốn gọi lại đồng bộ với yêu cầu web, vì vậy tôi muốn sử dụng nhóm luồng.
  • Tôi thường nhận được một vài trong số này trong một phút vì vậy tôi không muốn có một newFixedThreadPool(...)số lượng lớn các chủ đề hầu như không hoạt động.
  • Tôi thường xuyên nhận được một loạt lưu lượng truy cập này và tôi muốn tăng số lượng luồng lên một giá trị tối đa nào đó (giả sử là 50).
  • Tôi cần cố gắng tốt nhất để thực hiện tất cả các lệnh gọi lại, vì vậy tôi muốn xếp hàng thêm bất kỳ lệnh gọi lại nào trên 50. Tôi không muốn lấn át phần còn lại của máy chủ web của mình bằng cách sử dụng a newCachedThreadPool().

Làm cách nào tôi có thể khắc phục hạn chế này trong ThreadPoolExecutortrường hợp hàng đợi cần được giới hạn và đầy trước khi nhiều luồng hơn sẽ được bắt đầu? Làm cách nào tôi có thể làm cho nó bắt đầu nhiều chuỗi hơn trước khi thực hiện các tác vụ xếp hàng?

Biên tập:

@Flavio nêu một điểm tốt về việc sử dụng ThreadPoolExecutor.allowCoreThreadTimeOut(true)để có thời gian chờ và thoát các luồng lõi. Tôi đã cân nhắc điều đó nhưng tôi vẫn muốn có tính năng luồng lõi. Tôi không muốn số luồng trong nhóm giảm xuống dưới kích thước lõi nếu có thể.


1
Giả sử ví dụ của bạn tạo ra tối đa 10 luồng, liệu có tiết kiệm thực sự nào khi sử dụng thứ gì đó phát triển / thu nhỏ trên một nhóm luồng có kích thước cố định không?
bstempi

Điểm tốt @bstempi. Con số hơi tùy ý. Tôi đã tăng nó trong câu hỏi lên 50. Không chính xác có bao nhiêu chủ đề đồng thời tôi thực sự muốn hoạt động nhưng bây giờ tôi có giải pháp này.
Màu xám,

1
Ôi chết thật! 10 phiếu ủng hộ nếu tôi có thể ở đây, chính xác vị trí mà tôi đang làm.
Eugene

Câu trả lời:


50

Làm cách nào tôi có thể khắc phục hạn chế này ở ThreadPoolExecutornơi hàng đợi cần được giới hạn và đầy trước khi bắt đầu nhiều luồng hơn.

Tôi tin rằng cuối cùng tôi đã tìm thấy một giải pháp hơi thanh lịch (có thể là một chút hacky) cho hạn chế này với ThreadPoolExecutor. Nó liên quan đến việc mở rộng LinkedBlockingQueueđể có nó trở lại falsecho queue.offer(...)khi đã có một số nhiệm vụ xếp hàng đợi. Nếu các luồng hiện tại không theo kịp các tác vụ được xếp hàng đợi, TPE sẽ thêm các luồng bổ sung. Nếu nhóm đã ở mức tối đa luồng, thì nhóm RejectedExecutionHandlersẽ được gọi. Nó là trình xử lý sau đó thực hiện put(...)vào hàng đợi.

Chắc chắn là kỳ lạ khi viết một hàng đợi offer(...)có thể trả về falseput()không bao giờ chặn, vì vậy đó là phần hack. Nhưng điều này hoạt động tốt với việc sử dụng hàng đợi của TPE nên tôi không thấy có vấn đề gì khi thực hiện việc này.

Đây là mã:

// extend LinkedBlockingQueue to force offer() to return false conditionally
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    private static final long serialVersionUID = -6903933921423432194L;
    @Override
    public boolean offer(Runnable e) {
        // Offer it to the queue if there is 0 items already queued, else
        // return false so the TPE will add another thread. If we return false
        // and max threads have been reached then the RejectedExecutionHandler
        // will be called which will do the put into the queue.
        if (size() == 0) {
            return super.offer(e);
        } else {
            return false;
        }
    }
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1 /*core*/, 50 /*max*/,
        60 /*secs*/, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            // This does the actual put into the queue. Once the max threads
            //  have been reached, the tasks will then queue up.
            executor.getQueue().put(r);
            // we do this after the put() to stop race conditions
            if (executor.isShutdown()) {
                throw new RejectedExecutionException(
                    "Task " + r + " rejected from " + e);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return;
        }
    }
});

Với cơ chế này, khi tôi gửi nhiệm vụ vào hàng đợi, ý ThreadPoolExecutormuốn:

  1. Chia tỷ lệ số luồng lên đến kích thước lõi ban đầu (ở đây 1).
  2. Cung cấp nó cho hàng đợi. Nếu hàng đợi trống, nó sẽ được xếp hàng để được xử lý bởi các luồng hiện có.
  3. Nếu hàng đợi đã có 1 hoặc nhiều phần tử, giá trị offer(...)sẽ trả về false.
  4. Nếu trả về false, hãy mở rộng số luồng trong nhóm cho đến khi chúng đạt đến số lượng tối đa (ở đây là 50).
  5. Nếu ở mức tối đa thì nó gọi RejectedExecutionHandler
  6. Sau RejectedExecutionHandlerđó, đặt nhiệm vụ vào hàng đợi để xử lý bởi luồng có sẵn đầu tiên theo thứ tự FIFO.

Mặc dù trong đoạn mã ví dụ của tôi ở trên, hàng đợi không bị ràng buộc, bạn cũng có thể định nghĩa nó là hàng đợi có giới hạn. Ví dụ: nếu bạn thêm dung lượng 1000 vào LinkedBlockingQueuethì nó sẽ:

  1. mở rộng các chủ đề lên đến tối đa
  2. sau đó xếp hàng cho đến khi đầy với 1000 nhiệm vụ
  3. sau đó chặn người gọi cho đến khi có dung lượng cho hàng đợi.

Ngoài ra, nếu bạn cần sử dụng offer(...)trong thời gian RejectedExecutionHandlerđó, bạn có thể sử dụng offer(E, long, TimeUnit)phương pháp này thay vì Long.MAX_VALUEthời gian chờ.

Cảnh báo:

Nếu bạn mong đợi các tác vụ được thêm vào trình thực thi sau khi nó đã tắt, thì bạn có thể muốn thông minh hơn về việc RejectedExecutionExceptionloại bỏ tùy chỉnh của chúng tôi RejectedExecutionHandlerkhi dịch vụ thực thi đã bị tắt. Cảm ơn @RaduToader đã chỉ ra điều này.

Biên tập:

Một tinh chỉnh khác cho câu trả lời này có thể là hỏi TPE nếu có các chuỗi không hoạt động và chỉ xếp hạng mục nếu có. Bạn sẽ phải tạo một lớp thực sự cho điều này và thêmourQueue.setThreadPoolExecutor(tpe); phương thức vào nó.

Sau đó, offer(...)phương pháp của bạn có thể trông giống như sau:

  1. Kiểm tra xem nếu tpe.getPoolSize() == tpe.getMaximumPoolSize()trường hợp đó chỉ cần gọisuper.offer(...) .
  2. Khác nếu tpe.getPoolSize() > tpe.getActiveCount()sau đó gọisuper.offer(...) vì dường như có chủ đề nhàn rỗi.
  3. Nếu không, hãy quay lại falsefork một chủ đề khác.

Có lẽ điều này:

int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
    return super.offer(e);
} else {
    return false;
}

Lưu ý rằng các phương thức get trên TPE rất tốn kém vì chúng truy cập volatilecác trường hoặc (trong trường hợp getActiveCount()) khóa TPE và xem danh sách luồng. Ngoài ra, có các điều kiện chạy đua ở đây có thể khiến một tác vụ được xếp hàng không đúng cách hoặc một luồng khác được phân nhánh khi có một luồng không hoạt động.


Tôi cũng phải vật lộn với vấn đề tương tự, cuối cùng là ghi đè phương thức thực thi. Nhưng đây thực sự là một giải pháp tốt đẹp. :)
Batty

Càng nhiều càng tốt Tôi không thích ý tưởng phá vỡ hợp đồng Queueđể thực hiện điều này, bạn chắc chắn không đơn độc trong ý tưởng của bạn: groovy-programming.com/post/26923146865
bstempi

3
Bạn không nhận thấy một điều kỳ lạ ở đây là một vài nhiệm vụ đầu tiên sẽ được xếp hàng đợi, và chỉ sau đó các chuỗi mới xuất hiện? Ví dụ: nếu một luồng lõi của bạn đang bận với một tác vụ chạy dài và bạn gọi execute(runnable), thì luồng đó runnablechỉ được thêm vào hàng đợi. Nếu bạn gọi execute(secondRunnable), sau đó secondRunnableđược thêm vào hàng đợi. Nhưng bây giờ nếu bạn gọi execute(thirdRunnable), sau đó thirdRunnablesẽ được chạy trong một luồng mới. Các runnablesecondRunnablechỉ chạy một lần thirdRunnable(hoặc bản gốc dài chạy nhiệm vụ) đã kết thúc.
Robert Tupelo-Schneck

1
Đúng vậy, Robert nói đúng, trong môi trường đa luồng cao, hàng đợi đôi khi phát triển trong khi vẫn có các luồng miễn phí để sử dụng. Giải pháp dưới đây mở rộng TPE - hoạt động tốt hơn nhiều. Tôi nghĩ gợi ý của Robert nên được đánh dấu là câu trả lời, mặc dù vụ hack ở trên rất thú vị
Wanna Know All

1
"RejectedExecutionHandler" đã giúp người thực thi khi tắt máy. Bây giờ bạn đang bị buộc phải sử dụng shutdownNow () vì shutdown () không ngăn các tác vụ mới được thêm vào (vì phải làm lại)
Radu Toader

28

Đặt kích thước lõi và kích thước tối đa thành cùng một giá trị và cho phép xóa các luồng lõi khỏi nhóm với allowCoreThreadTimeOut(true).


+1 Vâng, tôi đã nghĩ đến điều đó nhưng tôi vẫn muốn có tính năng luồng lõi. Tôi không muốn nhóm chủ đề chuyển đến 0 chủ đề trong thời gian không hoạt động. Tôi sẽ chỉnh sửa câu hỏi của mình để chỉ ra điều đó. Nhưng điểm xuất sắc.
Grey

Cảm ơn bạn! Đó chỉ là cách đơn giản nhất để làm điều đó.
Dmitry Ovchinnikov

28

Tôi đã có hai câu trả lời khác cho câu hỏi này, nhưng tôi nghi ngờ câu trả lời này là tốt nhất.

Nó dựa trên kỹ thuật của câu trả lời được chấp nhận hiện nay , cụ thể là:

  1. Ghi đè offer()phương thức của hàng đợi để (đôi khi) trả về false,
  2. nguyên nhân ThreadPoolExecutortạo ra một chuỗi mới hoặc từ chối nhiệm vụ và
  3. đặt RejectedExecutionHandlerđể thực sự xếp hàng nhiệm vụ khi bị từ chối.

Vấn đề là khi nào offer()nên trả về false. Câu trả lời hiện được chấp nhận trả về false khi hàng đợi có một vài nhiệm vụ trên đó, nhưng như tôi đã chỉ ra trong nhận xét của mình ở đó, điều này gây ra các tác dụng không mong muốn. Ngoài ra, nếu bạn luôn trả về false, bạn sẽ tiếp tục tạo ra các chuỗi mới ngay cả khi bạn có các chuỗi đang chờ trên hàng đợi.

Giải pháp là sử dụng Java 7 LinkedTransferQueuevà có offer()cuộc gọi tryTransfer(). Khi có một luồng người tiêu dùng đang đợi, nhiệm vụ sẽ chỉ được chuyển đến luồng đó. Nếu không, offer()sẽ trả về false và ThreadPoolExecutorsẽ sinh ra một chuỗi mới.

    BlockingQueue<Runnable> queue = new LinkedTransferQueue<Runnable>() {
        @Override
        public boolean offer(Runnable e) {
            return tryTransfer(e);
        }
    };
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue);
    threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    });

Tôi phải đồng ý, điều này trông sạch sẽ nhất đối với tôi. Nhược điểm duy nhất của giải pháp là LinkedTransferQueue không bị ràng buộc, vì vậy bạn không nhận được hàng đợi tác vụ có giới hạn dung lượng mà không cần làm thêm.
Yeroc

Đã xảy ra sự cố khi hồ bơi phát triển đến kích thước tối đa. Giả sử pool đã mở rộng đến kích thước tối đa và mọi luồng hiện đang thực thi một tác vụ, khi runnable được gửi, phiếu mua hàng này sẽ trả về false và ThreadPoolExecutor cố gắng thêm luồng addWorker, nhưng pool đã đạt đến mức tối đa nên runnable sẽ đơn giản bị từ chối. Theo như ExceHandler bị từ chối mà bạn đã viết, nó sẽ được đưa vào hàng đợi một lần nữa, dẫn đến điệu nhảy khỉ này lại diễn ra từ đầu.
Sudheera

1
@Sudheera Tôi tin rằng bạn đã nhầm. queue.offer(), bởi vì nó thực sự đang gọi LinkedTransferQueue.tryTransfer(), sẽ trả về false và không xếp hàng đợi nhiệm vụ. Tuy nhiên, các RejectedExecutionHandlercuộc gọi queue.put(), không thất bại và không xếp hàng đợi nhiệm vụ.
Robert Tupelo-Schneck

1
@ RobertTupelo-Schneck cực kỳ hữu ích và tốt đẹp!
Eugene

1
@ RobertTupelo-Schneck Hoạt động như một sự quyến rũ! Tôi không biết lý do tại sao không có một cái gì đó như thế ra khỏi hộp trong java
Georgi Peev

7

Lưu ý: Bây giờ tôi thích và đề xuất câu trả lời khác của tôi .

Đây là một phiên bản mà tôi cảm thấy đơn giản hơn nhiều: Tăng corePoolSize (lên đến giới hạn của MaximumPoolSize) bất cứ khi nào một tác vụ mới được thực thi, sau đó giảm corePoolSize (xuống đến giới hạn của người dùng đã chỉ định "kích thước nhóm lõi") bất cứ khi nào nhiệm vụ hoàn thành.

Nói một cách khác, hãy theo dõi số lượng tác vụ đang chạy hoặc được xếp hàng và đảm bảo rằng corePoolSize bằng với số tác vụ miễn là nó nằm giữa "kích thước nhóm lõi" và tối đaPoolSize do người dùng chỉ định.

public class GrowBeforeQueueThreadPoolExecutor extends ThreadPoolExecutor {
    private int userSpecifiedCorePoolSize;
    private int taskCount;

    public GrowBeforeQueueThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        userSpecifiedCorePoolSize = corePoolSize;
    }

    @Override
    public void execute(Runnable runnable) {
        synchronized (this) {
            taskCount++;
            setCorePoolSizeToTaskCountWithinBounds();
        }
        super.execute(runnable);
    }

    @Override
    protected void afterExecute(Runnable runnable, Throwable throwable) {
        super.afterExecute(runnable, throwable);
        synchronized (this) {
            taskCount--;
            setCorePoolSizeToTaskCountWithinBounds();
        }
    }

    private void setCorePoolSizeToTaskCountWithinBounds() {
        int threads = taskCount;
        if (threads < userSpecifiedCorePoolSize) threads = userSpecifiedCorePoolSize;
        if (threads > getMaximumPoolSize()) threads = getMaximumPoolSize();
        setCorePoolSize(threads);
    }
}

Như đã viết, lớp không hỗ trợ thay đổi corePoolSize hoặc MaximumPoolSize do người dùng chỉ định sau khi xây dựng và không hỗ trợ thao tác hàng đợi công việc trực tiếp hoặc thông qua remove()hoặc purge().


Tôi thích nó ngoại trừ các synchronizedkhối. Bạn có thể gọi thông qua hàng đợi để nhận số lượng nhiệm vụ. Hoặc có thể sử dụng một AtomicInteger?
Xám

Tôi muốn tránh chúng, nhưng vấn đề là đây. Nếu có một số execute()cuộc gọi trong các chuỗi riêng biệt, mỗi cuộc sẽ (1) tìm ra bao nhiêu chuỗi là cần thiết, (2) setCorePoolSizeđến số đó, và (3) cuộc gọi super.execute(). Nếu các bước (1) và (2) không được đồng bộ hóa, tôi không chắc làm thế nào để ngăn chặn một thứ tự đáng tiếc trong đó bạn đặt kích thước nhóm lõi thành một số thấp hơn sau một số cao hơn. Với quyền truy cập trực tiếp vào trường lớp cha, điều này có thể được thực hiện bằng cách sử dụng so sánh và đặt thay thế, nhưng tôi không thấy một cách rõ ràng để thực hiện điều đó trong một lớp con mà không đồng bộ hóa.
Robert Tupelo-Schneck

Tôi nghĩ rằng các hình phạt cho điều kiện cuộc đua đó là tương đối thấp miễn là taskCountsân đó hợp lệ (tức là a AtomicInteger). Nếu hai luồng điều chỉnh lại kích thước nhóm ngay sau nhau, nó sẽ nhận được các giá trị thích hợp. Nếu cái thứ 2 thu hẹp các luồng cốt lõi thì chắc chắn nó đã thấy sự sụt giảm trong hàng đợi hay gì đó.
Màu xám

1
Đáng buồn là tôi nghĩ nó còn tồi tệ hơn thế. Giả sử nhiệm vụ 10 và 11 gọi execute(). Mỗi người sẽ gọi atomicTaskCount.incrementAndGet()và họ sẽ nhận được lần lượt là 10 và 11. Nhưng nếu không đồng bộ hóa (vượt quá số lượng nhiệm vụ và đặt kích thước nhóm lõi), sau đó bạn có thể nhận được (1) nhiệm vụ 11 đặt kích thước nhóm lõi thành 11, (2) nhiệm vụ 10 đặt kích thước nhóm lõi thành 10, (3) nhiệm vụ 10 gọi super.execute(), (4) nhiệm vụ 11 gọi super.execute()và được xếp vào hàng đợi.
Robert Tupelo-Schneck

2
Tôi đã đưa ra giải pháp này một số thử nghiệm nghiêm túc và nó rõ ràng là tốt nhất. Trong môi trường đa luồng cao, đôi khi nó vẫn sẽ xếp hàng khi có các luồng miễn phí (do tính chất TPE.execute của luồng tự do), nhưng nó hiếm khi xảy ra, trái ngược với giải pháp được đánh dấu là câu trả lời, trong đó điều kiện chủng tộc có nhiều cơ hội hơn xảy ra, vì vậy điều này xảy ra khá nhiều trên mỗi lần chạy đa luồng.
Muốn biết tất cả

6

Chúng tôi có một lớp con của ThreadPoolExecutornó cần bổ sung creationThresholdvà ghi đè execute.

public void execute(Runnable command) {
    super.execute(command);
    final int poolSize = getPoolSize();
    if (poolSize < getMaximumPoolSize()) {
        if (getQueue().size() > creationThreshold) {
            synchronized (this) {
                setCorePoolSize(poolSize + 1);
                setCorePoolSize(poolSize);
            }
        }
    }
}

có thể điều đó cũng hữu ích, nhưng tất nhiên trông nghệ thuật của bạn sẽ nghệ thuật hơn…


Hấp dẫn. Cảm ơn vì điều này. Tôi thực sự không biết rằng kích thước lõi có thể thay đổi.
Màu xám,

Bây giờ tôi nghĩ về nó một chút nữa, giải pháp này tốt hơn của tôi về mặt kiểm tra kích thước của hàng đợi. Tôi đã chỉnh sửa câu trả lời của mình để có offer(...)phương thức chỉ trả về có falseđiều kiện. Cảm ơn!
Màu xám,

4

Câu trả lời được đề xuất chỉ giải quyết một (1) vấn đề với nhóm luồng JDK:

  1. Nhóm luồng JDK thiên về xếp hàng. Vì vậy, thay vì tạo ra một luồng mới, chúng sẽ xếp hàng nhiệm vụ. Chỉ khi hàng đợi đạt đến giới hạn của nó thì nhóm luồng mới sinh ra một luồng mới.

  2. Việc nghỉ hưu chỉ không xảy ra khi tải giảm nhẹ. Ví dụ: nếu chúng ta có một loạt các công việc chạm vào nhóm làm cho nhóm đạt mức tối đa, tiếp theo là tải nhẹ của tối đa 2 tác vụ cùng một lúc, nhóm sẽ sử dụng tất cả các luồng để phục vụ tải nhẹ ngăn chặn việc nghỉ hưu của luồng. (chỉ cần 2 chủ đề…)

Không hài lòng với hành vi trên, tôi đã tiếp tục và triển khai một nhóm để khắc phục những thiếu sót ở trên.

Để giải quyết 2) Sử dụng lập lịch Lifo sẽ giải quyết được sự cố. Ý tưởng này đã được Ben Maurer trình bày tại hội nghị ứng dụng ACM 2015: Quy mô hệ thống @ Facebook

Vì vậy, một triển khai mới đã được sinh ra:

LifoThreadPoolExecutorSQP

Cho đến nay, triển khai này cải thiện hiệu suất thực thi không đồng bộ cho ZEL .

Việc triển khai có khả năng xoay để giảm chi phí chuyển đổi ngữ cảnh, mang lại hiệu suất vượt trội cho các trường hợp sử dụng nhất định.

Hy vọng nó giúp...

PS: JDK Fork Join Pool triển khai ExecutorService và hoạt động như một nhóm luồng "bình thường", Triển khai là hiệu suất, Nó sử dụng lập lịch luồng LIFO, tuy nhiên không có quyền kiểm soát kích thước hàng đợi nội bộ, thời gian chờ nghỉ hưu ... và quan trọng nhất là các tác vụ không thể bị gián đoạn khi hủy chúng


1
Quá tệ khi triển khai này có quá nhiều phụ thuộc bên ngoài. Làm cho nó vô dụng đối với tôi: - /
Martin L.

1
Đó là một điểm thực sự tốt (thứ 2). Thật không may, việc triển khai không rõ ràng khỏi các phụ thuộc bên ngoài, nhưng vẫn có thể được áp dụng nếu bạn muốn.
Alexey Vlasov

1

Lưu ý: Bây giờ tôi thích và đề xuất câu trả lời khác của tôi .

Tôi có một đề xuất khác, theo ý tưởng ban đầu là thay đổi hàng đợi để trả về sai. Trong phần này, tất cả các tác vụ đều có thể vào hàng đợi, nhưng bất cứ khi nào một tác vụ được xếp sau execute(), chúng tôi sẽ theo sau nó với một tác vụ không tham gia giám sát mà hàng đợi từ chối, khiến một chuỗi mới xuất hiện, chuỗi này sẽ thực hiện lệnh cấm ngay sau đó một cái gì đó từ hàng đợi.

Bởi vì các chủ đề công nhân có thể đang thăm dò ý kiến LinkedBlockingQueue cho một nhiệm vụ mới, một nhiệm vụ có thể được xếp vào hàng ngay cả khi có một chuỗi khả dụng. Để tránh tạo ra các luồng mới ngay cả khi có sẵn các luồng, chúng ta cần theo dõi có bao nhiêu luồng đang chờ các tác vụ mới trên hàng đợi và chỉ sinh ra một luồng mới khi có nhiều tác vụ trên hàng đợi hơn các luồng đang chờ.

final Runnable SENTINEL_NO_OP = new Runnable() { public void run() { } };

final AtomicInteger waitingThreads = new AtomicInteger(0);

BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    @Override
    public boolean offer(Runnable e) {
        // offer returning false will cause the executor to spawn a new thread
        if (e == SENTINEL_NO_OP) return size() <= waitingThreads.get();
        else return super.offer(e);
    }

    @Override
    public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.poll(timeout, unit);
        } finally {
            waitingThreads.decrementAndGet();
        }
    }

    @Override
    public Runnable take() throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.take();
        } finally {
            waitingThreads.decrementAndGet();
        }
    }
};

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue) {
    @Override
    public void execute(Runnable command) {
        super.execute(command);
        if (getQueue().size() > waitingThreads.get()) super.execute(SENTINEL_NO_OP);
    }
};
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        if (r == SENTINEL_NO_OP) return;
        else throw new RejectedExecutionException();            
    }
});

0

Giải pháp tốt nhất mà tôi có thể nghĩ đến là gia hạn.

ThreadPoolExecutorcung cấp một số phương pháp hook: beforeExecuteafterExecute. Trong tiện ích mở rộng của mình, bạn có thể duy trì sử dụng hàng đợi có giới hạn để cung cấp các tác vụ và hàng đợi không bị giới hạn thứ hai để xử lý tràn. Khi ai đó gọi submit, bạn có thể cố gắng đặt yêu cầu vào hàng đợi có giới hạn. Nếu gặp trường hợp ngoại lệ, bạn chỉ cần dán nhiệm vụ vào hàng đợi bổ sung của mình. Sau đó, bạn có thể sử dụngafterExecute hook để xem liệu có thứ gì trong hàng đợi tràn sau khi hoàn thành một tác vụ hay không. Bằng cách này, trình thực thi sẽ xử lý nội dung trong hàng đợi có giới hạn trước và tự động kéo từ hàng đợi không bị giới hạn này khi thời gian cho phép.

Nó có vẻ như nhiều công việc hơn giải pháp của bạn, nhưng ít nhất nó không liên quan đến việc đưa ra các hành vi không mong muốn của hàng đợi. Tôi cũng tưởng tượng rằng có một cách tốt hơn để kiểm tra trạng thái của hàng đợi và luồng thay vì dựa vào các ngoại lệ, vốn khá chậm.


Tôi không thích giải pháp này. Tôi khá chắc chắn rằng ThreadPoolExecutor không được thiết kế để kế thừa.
scottb

Thực sự có một ví dụ về tiện ích mở rộng ngay trong JavaDoc. Họ nói rằng hầu hết có thể sẽ chỉ triển khai các phương thức hook, nhưng họ cho bạn biết những gì khác bạn cần chú ý khi mở rộng.
bstempi

0

Lưu ý: Đối với JDK ThreadPoolExecutor khi bạn có hàng đợi bị giới hạn, bạn chỉ tạo luồng mới khi phiếu mua hàng trả về false. Bạn có thể có được thứ gì đó hữu ích với CallerRunsPolicy tạo ra một chút BackPressure và trực tiếp các cuộc gọi chạy trong chuỗi người gọi.

Tôi cần công việc được thực hiện từ các chủ đề được tạo ra bởi các hồ bơi và có một hàng đợi ubounded cho lịch trình, trong khi số lượng các chủ đề trong hồ bơi có thể phát triển hoặc co lại giữa corePoolSizemaximumPoolSize như vậy ...

Tôi đã kết thúc việc dán bản sao đầy đủ từ ThreadPoolExecutorthay đổi một chút phương thức thực thi vì rất tiếc điều này không thể được thực hiện bởi tiện ích mở rộng (nó gọi các phương thức riêng tư).

Tôi không muốn tạo ra các chủ đề mới ngay lập tức khi yêu cầu mới đến và tất cả các chủ đề đang bận (vì nói chung tôi có các nhiệm vụ ngắn hạn). Tôi đã thêm một ngưỡng nhưng vui lòng thay đổi nó theo nhu cầu của bạn (có thể đối với chủ yếu là IO thì tốt hơn nên xóa ngưỡng này)

private final AtomicInteger activeWorkers = new AtomicInteger(0);
private volatile double threshold = 0.7d;

protected void beforeExecute(Thread t, Runnable r) {
    activeWorkers.incrementAndGet();
}
protected void afterExecute(Runnable r, Throwable t) {
    activeWorkers.decrementAndGet();
}
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

        if (isRunning(c) && this.workQueue.offer(command)) {
            int recheck = this.ctl.get();
            if (!isRunning(recheck) && this.remove(command)) {
                this.reject(command);
            } else if (workerCountOf(recheck) == 0) {
                this.addWorker((Runnable) null, false);
            }
            //>>change start
            else if (workerCountOf(recheck) < maximumPoolSize //
                && (activeWorkers.get() > workerCountOf(recheck) * threshold
                    || workQueue.size() > workerCountOf(recheck) * threshold)) {
                this.addWorker((Runnable) null, false);
            }
            //<<change end
        } else if (!this.addWorker(command, false)) {
            this.reject(command);
        }
    }
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.