Heim  >  Artikel  >  Java  >  Mehrere Implementierungsmethoden des Java-Thread-Pools und Antworten auf häufige Fragen

Mehrere Implementierungsmethoden des Java-Thread-Pools und Antworten auf häufige Fragen

黄舟
黄舟Original
2017-01-20 11:15:171393Durchsuche

Der folgende Editor bietet Ihnen verschiedene Implementierungsmethoden und Antworten auf häufig gestellte Fragen zu Java-Thread-Pools. Der Herausgeber findet es ziemlich gut, daher möchte ich es jetzt mit Ihnen teilen und als Referenz dienen. Folgen wir dem Herausgeber und werfen wir einen Blick darauf.

Threads sind oft an der Arbeit beteiligt. Beispielsweise werden einige Aufgaben häufig zur asynchronen Ausführung an Threads übergeben. Oder das Serverprogramm erstellt für jede Anfrage eine separate Thread-Verarbeitungsaufgabe. Außerhalb von Threads, z. B. der von uns verwendeten Datenbankverbindung. Diese Erstellungs-, Zerstörungs- oder Öffnungs- und Schließvorgänge wirken sich stark auf die Systemleistung aus. Daher kommt der Nutzen von „Pool“ zum Vorschein.

1. Warum einen Thread-Pool verwenden?

In der in Abschnitt 3.6.1 vorgestellten Implementierung wird jedem Client ein neuer Worker-Thread zugewiesen. Wenn die Kommunikation zwischen dem Worker-Thread und dem Client endet, wird dieser Thread zerstört. Diese Implementierung weist die folgenden Mängel auf:

• Der Aufwand für die Servererstellung und -zerstörung (einschließlich der aufgewendeten Zeit und Systemressourcen) ist groß. Es besteht keine Notwendigkeit, diesen Punkt zu erklären. Sie können den „Thread-Erstellungsprozess“ überprüfen. Zusätzlich zu der von der Maschine selbst geleisteten Arbeit müssen wir auch Instanziierungen und Starts durchführen, die alle Stapelressourcen erfordern.

• Zusätzlich zum Aufwand für das Erstellen und Zerstören von Threads verbrauchen aktive Threads auch Systemressourcen. Ich denke, dass es auch eine Überlegung wert ist, einen angemessenen Wert für die Anzahl der Datenbankverbindungen festzulegen.

•Wenn die Anzahl der Threads festgelegt ist und jeder Thread einen langen Lebenszyklus hat, ist auch der Threadwechsel relativ festgelegt. Verschiedene Betriebssysteme haben unterschiedliche Schaltzyklen, normalerweise etwa 20 ms. Bei der hier erwähnten Umschaltung handelt es sich um die Übertragung von CPU-Nutzungsrechten zwischen Threads im Rahmen der Planung des JVM und des zugrunde liegenden Betriebssystems. Wenn Threads häufig erstellt und zerstört werden, werden Threads häufig gewechselt, da nach der Zerstörung eines Threads die Nutzungsrechte an den bereits bereitstehenden Thread übergeben werden müssen, damit der Thread ausgeführt werden kann. In diesem Fall folgt der Wechsel zwischen Threads nicht mehr dem festen Wechselzyklus des Systems, und die Kosten für den Wechsel von Threads sind sogar noch höher als die Kosten für die Erstellung und Zerstörung.

Relativ gesehen werden bei Verwendung eines Thread-Pools einige Threads vorab erstellt, die kontinuierlich Aufgaben aus der Arbeitswarteschlange entnehmen und diese dann ausführen. Wenn der Arbeitsthread die Ausführung einer Aufgabe beendet hat, fährt er mit der Ausführung einer anderen Aufgabe in der Arbeitswarteschlange fort. Die Vorteile sind wie folgt:

• Reduziert die Anzahl der Erstellungen und Zerstörungen. Jeder Arbeitsthread kann jederzeit wiederverwendet werden und mehrere Aufgaben ausführen.

•Sie können die Anzahl der Threads im Thread-Pool einfach an die Tragfähigkeit des Systems anpassen, um Systemabstürze aufgrund übermäßigen Verbrauchs von Systemressourcen zu verhindern.

2. Einfache Implementierung des Thread-Pools

Das Folgende ist ein einfacher Thread-Pool, den ich selbst geschrieben habe, der auch direkt aus dem Buch Java Network Programming kopiert wurde

package thread;
 
import java.util.LinkedList;
 
/**
 * 线程池的实现,根据常规线程池的长度,最大长度,队列长度,我们可以增加数目限制实现
 * @author Han
 */
public class MyThreadPool extends ThreadGroup{
  //cpu 数量 ---Runtime.getRuntime().availableProcessors();
  //是否关闭
  private boolean isClosed = false;
  //队列
  private LinkedList<Runnable> workQueue;
  //线程池id
  private static int threadPoolID;
  private int threadID;
  public MyThreadPool(int poolSize){
    super("MyThreadPool."+threadPoolID);
    threadPoolID++;
    setDaemon(true);
    workQueue = new LinkedList<Runnable>();
    for(int i = 0;i<poolSize;i++){
      new WorkThread().start();
    }
  }
  //这里可以换成ConcurrentLinkedQueue,就可以避免使用synchronized的效率问题
  public synchronized void execute(Runnable task){
    if(isClosed){
      throw new IllegalStateException("连接池已经关闭...");
    }else{
      workQueue.add(task);
      notify();
    }
  }
   
  protected synchronized Runnable getTask() throws InterruptedException {
    while(workQueue.size() == 0){
      if(isClosed){
        return null;
      }
      wait();
    }
    return workQueue.removeFirst();
  }
   
  public synchronized void close(){
    if(!isClosed){
      isClosed = true;
      workQueue.clear();
      interrupt();
    }
  }
   
  public void join(){
    synchronized (this) {
      isClosed = true;
      notifyAll();
    }
    Thread[] threads = new Thread[activeCount()];
    int count = enumerate(threads);
    for(int i = 0;i<count;i++){
      try {
        threads[i].join();
      } catch (Exception e) {
      }
    }
  }
   
  class WorkThread extends Thread{
    public WorkThread(){
      super(MyThreadPool.this,"workThread"+(threadID++));
      System.out.println("create...");
    }
    @Override
    public void run() {
      while(!isInterrupted()){
        System.out.println("run..");
        Runnable task = null;
        try {
          //这是一个阻塞方法
          task = getTask();
           
        } catch (Exception e) {
           
        }
        if(task != null){
          task.run();
        }else{
          break;
        }
      }
    }
  }
}

Dieser Thread-Pool definiert hauptsächlich eine Arbeitswarteschlange und einige vorab erstellte Threads. Solange die Ausführungsmethode aufgerufen wird, kann die Aufgabe an den Thread gesendet werden.

Wenn keine Aufgabe vorhanden ist, blockiert der nachfolgende Thread in getTask(), bis eine neue Aufgabe eintrifft und aktiviert wird.

Sowohl Join als auch Close können zum Schließen des Thread-Pools verwendet werden. Der Unterschied besteht darin, dass „Join“ die Ausführung der Aufgaben in der Warteschlange beendet, während „Close“ die Warteschlange sofort löscht und alle Arbeitsthreads unterbricht. Der Interrupt() in close() entspricht dem Aufruf des jeweiligen Interrupt() der untergeordneten Threads in der ThreadGroup. Wenn sich ein Thread also im Warte- oder Ruhezustand befindet, wird eine InterruptException ausgelöst

Die Testklasse ist wie folgt:

public class TestMyThreadPool {
  public static void main(String[] args) throws InterruptedException {
    MyThreadPool pool = new MyThreadPool(3);
    for(int i = 0;i<10;i++){
      pool.execute(new Runnable() {
        @Override
        public void run() {
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
          }
          System.out.println("working...");
        }
      });
    }
    pool.join();
    //pool.close();
  }
}

3. Der von der JDK-Klassenbibliothek bereitgestellte Thread-Pool

Java bietet eine gute Thread-Pool-Implementierung, die robuster und effizienter ist als unsere eigene Implementierung auch leistungsfähiger.

Das Klassendiagramm sieht wie folgt aus:

Senioren haben bereits gute Erklärungen zu dieser Art von Thread-Pool gegeben. Jeder Java-Thread-Pool unter Baidu verfügt über sehr detaillierte Beispiele und Tutorials, daher werde ich hier nicht auf Details eingehen.

4. Spring-Injection-Thread-Pool

Wenn wir bei Verwendung des Spring-Frameworks die von Java bereitgestellte Methode zum Erstellen eines Thread-Pools verwenden, ist die Verwaltung in Multithread-Anwendungen sehr umständlich , und es ist nicht konform. Wir verwenden Frühlingsideen. (Obwohl Spring über statische Methoden injiziert werden kann)

Tatsächlich bietet Spring selbst auch eine gute Thread-Pool-Implementierung. Diese Klasse heißt ThreadPoolTaskExecutor.

Die Konfiguration im Frühjahr ist wie folgt:

<bean id="executorService" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <property name="corePoolSize" value="${threadpool.corePoolSize}" />
    <!-- 线程池维护线程的最少数量 -->
    <property name="keepAliveSeconds" value="${threadpool.keepAliveSeconds}" />
    <!-- 线程池维护线程所允许的空闲时间 -->
    <property name="maxPoolSize" value="${threadpool.maxPoolSize}" />
    <!-- 线程池维护线程的最大数量 -->
    <property name="queueCapacity" value="${threadpool.queueCapacity}" />
    <!-- 线程池所使用的缓冲队列 -->
  </bean>

5. Vorsichtsmaßnahmen für die Verwendung des Thread-Pools

•Deadlock

Jedes Multithread-Programm wird Es besteht die Gefahr eines Deadlocks. Die einfachste Situation sind zwei Threads AB, A hält Sperre 1 und fordert Sperre 2 an, und B hält Sperre 2 und fordert Sperre 1 an. (Diese Situation tritt auch bei der exklusiven Sperre von MySQL auf und die Datenbank meldet direkt eine Fehlermeldung.) Es gibt eine andere Art von Deadlock im Thread-Pool: Angenommen, alle Arbeitsthreads im Thread-Pool sind während der Ausführung ihrer jeweiligen Aufgaben blockiert und warten auf das Ausführungsergebnis einer bestimmten Aufgabe A. Allerdings befindet sich Aufgabe A in der Warteschlange und kann nicht ausgeführt werden, da kein Leerlauf-Thread vorhanden ist. Auf diese Weise werden alle Ressourcen im Thread-Pool für immer blockiert und es kommt zu einem Deadlock.

•Unzureichende Systemressourcen

Wenn die Anzahl der Threads im Thread-Pool sehr groß ist, verbrauchen diese Threads eine große Menge an Ressourcen, einschließlich Speicher und anderen Systemressourcen, wodurch die Systemleistung ernsthaft beeinträchtigt wird .

•Parallelitätsfehler

Die Arbeitswarteschlange des Thread-Pools basiert auf den Methoden wait () und notify (), damit der Arbeitsthread Aufgaben rechtzeitig abrufen kann. Diese beiden Methoden sind jedoch schwierig zu verwenden. Wenn der Code falsch ist, können Benachrichtigungen verloren gehen, was dazu führt, dass der Arbeitsthread im Leerlauf bleibt und die Aufgaben ignoriert, die in der Arbeitswarteschlange verarbeitet werden müssen. Weil es am besten ist, einige ausgereiftere Thread-Pools zu verwenden.

• Thread-Lecks

Ein ernstes Risiko bei der Verwendung von Thread-Pools sind Thread-Lecks. Wenn bei einem Thread-Pool mit einer festen Anzahl von Arbeitsthreads der Arbeitsthread beim Ausführen einer Aufgabe eine RuntimeException oder einen Fehler auslöst und diese Ausnahmen oder Fehler nicht abgefangen werden, wird der Arbeitsthread abnormal beendet, was dazu führt, dass der Thread-Pool dauerhaft verloren geht ein Thread. (Das ist so interessant)

Eine andere Situation ist, dass der Arbeitsthread beim Ausführen einer Aufgabe blockiert wird. Wenn er auf die Eingabe von Daten durch den Benutzer wartet, der Benutzer jedoch keine Daten eingegeben hat, wurde der Thread blockiert . Ein solcher Arbeitsthread existiert nur dem Namen nach und führt tatsächlich keine Aufgaben aus. Wenn sich alle Threads im Thread-Pool in diesem Zustand befinden, kann der Thread-Pool keine neuen Aufgaben hinzufügen.

•Aufgabenüberlastung

Wenn in der Worker-Thread-Warteschlange eine große Anzahl von Aufgaben zur Ausführung in der Warteschlange steht, verbrauchen diese Aufgaben selbst möglicherweise zu viele Systemressourcen und verursachen Ressourcenmangel.

Zusammenfassend müssen Sie bei der Verwendung des Thread-Pools die folgenden Prinzipien befolgen:

1. Wenn Task A während der Ausführung synchron auf das Ausführungsergebnis von Task B warten muss, dann Task A ist nicht geeignet. Treten Sie der Arbeitswarteschlange des Thread-Pools bei. Wenn Sie Aufgaben wie Aufgabe A, die auf die Ausführungsergebnisse anderer Aufgaben warten müssen, zur Warteschlange hinzufügen, kann dies zu einem Deadlock führen Für längere Zeit sollten Sie eine Zeitüberschreitung festlegen, um zu verhindern, dass der Arbeitsthread dauerhaft blockiert wird und Thread-Lecks auftreten. Wenn der Thread im Serverprogramm auf die Verbindung des Clients oder auf die vom Client gesendeten Daten wartet, kann dies zu einer Blockierung führen. Sie können die Zeit wie folgt festlegen:

Rufen Sie setSotimeout auf Methode von ServerSocket, um die Zeit festzulegen, die auf die Zeitüberschreitung des Clients gewartet werden soll.

Rufen Sie für jeden mit dem Client verbundenen Socket die setSoTImeout-Methode des Sockets auf, um die Zeitüberschreitungsdauer für das Warten auf das Senden von Daten durch den Client festzulegen.

3. Verstehen Sie die Eigenschaften der Aufgabe und analysieren Sie, ob die Aufgabe IO-Vorgänge ausführt, die häufig blockieren, oder Vorgänge ausführt, die nie blockieren. Ersteres belegt die CPU zeitweise, während letzteres eine höhere Auslastung aufweist. Schätzen Sie, wie lange es dauern wird, die Aufgabe zu erledigen, unabhängig davon, ob es sich um eine kurzfristige oder eine langfristige Aufgabe handelt, klassifizieren Sie die Aufgaben dann entsprechend den Merkmalen der Aufgaben und fügen Sie dann verschiedene Arten von Aufgaben zu den Arbeitswarteschlangen hinzu von verschiedenen Thread-Pools, damit sie entsprechend den Eigenschaften der Aufgaben verarbeitet werden können, Zuordnung und Anpassung jedes Thread-Pools

4. Passen Sie die Größe des Thread-Pools an. Die optimale Größe des Thread-Pools hängt hauptsächlich von der Anzahl der verfügbaren CPUs im System und den Eigenschaften der Aufgaben in der Arbeitswarteschlange ab. Wenn es auf einem System mit N CPUs nur eine Arbeitswarteschlange gibt und es sich bei allen um Rechenaufgaben (nicht blockierend) handelt, wird im Allgemeinen die maximale CPU-Nutzungsrate erreicht, wenn der Thread-Pool über N oder N+1 Worker-Threads verfügt .

Wenn die Arbeitswarteschlange Aufgaben enthält, die E/A-Vorgänge ausführen und häufig blockieren, sollte die Größe des Thread-Pools die Anzahl der verfügbaren CPUs überschreiten, da nicht alle Arbeitsthreads ständig arbeiten. Wählen Sie eine typische Aufgabe aus und schätzen Sie dann das Verhältnis WT/ST der Wartezeit zur tatsächlichen Zeit, die die CPU für Berechnungen in dem Projekt einnimmt, das diese Aufgabe ausführt. Für ein System mit N CPUs müssen ungefähr N*(1+WT/ST) Threads eingerichtet werden, um sicherzustellen, dass die CPU vollständig ausgelastet ist.

Natürlich ist bei der Anpassung des Thread-Pools nicht nur die CPU-Auslastung zu berücksichtigen. Mit zunehmender Anzahl von Thread-Pool-Jobs werden Sie auch auf Einschränkungen im Speicher oder bei anderen Ressourcen stoßen, z. B. bei Sockets und offenen Dateien Handle oder Anzahl der Datenbankverbindungen usw. Es muss sichergestellt werden, dass die von mehreren Threads verbrauchten Systemressourcen innerhalb des Bereichs liegen, den das System ertragen kann.

5. Vermeiden Sie Aufgabenüberlastung. Der Server sollte die Anzahl gleichzeitiger Clientverbindungen basierend auf der Tragfähigkeit des Systems begrenzen. Wenn die Verbindung des Clients das Limit überschreitet, kann der Server die Verbindung ablehnen und eine freundliche Eingabeaufforderung ausgeben oder die Warteschlangenlänge begrenzen.

Die oben genannten sind verschiedene Implementierungsmethoden des Java-Thread-Pools und der Inhalt von FAQs. Weitere verwandte Inhalte finden Sie auf der chinesischen PHP-Website (www.php.cn).

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn