Parallelität ist die Fähigkeit, mehrere Programme parallel oder mehrere Teile eines Programms parallel auszuführen. Wenn eine zeitaufwändige Aufgabe im Programm asynchron oder parallel ausgeführt werden kann, werden der Durchsatz und die Interaktivität des gesamten Programms erheblich verbessert. Moderne PCs verfügen über mehrere CPUs oder mehrere Kerne in einer CPU. Ob die Fähigkeit, mehrere Kerne richtig zu nutzen, zum Schlüssel für eine groß angelegte Anwendung wird.
Grundlegende Verwendung von Threads
Es gibt zwei Möglichkeiten, Code zu schreiben, der ausgeführt wird, wenn ein Thread ausgeführt wird: Eine besteht darin, eine Instanz der Thread-Unterklasse zu erstellen und die Ausführungsmethode zu überschreiben, und die andere Die zweite besteht darin, bei der Implementierung der Runnable-Schnittstelle eine Klasse zu erstellen. Natürlich ist die Implementierung von Callable auch eine Möglichkeit, den Rückgabewert nach Abschluss der Aufgabe zu erhalten, aber die Methoden Runnable und Thread können das Ergebnis nach der Ausführung der Aufgabe nicht erhalten.
public class ThreadMain { public static void main(String[] args) { MyThread myThread = new MyThread(); new Thread(myThread).start(); new MyThreas2().start(); } }// 第一种方式,实现Runable接口class MyThread implements Runnable { @Override public void run() { System.out.println("MyThread run..."); } }// 第二种方式,继承Thread类,重写run()方法class MyThreas2 extends Thread { @Override public void run() { System.out.println("MyThread2 run..."); } }
Sobald der Thread gestartet ist, kehrt die start()-Methode sofort zurück, ohne darauf zu warten, dass die run()-Methode die Ausführung abschließt, als ob die run-Methode auf einer anderen CPU ausgeführt würde.
Hinweis: Ein häufiger Fehler beim Erstellen und Ausführen eines Threads besteht darin, die run()-Methode des Threads anstelle der start()-Methode aufzurufen, wie unten gezeigt:
Thread newThread = new Thread(MyRunnable()); newThread.run(); //should be start();
Zuerst Sie werden das Gefühl haben, dass etwas nicht stimmt, denn die run()-Methode wird tatsächlich wie erwartet aufgerufen. Tatsächlich wird die run()-Methode jedoch nicht vom gerade erstellten neuen Thread ausgeführt, sondern vom aktuellen Thread. Das heißt, es wird von dem Thread ausgeführt, der die beiden oben genannten Codezeilen ausführt. Wenn Sie möchten, dass der erstellte neue Thread die Methode run() ausführt, müssen Sie die Startmethode des neuen Threads aufrufen.
Callable und Future werden kombiniert, um das Abrufen des Rückgabewerts nach der Ausführung der Aufgabe zu implementieren:
public static void main(String[] args) { ExecutorService exec = Executors.newSingleThreadExecutor(); Future<String> future = exec.submit(new CallTask()); System.out.println(future.get()); }class CallTask implements Callable { public String call() { return "hello"; } }
Legen Sie den Thread-Namen für den Thread fest:
MyTask myTask = new MyTask(); Thread thread = new Thread(myTask, "myTask thread");
thread.start(); System.out.println(thread.getName());
Bei der Erstellung Beim Erstellen eines Threads können Sie dem Thread einen Namen geben. Es hilft uns, zwischen verschiedenen Threads zu unterscheiden.
Volatile
Sowohl synchronisiert als auch Volatile spielen eine wichtige Rolle in der gleichzeitigen Multithread-Programmierung. Sichtbarkeit bedeutet, dass ein anderer Thread den geänderten Wert lesen kann, wenn ein Thread eine gemeinsam genutzte Variable ändert. In einigen Fällen ist es kostengünstiger als synchronisiert, aber volatile kann die Atomizität von Variablen nicht garantieren.
Wenn eine flüchtige Variable geschrieben wird (es gibt eine Sperranweisung unter Assembly), hat die Sperranweisung in einem Mehrkernsystem zwei Funktionen:
Schreiben Sie die aktuelle CPU-Cache-Zeile zurück in die Systemspeicher.
Dieser Rückschreibvorgang führt dazu, dass die von anderen CPUs zwischengespeicherten Daten die Adresse ändern und ungültig werden.
Mehrere CPUs folgen dem Cache-Konsistenzprinzip. Jede CPU prüft, ob ihr Cache-Wert abgelaufen ist, indem sie die auf dem Bus übertragenen Daten abhört. Wenn festgestellt wird, dass die dem Cache entsprechende Speicheradresse geändert wurde Wenn der Status auf „Ungültig“ gesetzt ist, wird der nächste Datenvorgang erneut aus dem Systemspeicher gelesen. Für weitere Informationen zu Volatile klicken Sie bitte für eine ausführliche Analyse der Implementierungsprinzipien von Volatile.
synchronized
Synchronized war schon immer eine Veteranenrolle in der gleichzeitigen Multithread-Programmierung. Viele Leute werden es als Schwergewichtssperre bezeichnen, aber mit Java SE1.6 wurde Synchronized auf verschiedene Weise optimiert Danach gab es Fälle, in denen es nicht mehr so schwer war.
Jedes Objekt in Java kann als Sperre verwendet werden. Wenn ein Thread versucht, auf einen synchronisierten Codeblock zuzugreifen, muss er zuerst die Sperre erhalten und die Sperre aufheben, wenn er beendet wird oder eine Ausnahme auslöst.
Bei synchronisierten Methoden ist die Sperre das aktuelle Instanzobjekt.
Bei statischen Synchronisationsmethoden ist die Sperre das Klassenobjekt des aktuellen Objekts.
Bei synchronisierten Methodenblöcken ist die Sperre das in Synchronized-Klammern konfigurierte Objekt.
Das synchronisierte Schlüsselwort kann nicht vererbt werden, was bedeutet, dass die synchronisierte Methode in der Basisklasse standardmäßig nicht in der Unterklasse synchronisiert ist. Wenn ein Thread versucht, auf einen synchronisierten Codeblock zuzugreifen, muss er zuerst die Sperre erwerben und die Sperre aufheben, wenn er beendet wird oder eine Ausnahme auslöst. Jedes Objekt in Java kann als Sperre verwendet werden. Wo ist also die Sperre? Die Sperre wird im Java-Objekt-Header gespeichert. Wenn das Objekt ein Array-Typ ist, verwendet die virtuelle Maschine 3 Wörter (Wortbreite), um den Objekt-Header zu speichern. Wenn das Objekt ein Nicht-Array-Typ ist, verwendet die virtuelle Maschine 2 Wörter (Wortbreite), um den Objektheader zu speichern. Für weitere Informationen zur Synchronisierung klicken Sie bitte auf „Synchronisiert in Java SE1.6“.
Thread-Pool
Der Thread-Pool ist für die Verwaltung von Arbeitsthreads verantwortlich und enthält eine Warteschlange von Aufgaben, die auf die Ausführung warten. Die Aufgabenwarteschlange des Thread-Pools ist eine Sammlung von Runnables, und der Arbeitsthread ist für die Entnahme und Ausführung von Runnable-Objekten aus der Aufgabenwarteschlange verantwortlich.
ExecutorService executor = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { executor.execute(new MyThread2()); }executor.shutdown();
Java stellt 4 Arten von Thread-Pools über Executors bereit:
newCachedThreadPool: Erstellen Sie einen zwischenspeicherbaren Thread-Pool. Wenn für neue Aufgaben kein inaktiver Thread vorhanden ist, wird ein neuer Thread erstellt . Wenn ein Thread im Leerlauf vorhanden ist, wird er nach einer bestimmten Zeit recycelt.
newFixedThreadPool: Erstellen Sie einen Thread-Pool mit einer festen Anzahl von Threads.
newSingleThreadExecutor: Erstellen Sie einen Single-Threaded-Thread-Pool, der nur einen Thread zum Ausführen von Aufgaben verwendet, um sicherzustellen, dass alle Aufgaben in FIFO-Reihenfolge ausgeführt werden.
newScheduledThreadPool: Erstellen Sie einen Thread-Pool mit fester Länge, um die geplante und periodische Aufgabenausführung zu unterstützen.
Die unteren Ebenen der oben genannten Thread-Pools rufen alle ThreadPoolExecutor auf, um Thread-Pools zu erstellen.
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。,可以选择的阻塞队列有以下几种:
workQueue(任务队列):用于保存等待执行的任务的阻塞队列。
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
当提交新任务到线程池时,其处理流程如下:
1.先判断基本线程池是否已满?没满则创建一个工作线程来执行任务,满了则进入下个流程。
2.其次判断工作队列是否已满?没满则提交新任务到工作队列中,满了则进入下个流程。
3.最后判断整个线程池是否已满?没满则创建一个新的工作线程来执行任务,满了则交给饱和策略来处理这个任务。