ThreadPoolExecutor, sıraya girmeden önce iş parçacıklarını maksimuma çıkarmak için nasıl elde edilir?


100

Bir süredir çoğumuzun kullandığı iş parçacığı havuzlarını ThreadPoolExecutordestekleyen varsayılan davranıştan dolayı hayal kırıklığına uğradım ExecutorService. Javadocs'tan alıntı yapmak için:

CorePoolSize değerinden fazla ancak maksimumPoolSize iş parçacığından daha az çalışıyorsa, yeni bir iş parçacığı yalnızca kuyruk doluysa oluşturulur .

Bunun anlamı, aşağıdaki kodla bir iş parçacığı havuzu tanımlarsanız , sınırlanmamış olduğu için hiçbir zaman 2. iş parçacığını başlatmayacaktır LinkedBlockingQueue.

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

Yalnızca sınırlı bir kuyruğunuz varsa ve kuyruk doluysa , çekirdek numarasının üzerindeki herhangi bir iş parçacığı başlatılır. Çok sayıda junior Java çok iş parçacıklı programcının ThreadPoolExecutor,.

Şimdi bunun optimal olmadığı özel kullanım durumum var. Kendi TPE sınıfımı yazmadan, bunun etrafında çalışmanın yollarını arıyorum.

Gereksinimlerim, muhtemelen güvenilmez bir üçüncü tarafa geri arama yapan bir web hizmeti içindir.

  • Geri aramayı web isteği ile eşzamanlı olarak yapmak istemiyorum, bu yüzden bir iş parçacığı havuzu kullanmak istiyorum.
  • Genellikle bunlardan birkaç dakika alırım, bu yüzden newFixedThreadPool(...) çoğunlukla uykuda olan çok sayıda iş parçacığına .
  • Sık sık bu trafiğin patlamasını alıyorum ve iş parçacığı sayısını maksimum değere yükseltmek istiyorum (50 diyelim).
  • Tüm geri aramaları yapmak için en iyi girişimde bulunmalıyım, bu nedenle 50'nin üzerindeki herhangi bir geri aramayı sıraya koymak istiyorum. Web sunucumun geri kalanını a kullanarak boğmak istemiyorum newCachedThreadPool().

Daha fazla iş parçacığı başlatılmadan önceThreadPoolExecutor kuyruğun sınırlı ve dolu olması gereken yerlerde bu sınırlamayı nasıl aşabilirim ? Görevleri sıraya koymadan önce daha fazla iş parçacığı başlatmasını nasıl sağlayabilirim ?

Düzenle:

@Flavio ThreadPoolExecutor.allowCoreThreadTimeOut(true), çekirdek iş parçacıklarının zaman aşımına uğraması ve çıkması için kullanma konusunda iyi bir noktaya işaret ediyor . Bunu düşündüm ama yine de çekirdek iş parçacığı özelliğini istedim. Havuzdaki iş parçacığı sayısının mümkünse çekirdek boyutunun altına düşmesini istemedim.


1
Örneğinizin maksimum 10 iş parçacığı oluşturduğunu düşünürsek, sabit boyutlu bir iş parçacığı havuzunda büyüyen / küçülen bir şeyin kullanılmasında gerçek bir tasarruf var mı?
bstempi

İyi nokta @bstempi. Sayı biraz keyfi idi. Sorudaki değeri 50'ye çıkardım. Şu anda bu çözüme sahip olduğum için aslında kaç tane eşzamanlı iş parçacığı çalışmak istediğimi tam olarak bilmiyorum.
Grey

1
Lanet olsun! Burada yapabilseydim 10 olumlu oy, benim bulunduğum pozisyonun aynısı.
Eugene

Yanıtlar:


51

ThreadPoolExecutorDaha fazla iş parçacığı başlatılmadan önce kuyruğun sınırlı ve dolu olması gereken yerlerde bu sınırlamayı nasıl aşabilirim.

Sonunda bu sınırlamaya biraz zarif (belki biraz hile) bir çözüm bulduğuma inanıyorum ThreadPoolExecutor. Halihazırda sıraya alınmış bazı görevler olduğunda LinkedBlockingQueuegeri dönmesi falseiçin genişletmeyi içerir queue.offer(...). Mevcut iş parçacıkları sıraya alınmış görevlere yetişmiyorsa, TPE ek iş parçacıkları ekleyecektir. Havuz zaten maksimum iş parçacığında ise, o RejectedExecutionHandlerzaman çağrılacaktır. Daha sonra put(...)kuyruğa giren işleyicidir .

Geri offer(...)dönebilecek falseve put()asla engellemeyecek bir kuyruk yazmak kesinlikle gariptir, bu yüzden hack kısmı budur. Ancak bu, TPE'nin kuyruk kullanımıyla iyi çalışıyor, bu yüzden bunu yaparken herhangi bir sorun görmüyorum.

İşte kod:

// 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;
        }
    }
});

Bu mekanizma ile, görevleri kuyruğa gönderdiğimde, ThreadPoolExecutorirade:

  1. Başlangıçta çekirdek boyutuna kadar iş parçacığı sayısını ölçekleyin (burada 1).
  2. Sıraya sunun. Kuyruk boşsa, mevcut evreler tarafından işlenmek üzere sıraya alınacaktır.
  3. Kuyrukta zaten 1 veya daha fazla öğe varsa, offer(...)yanlış döndürür.
  4. Yanlış döndürülürse, havuzdaki iş parçacığı sayısını maksimum sayıya ulaşana kadar ölçekleyin (burada 50).
  5. En fazla ise, o zaman RejectedExecutionHandler
  6. Ardından RejectedExecutionHandler, görevi FIFO sırasındaki ilk kullanılabilir iş parçacığı tarafından işlenmek üzere kuyruğa koyar.

Yukarıdaki örnek kodumda, kuyruk sınırsız olmasına rağmen, onu sınırlı bir kuyruk olarak da tanımlayabilirsiniz. Örneğin, 1000 kapasite eklerseniz, LinkedBlockingQueueo zaman:

  1. konuları maksimuma ölçeklendirin
  2. sonra 1000 görevle dolana kadar sıraya girin
  3. daha sonra kuyrukta yer olana kadar arayanı engelleyin.

Ayrıca, offer(...)içinde RejectedExecutionHandlerkullanmanız gerekirse , zaman aşımı olarak offer(E, long, TimeUnit)bunun yerine yöntemi kullanabilirsiniz Long.MAX_VALUE.

Uyarı:

Eğer görevleri uygulamakla eklenecek bekliyorsanız sonra bu kapatma olmuştur o zaman atma hakkında daha verimli olmasını isteyebilirsiniz, RejectedExecutionExceptionbizim özel dışına RejectedExecutionHandlerinfaz hizmet kapatma olmuştur zaman. @RaduToader'a bunu işaret ettiği için teşekkürler.

Düzenle:

Bu cevaba yönelik başka bir ince ayar, TPE'ye boşta iş parçacığı olup olmadığını sormak ve varsa öğeyi yalnızca kuyruğa almak olabilir. Bunun için gerçek bir sınıf oluşturmanız ve ourQueue.setThreadPoolExecutor(tpe);üzerine yöntem eklemeniz gerekir.

O zaman offer(...)yönteminiz şöyle görünebilir:

  1. Olmadığını kontrol edin tpe.getPoolSize() == tpe.getMaximumPoolSize(), bu durumda sadece ara super.offer(...).
  2. Aksi takdirde boşta iş parçacığı olduğu için tpe.getPoolSize() > tpe.getActiveCount()arayın super.offer(...).
  3. Aksi takdirde falsebaşka bir iş parçacığını çatallamaya geri dönün .

Belki bu:

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

TPE üzerindeki alma yöntemlerinin, volatilealanlara eriştikleri veya (olması durumunda getActiveCount()) TPE'yi kilitledikleri ve iş parçacığı listesinde yürüdükleri için pahalı olduğuna dikkat edin . Ayrıca, burada bir görevin yanlış bir şekilde sıralanmasına veya boşta bir iş parçacığı olduğunda başka bir iş parçacığının çatallanmasına neden olabilecek yarış koşulları vardır.


Ben de aynı sorunla mücadele ettim, yürütme yöntemini geçersiz kılmaya başladım. Ama bu gerçekten güzel bir çözüm. :)
Batty

Bunu Queuebaşarmak için sözleşmeyi bozma fikrini sevmesem
bstempi

3
Burada ilk birkaç görevin sıraya girmesi ve ancak bu yeni iş parçacığı ortaya çıktıktan sonra bir tuhaflık görmüyor musunuz? Örneğin, sizin tek çekirdek parçacığı tek uzun süren görev ile meşgulse ve çağrı execute(runnable)ardından runnablesadece kuyruğuna eklenir. Eğer ararsanız execute(secondRunnable), secondRunnablekuyruğa eklenir. Ama şimdi ararsanız execute(thirdRunnable), thirdRunnableyeni bir ileti dizisinde çalıştırılacaktır. runnableVe secondRunnablesadece bir kez çalıştırın thirdRunnable(veya görevi uzun süren orijinal) bitmiştir.
Robert Tupelo-Schneck

1
Evet, Robert haklı, çok iş parçacıklı bir ortamda, kullanılacak ücretsiz iş parçacıkları varken kuyruk bazen büyüyor. Aşağıdaki TPE'yi genişleten çözüm - çok daha iyi çalışıyor. Yukarıdaki hack ilginç olsa da Robert'ın önerisinin cevap olarak işaretlenmesi gerektiğini düşünüyorum
Wanna Know All

1
"Reddedilen Yürütme İşleyicisi" kapatma sırasında yürütücüye yardımcı oldu. Şimdi shutdown () yeni görevlerin eklenmesini engellemediği için (reque nedeniyle) shutdownNow () kullanmak zorunda kalıyorsunuz
Radu Toader

28

Çekirdek boyutunu ve maksimum boyutu aynı değere ayarlayın ve çekirdek iş parçacıklarının havuzdan kaldırılmasına izin verin allowCoreThreadTimeOut(true).


+1 Evet Bunu düşündüm ama yine de çekirdek-iş parçacığı özelliğine sahip olmak istedim. İş parçacığı havuzunun uykuda olduğu dönemlerde 0 iş parçacığına gitmesini istemedim. Bunu belirtmek için sorumu düzenleyeceğim. Ama mükemmel bir nokta.
Grey

Teşekkür ederim! Bunu yapmanın en basit yolu bu.
Dmitry Ovchinnikov

28

Bu soruya zaten iki cevabım daha var, ama bunun en iyisi olduğundan şüpheleniyorum.

Şu anda kabul edilen cevabın tekniğine , yani:

  1. Sıranın offer()yöntemini (bazen) yanlış döndürmek için geçersiz kılın ,
  2. bu, ThreadPoolExecutoryeni bir iş parçacığı oluşturmasına veya görevi reddetmesine neden olur ve
  3. ayarlamak RejectedExecutionHandleriçin aslında reddi üzerine görevi sıraya.

Sorun ne zaman offer()yanlış döndürülmesi gerektiğidir. Şu anda kabul edilen cevap, kuyrukta birkaç görev olduğunda yanlış döndürüyor, ancak oradaki yorumumda da belirttiğim gibi, bu istenmeyen etkilere neden oluyor. Alternatif olarak, her zaman yanlış döndürürseniz, kuyrukta bekleyen iş parçacıklarınız olsa bile yeni iş parçacıkları üretmeye devam edersiniz.

Çözüm, Java 7 kullanmak LinkedTransferQueueve offer()arama yapmaktır tryTransfer(). Bekleyen bir tüketici iş parçacığı olduğunda, görev yalnızca o iş parçacığına aktarılır. Aksi takdirde, offer()false döndürür ve ThreadPoolExecutoryeni bir iş parçacığı oluşturur.

    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();
            }
        }
    });

Kabul etmeliyim, bu bana çok temiz görünüyor. Çözümün tek dezavantajı, LinkedTransferQueue'nun sınırsız olmasıdır, böylece fazladan çalışma olmadan kapasite sınırlı bir görev kuyruğu elde edemezsiniz.
Yeroc

Havuz maksimum boyuta ulaştığında bir sorun var. Diyelim ki havuz maksimum boyuta kadar ölçeklendi ve her iş parçacığı şu anda bir görevi yürütüyor, çalıştırılabilir sunulduğunda bu teklif impl yanlış döndürür ve ThreadPoolExecutor addWorker iş parçacığı eklemeye çalışır, ancak havuz zaten maksimuma ulaştı, bu nedenle çalıştırılabilir yalnızca reddedilir. Reddedilen ExceHandler'a göre, yazdığınız tekrar sıraya sunulacak ve bu maymun dansı yeniden baştan gerçekleşecek.
Sudheera

1
@Sudheera Yanıldığına inanıyorum. queue.offer()çünkü aslında arıyorLinkedTransferQueue.tryTransfer() , yanlış döndürür ve görevi sıraya koymaz. Ancak başarısız olmayan ve görevi sıraya koyan RejectedExecutionHandlerçağrılar queue.put().
Robert Tupelo-Schneck 21

1
@ RobertTupelo-Schneck son derece kullanışlı ve güzel!
Eugene

1
@ RobertTupelo-Schneck Büyüleyici çalışıyor! Neden javada kutunun dışında böyle bir şey olmadığını bilmiyorum
Java'nın

7

Not: Şimdi diğer cevabımı tercih ediyor ve tavsiye ediyorum .

İşte bana çok daha basit gelen bir sürüm: Yeni bir görev yürütüldüğünde corePoolSize değerini artırın (maximumPoolSize sınırına kadar), ardından corePoolSize değerini azaltın (kullanıcının belirlediği "çekirdek havuz boyutu" sınırına kadar) görev tamamlandı.

Başka bir deyişle, çalışan veya sıraya alınmış görevlerin sayısını takip edin ve corePoolSize'ın, kullanıcının belirlediği "çekirdek havuz boyutu" ile maximumPoolSize arasında olduğu sürece görev sayısına eşit olduğundan emin olun.

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);
    }
}

Yazıldığı gibi, sınıf, kullanıcı tarafından belirtilen corePoolSize veya maximumPoolSize'ın yapım sonrasında değiştirilmesini desteklemez ve iş kuyruğunun doğrudan veya aracılığıyla değiştirilmesini desteklemez. remove() veya purge().


synchronizedBloklar dışında seviyorum . Görevlerin sayısını öğrenmek için kuyruğu arayabilir misin? Ya da belki bir AtomicInteger?
Grey

Onlardan kaçınmak istedim ama sorun bu. execute()Ayrı iş parçacıklarında bir dizi arama varsa , her biri (1) kaç iş parçacığının gerekli olduğunu, (2) setCorePoolSizebu numaraya ve (3) aramayı bulacaktır super.execute(). Adım (1) ve (2) senkronize değilse, daha yüksek bir sayıdan sonra çekirdek havuz boyutunu daha düşük bir sayıya ayarladığınız talihsiz bir sıralamayı nasıl önleyeceğimi bilmiyorum. Üst sınıf alanına doğrudan erişim ile bu, bunun yerine karşılaştırma ve ayarlama kullanılarak yapılabilir, ancak bunu senkronizasyon olmadan bir alt sınıfta yapmanın temiz bir yolunu görmüyorum.
Robert Tupelo-Schneck

taskCountAlan geçerli olduğu sürece bu yarış koşulunun cezalarının nispeten düşük olduğunu düşünüyorum (yani a AtomicInteger). İki iş parçacığı havuz boyutunu birbirinin hemen ardından yeniden hesaplarsa, uygun değerleri almalıdır. Eğer ikincisi çekirdek iş parçacığını küçültürse, kuyrukta bir düşüş ya da başka bir şey görmüş olmalı.
Grey

1
Maalesef bundan daha kötü olduğunu düşünüyorum. 10. ve 11. görevlerin çağırdığını varsayalım execute(). Her biri arayacak atomicTaskCount.incrementAndGet()ve sırasıyla 10 ve 11 alacaklar. Ancak senkronizasyon olmadan (görev sayısını alma ve çekirdek havuz boyutunu ayarlama üzerinden), daha sonra (1) görev 11'i çekirdek havuz boyutunu 11'e, (2) görev 10, çekirdek havuz boyutunu 10'a ayarlayabilir, (3) görev 10 çağrı super.execute(), (4) görev 11 çağrı super.execute()ve sıraya alınır.
Robert Tupelo-Schneck

2
Bu çözüme bazı ciddi testler verdim ve açıkça en iyisi bu. Çok iş parçacıklı ortamda, bazen serbest iş parçacıkları olduğunda (serbest iş parçacıklı TPE.execute doğası nedeniyle) hala sıraya girecektir, ancak yarış koşulunun daha fazla şansa sahip olduğu yanıt olarak işaretlenmiş çözümün aksine nadiren gerçekleşir. olur, bu yüzden bu hemen hemen her çok iş parçacıklı çalışmada olur.
Bilmek İstiyorum

6

ThreadPoolExecutorEk creationThresholdve geçersiz kılmalar alan bir alt sınıfımız var 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);
            }
        }
    }
}

belki bu da yardımcı olur, ama seninki elbette daha sanatsal görünüyor ...


İlginç. Bunun için teşekkürler. Aslında çekirdek boyutunun değiştirilebilir olduğunu bilmiyordum.
Grey

Şimdi biraz daha düşünüyorum, bu çözüm kuyruğun boyutunu kontrol etmek açısından benimkinden daha iyi. offer(...)Yöntemin yalnızca falsekoşullu olarak geri dönmesi için cevabımı değiştirdim . Teşekkürler!
Grey

4

Önerilen yanıt, JDK iş parçacığı havuzundaki sorunun yalnızca birini (1) çözer:

  1. JDK iş parçacığı havuzları kuyruğa alma eğilimindedir. Bu nedenle, yeni bir iş parçacığı oluşturmak yerine görevi sıraya koyacaklar. Yalnızca kuyruk sınırına ulaştığında, iş parçacığı havuzu yeni bir iş parçacığı oluşturur.

  2. Yük hafiflediğinde iplik çıkmaz. Örneğin, havuzun maksimuma çıkmasına neden olan havuza isabet eden bir iş patlaması yaşarsak, ardından bir seferde en fazla 2 görevden oluşan hafif yük gelirse, havuz, iş parçacığının emekli olmasını önleyen hafif yüke hizmet etmek için tüm iş parçacıkları kullanır. (yalnızca 2 iş parçacığı gerekli olacaktır…)

Yukarıdaki davranıştan memnun kalmadım ve yukarıdaki eksikliklerin üstesinden gelmek için bir havuz oluşturdum.

Çözmek için 2) Lifo planlamayı kullanmak sorunu çözer. Bu fikir, Ben Maurer tarafından ACM uygulama 2015 konferansında sunulmuştur: Systems @ Facebook scale

Böylece yeni bir uygulama doğdu:

LifoThreadPoolExecutorSQP

Şimdiye kadar bu uygulama, ZEL için eşzamansız yürütme performansını iyileştirdi .

Uygulama, belirli kullanım durumları için üstün performans sağlayarak bağlam değiştirme yükünü azaltma yeteneğine sahiptir.

Umarım yardımcı olur...

Not: JDK Çatal Birleştirme Havuzu, ExecutorService'i uygular ve "normal" bir iş parçacığı havuzu olarak çalışır, Uygulama performanslıdır, LIFO İş Parçacığı zamanlamasını kullanır, ancak dahili kuyruk boyutu, emeklilik zaman aşımı üzerinde kontrol yoktur ... ve en önemlisi görevler olamaz onları iptal ederken kesintiye uğradı


1
Bu uygulamanın çok fazla dış bağımlılığı olması çok kötü. Benim için faydasız Yapımı: - /
Martin L.

1
Bu gerçekten iyi bir nokta (2.). Maalesef, uygulama dış bağımlılıklardan net değil, ancak isterseniz yine de benimsenebilir.
Alexey Vlasov

1

Not: Şimdi diğer cevabımı tercih ediyor ve tavsiye ediyorum .

Sırayı yanlış döndürmek için değiştirme şeklindeki orijinal fikri izleyen başka bir teklifim var. Bunda tüm görevler kuyruğa girebilir, ancak bir görev daha sonra sıraya alındığındaexecute() , sıranın reddettiği nöbetçi bir işlemsiz görevle onu takip ederiz ve yeni bir iş parçacığının ortaya çıkmasına neden olur, bu da işlemsizin hemen ardından kuyruktan bir şey.

Çalışan iş parçacıkları LinkedBlockingQueueyeni bir görev için yoklama yapıyor olabileceğinden , kullanılabilir bir iş parçacığı olsa bile bir görevin sıraya alınması mümkündür. Kullanılabilir iş parçacıkları olsa bile yeni iş parçacığı üretmekten kaçınmak için, kuyruktaki yeni görevler için kaç iş parçacığının beklediğini takip etmemiz ve kuyrukta bekleyen iş parçacıklarından daha fazla görev olduğunda yalnızca yeni bir iş parçacığı oluşturmamız gerekir.

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

Aklıma gelen en iyi çözüm, genişletmek.

ThreadPoolExecutorbirkaç kanca yöntemi sunar: beforeExecuteve afterExecute. Uzantınızda, görevleri beslemek için sınırlı bir kuyruk ve taşmayı işlemek için ikinci bir sınırsız kuyruk kullanabilirsiniz. Biri aradığında submit, isteği sınırlı kuyruğa yerleştirmeyi deneyebilirsiniz. Bir istisna ile karşılaşırsanız, görevi taşma kuyruğunuza yapıştırmanız yeterlidir. Daha sonra kullanabilirsinizafterExecute bir görevi tamamladıktan sonra taşma kuyruğunda herhangi bir şey olup olmadığını görmek kancayı . Bu şekilde, uygulayıcı önce sınırlı kuyruğundaki şeylerle ilgilenecek ve zamanın izin verdiği ölçüde bu sınırsız kuyruktan otomatik olarak çekilecektir.

Çözümünüzden daha fazla iş gibi görünüyor, ancak en azından kuyruklara beklenmedik davranışlar vermeyi içermiyor. Ayrıca, atılması oldukça yavaş olan istisnalara güvenmek yerine kuyruğun ve iş parçacığının durumunu kontrol etmenin daha iyi bir yolu olduğunu hayal ediyorum.


Bu çözüme düşkün değilim. ThreadPoolExecutor'un kalıtım için tasarlanmadığından oldukça eminim.
scottb

JavaDoc'ta aslında bir uzantı örneği var. Muhtemelen kanca yöntemlerini uygulayacaklarını belirtiyorlar, ancak genişletme sırasında nelere dikkat etmeniz gerektiğini size söylüyorlar.
bstempi

0

Not: JDK ThreadPoolExecutor için sınırlı bir kuyruğunuz olduğunda, sadece teklif yanlış döndürdüğünde yeni evreler oluşturursunuz. CallerRunsPolicy ile yararlı bir şey elde edebilirsiniz, bu da biraz BackPressure oluşturur ve doğrudan arayan iş parçacığında çalıştırılan çağrılardır.

Havuz tarafından oluşturulan iş parçacıklarından yürütülmesi gereken görevlere ihtiyacım var ve zamanlama için sınırsız bir kuyruğa sahipken, havuzdaki iş parçacığı sayısı corePoolSize ve maximumPoolSize arasında büyüyebilir veya küçülebilir , yani ...

Bir yapıyor sona erdi tam kopya yapıştırmak gelen ThreadPoolExecutor ve değiştirmek çünkü biraz yöntemi yürütmek maalesef bu uzantı tarafından yapılabilir değil (o özel yöntemler çağırır).

Yeni istek geldiğinde ve tüm iş parçacıkları meşgul olduğunda hemen yeni iş parçacıkları oluşturmak istemedim (çünkü genel olarak kısa ömürlü görevlerim var). Bir eşik ekledim, ancak bunu ihtiyaçlarınıza göre değiştirmekten çekinmeyin (belki de çoğunlukla IO için bu eşiği kaldırmak daha iyidir)

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);
        }
    }
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.