Lernen Sie jeden Tag ein bisschen Multithreading

Multithreading

1. Verwandte Konzepte

Nebenläufigkeit und Parallelität

Parallel (parallel): Bezieht sich auf mehrere gleichzeitig (gleichzeitig) auftretende Ereignisaufgaben .
Parallelität : Bezieht sich auf zwei oder mehr Ereignisse, die innerhalb derselben winzigen Zeitspanne auftreten .Die gleichzeitige Ausführung von Programmen kann CPU-Ressourcen unter eingeschränkten Bedingungen voll ausnutzen.
Bildbeschreibung hier einfügen
Single-Core-CPU: nur gleichzeitig
Multi-Core-CPU: parallel + gleichzeitig

Threads und Prozesse

  • Programm : Um eine bestimmte Aufgabe und Funktion auszuführen, wählen Sie eine Reihe von Anweisungen aus, die in einer Programmiersprache geschrieben sind.

  • Software : 1 oder mehrere Anwendungen + zugehörige Materialien und Ressourcendateien bilden ein Softwaresystem.

  • Ein Prozess stellt eine Beschreibung des laufenden Prozesses (Creation-Run-Death) eines Programms dar. Das System erstellt für jedes laufende Programm einen Prozess und weist dem Prozess unabhängige Systemressourcen, wie beispielsweise Speicherplatz, zu.

  • Thread : Ein Thread ist eine Ausführungseinheit in einem Prozess, die für die Ausführung der Aufgabe des Ausführens des aktuellen Programms verantwortlich ist.In einem Prozess gibt es mindestens einen Thread. In einem Prozess kann es mehrere Threads geben, und dieses Anwendungsprogramm kann zu diesem Zeitpunkt auch als Multithread-Programm bezeichnet werden. Multi-Threading ermöglicht die gleichzeitige Ausführung von Programmen und die volle Nutzung der CPU-Ressourcen.

    Interviewfrage : Ein Prozess ist die kleinste Einheit der Betriebssystemplanung und Ressourcenzuweisung, und ein Thread ist die kleinste Einheit der CPU-Planung. Verschiedene Prozesse teilen sich keinen Speicher. Die Kosten für den Datenaustausch und die Kommunikation zwischen Prozessen sind hoch. Verschiedene Threads teilen sich den Speicher desselben Prozesses. Natürlich haben verschiedene Threads auch ihren eigenen unabhängigen Speicherplatz. Für den Methodenbereich kann der Speicher desselben Objekts im Heap zwischen Threads geteilt werden, aber die lokalen Variablen des Stacks sind immer unabhängig.

Vorteile und Anwendungsszenarien von Multithreading

  • Der Hauptvorteil:
    • Nutzen Sie CPU-Leerlaufzeiten voll aus, um Benutzeranfragen in kürzester Zeit zu erledigen. Damit das Programm schneller reagiert.
  • Anwendungsszenario:
    • Multitasking. Wenn mehrere Benutzer den Server anfordern, kann das Serverprogramm mehrere Threads öffnen, um die Anforderung jedes Benutzers separat zu verarbeiten, ohne sich gegenseitig zu beeinflussen.
    • Einzelne große Aufgabenverarbeitung. Um eine große Datei herunterzuladen, können Sie mehrere Threads öffnen, um sie zusammen herunterzuladen, wodurch die Downloadzeit insgesamt verkürzt wird.

Thread-Planung

Bezieht sich darauf, wie CPU-Ressourcen verschiedenen Threads zugewiesen werden. Zwei gängige Thread-Scheduling-Methoden:

  • Time-Sharing-Planung

    Alle Threads verwenden abwechselnd die CPU, und jeder Thread beansprucht gleichmäßig CPU-Zeit.

  • vorbeugende Planung

    Geben Sie Threads mit hoher Priorität Priorität, um die CPU zu verwenden. Wenn die Threads die gleiche Priorität haben, wird einer zufällig ausgewählt (Thread Randomness). Java verwendet eine präemptive Scheduling-Methode .

2. Threaderstellung und Start

Erben Sie die Thread-Klasse

Schritte zum Erstellen und Starten von Multithreading durch Erben der Thread-Klasse:

  1. Definieren Sie die Unterklasse der Thread-Klasse und schreiben Sie die run()-Methode dieser Klasse neu. Der Methodenkörper der run()-Methode stellt die Aufgabe dar, die der Thread ausführen muss, daher wird die run()-Methode als Thread-Ausführungskörper bezeichnet .
  2. Erstellen Sie eine Instanz der Thread-Unterklasse, dh erstellen Sie ein Thread-Objekt
  3. Rufen Sie die start()-Methode des Thread-Objekts auf, um den Thread zu starten

Hinweise zur Multithread-Ausführungsanalyse :
Bildbeschreibung hier einfügen

  • Das manuelle Aufrufen der run-Methode ist nicht die Art, einen Thread zu starten, es ist nur ein normaler Methodenaufruf.

  • Nachdem die start-Methode den Thread gestartet hat, wird die run-Methode von der JVM aufgerufen und ausgeführt.

  • Starten Sie denselben Thread nicht wiederholt, da sonst eine Ausnahme ausgelöst wirdIllegalThreadStateException

  • Verwenden Sie die Junit-Unit nicht zum Testen von Multi-Threading, es wird nicht unterstützt, nachdem der Haupt-Thread endet, wird aufgerufen, um System.exit()die JVM direkt zu beenden;

Implementieren Sie die Runnable-Schnittstelle

  1. Definieren Sie die Implementierungsklasse der Runnable-Schnittstelle und schreiben Sie die run()-Methode der Schnittstelle neu. Der Methodenrumpf der run()-Methode ist auch der Thread-Ausführungsrumpf des Threads.
  2. Erstellen Sie eine Instanz der Runnable-Implementierungsklasse und verwenden Sie diese Instanz als Ziel von Thread, um ein Thread-Objekt zu erstellen, das das eigentliche Thread-Objekt ist.
  3. Rufen Sie die start()-Methode des Thread-Objekts auf, um den Thread zu starten.

Vergleich zweier Möglichkeiten, Threads zu erstellen

  • Die Thread-Klasse implementiert auch das Runnable-Interface selbst. Die Run-Methode kommt von dem Runnable-Interface, und die Run-Methode ist auch die eigentliche Thread-Aufgabe, die ausgeführt werden soll.

    public class Thread implements Runnable {
          
          }
    
  • Da Java-Klassen einfach vererbt werden, hat die Art der Vererbung von Thread die Einschränkung der einfachen Vererbung, ist aber einfacher zu verwenden.

  • Die Methode zur Implementierung der Runnable-Schnittstelle vermeidet die Einschränkung der Einzelvererbung und kann mehrere Thread-Objekte erstellenGeben Sie ein Objekt der ausführbaren Implementierungsklasse (Thread-Task-Klasse) frei, um die Ausführung von Multithread-Aufgaben zu erleichternDaten teilen.

Anonymer Thread zur Objekterstellung innerhalb der Klasse

Das Erstellen von Threads mithilfe anonymer innerer Klassenobjekte ist keine neue Art, Threads zu erstellen, aber wenn die Thread-Aufgabe nur einmal ausgeführt werden muss, müssen wir keine Thread-Klassen separat erstellen, wir können anonyme Objekte verwenden:

new Thread("新的线程!"){
    
    
	@Override
	public void run() {
    
    
		for (int i = 0; i < 10; i++) {
    
    
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}.start();

new Thread(new Runnable(){
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println(Thread.currentThread().getName()+":" + i);
        }
    }
}).start();

3. Thread-Klasse

Konstruktionsmethode

  • public Thread() : Weist ein neues Thread-Objekt zu.
  • public Thread(String name): Weisen Sie ein neues Thread-Objekt mit einem bestimmten Namen zu.
  • public Thread(Runnable target) : Weist ein neues Thread-Objekt mit dem angegebenen Ziel zu.
  • public Thread(Runnable target, String name) : Weisen Sie ein neues Thread-Objekt mit dem angegebenen Ziel zu und geben Sie den Namen an.

Threads verwenden die grundlegende Methode

  • public void run() : Die Aufgabe, die von diesem Thread ausgeführt werden soll, wird hier definiert.

  • public String getName() : Ruft den Namen des aktuellen Threads ab.

  • public static Thread currentThread() : Gibt eine Referenz auf das aktuell ausgeführte Thread-Objekt zurück.

  • public final boolean isAlive(): Testet, ob der Thread aktiv ist. Aktiv, wenn der Thread gestartet und noch nicht beendet wurde.

  • public final int getPriority() : Gibt die Thread-Priorität zurück

  • public final void setPriority(int newPriority): ändert die Priorität des Threads

    • Jeder Thread hat eine bestimmte Priorität, und Threads mit höherer Priorität erhalten mehr Ausführungsmöglichkeiten. Standardmäßig hat jeder Thread dieselbe Priorität wie der übergeordnete Thread, der ihn erstellt hat. Die Klasse Thread stellt die Methodenklassen setPriority(int newPriority) und getPriority() bereit, um die Priorität des Threads festzulegen und abzurufen, wobei die Methode setPriority eine Ganzzahl erfordert und der Bereich zwischen [1,10] liegt Legen Sie die drei Prioritäten der Klassenkonstanten der Thread-Klasse fest:
    • MAX_PRIORITY (10): höchste Priorität
    • MIN_PRIORITY (1): niedrigste Priorität
    • NORM_PRIORITY (5): normale Priorität, standardmäßig hat der Haupt-Thread normale Priorität.

Gängige Methoden der Thread-Steuerung

  • public void start() : Bewirkt, dass dieser Thread mit der Ausführung beginnt; die Java Virtual Machine ruft die run-Methode dieses Threads auf.

  • public static void sleep(long millis): Thread-Sleep, wodurch der aktuell ausgeführte Thread für die angegebene Anzahl von Millisekunden angehalten (die Ausführung vorübergehend gestoppt) wird.

  • public static void yield(): Thread-Höflichkeit, yield lässt den aktuellen Thread nur vorübergehend das Ausführungsrecht verlieren, lässt den Thread-Scheduler des Systems neu planen, in der Hoffnung, dass andere Threads mit derselben oder einer höheren Priorität wie der aktuelle Thread die Chance zur Ausführung erhalten , aber dies Es gibt keine Garantie.Es ist durchaus möglich, dass der Thread-Scheduler eine erneute Ausführung plant, wenn ein Thread die yield-Methode zum Anhalten aufruft.

  • void join() : Tritt einem Thread bei, füge einen neuen Thread zum aktuellen Thread hinzu, warte, bis der verbundene Thread beendet ist, bevor du mit der Ausführung des aktuellen Threads fortfährst.

    void join(long millis): wartet bis zu millis Millisekunden auf die Beendigung dieses Threads. Wenn die Millis-Zeit abgelaufen ist, gibt es kein Warten mehr.

    void join(long millis, int nanos) : Warten Sie bis zu millis Millis + Nanos Nanosekunden, bis der Thread beendet ist.

  • public final void stop(): Zwingt den Thread, die Ausführung zu stoppen. Diese Methode ist unsicher, veraltet und sollte nicht verwendet werden.

    • Das Aufrufen der stop()-Methode stoppt sofort die gesamte verbleibende Arbeit in der run()-Methode, einschließlich der in der catch- oder finally-Anweisung, und löst eine ThreadDeath-Ausnahme aus (normalerweise erfordert diese Ausnahme keine explizite Erfassung), sodass sie einige verursachen kann Die Reinigungsarbeiten können nicht abgeschlossen werden, wie das Schließen von Dateien, Datenbanken usw.
    • Der Aufruf der Methode stop() gibt sofort alle vom Thread gehaltenen Sperren frei, was zu nicht synchronisierten Daten und Dateninkonsistenz führt.
  • public void interrupt(): Das Unterbrechen des Threads markiert den Thread tatsächlich als Interrupt und stoppt nicht wirklich die Ausführung des Threads.

  • public static boolean interrupt(): Überprüfen Sie den Unterbrechungsstatus des Threads, das Aufrufen dieser Methode löscht den Unterbrechungsstatus (Flag).

  • public boolean isInterrupted(): Überprüfen Sie den Unterbrechungsstatus des Threads, wird den Unterbrechungsstatus nicht löschen (Flag)

  • public void setDaemon(boolean on): Legt den Thread als Daemon-Thread fest. Es muss gesetzt werden, bevor der Thread startet, andernfalls IllegalThreadStateExceptionwird eine Ausnahme gemeldet.

    • Der Daemon-Thread bedient hauptsächlich andere Threads.Wenn kein Nicht-Daemon-Thread im Programm ausgeführt wird, beendet der Daemon-Threadauch die Ausführung. Der JVM-Garbage Collector ist ebenfalls ein Daemon-Thread.
  • public boolean isDaemon(): Prüft, ob der aktuelle Thread ein Daemon-Thread ist.

  • Die Rolle von volatile besteht darin, sicherzustellen, dass einige Anweisungen aufgrund der Compiler-Optimierung nicht ausgelassen werden. Die volatile Variable bedeutet, dass die Variable unerwartet geändert werden kann. Lesen Sie den Wert dieser Variablen jedes Mal sorgfältig durch, anstatt eine Sicherung in der zu verwenden registrieren, damit der Compiler den Wert dieser Variablen nicht annimmt.

Thread-Lebenszyklus

Fünf Thread-Zustände des traditionellen Threading-Modells
Bildbeschreibung hier einfügen

Die sechs vom JDK definierten Thread-Zustände definieren eine Aufzählungsklasse innerhalb der Klasse, um die sechs Zustände des Threads zu beschreiben
:java.lang.Thread

    public enum State {
    
    
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

Bildbeschreibung hier einfügen
Beschreibung: Wenn beim Zurückkehren aus WAITINGoder in den Status festgestellt wird, dass der aktuelle Thread die Monitorsperre nicht erhalten hat, wechselt er sofort in den Status.TIMED_WAITINGRunnableBLOCKED

Gewindesicherheit

Wenn wir mehrere Threads verwenden, um auf dieselbe Ressource zuzugreifen (es kann dieselbe Variable, dieselbe Datei, derselbe Datensatz usw. sein), aber wenn es Lese- und Schreibvorgänge auf der Ressource in mehreren Threads gibt, kommt es zu Dateninkonsistenzen Probleme vor und nach , was ein Thread-Sicherheitsproblem ist.

Thread-Sicherheitsprobleme führen zu

  • Lokale Variablen können nicht gemeinsam genutzt werden : Lokale Variablen sind bei jedem Aufruf der Methode unabhängig, daher sind die Daten von run() jedes Threads unabhängig und keine gemeinsam genutzten Daten.
  • Instanzvariablen verschiedener Objekte werden nicht gemeinsam genutzt : Instanzvariablen verschiedener Instanzobjekte sind unabhängig.
  • statische Variablen werden geteilt
  • Instanzvariablen desselben Objekts werden gemeinsam genutzt

Zusammenfassung: Thread-Sicherheitsprobleme treten aufgrund der folgenden Bedingungen auf

  1. Multithread-Ausführung
  2. Daten teilen
  3. Mehrere Anweisungen arbeiten mit gemeinsam genutzten Daten

Lösung für Thread-Sicherheitsproblem

Bildbeschreibung hier einfügen
Synchronisierungsmethode : Das Schlüsselwort „synced“ modifiziert die Methode direkt und gibt an, dass nur ein Thread gleichzeitig in diese Methode eintreten kann und andere Threads draußen warten.

public synchronized void method(){
    
    
    可能会产生线程安全问题的代码
}

Synchronisierter Codeblock : Das Schlüsselwort "synced" kann vor einem bestimmten Block verwendet werden, um anzuzeigen, dass sich nur die Ressourcen dieses Blocks gegenseitig ausschließen.

synchronized(同步锁){
    
    
     需要同步操作的代码
}

Objektauswahl sperren

bevorzugte diesgefolgt vonKlasse.Klassekann auch sein" "leeres Zeichenfolgenobjekt

Synchronisationssperrobjekt:

  • Sperrobjekte können von beliebigem Typ sein.
  • Mehrere Thread-Objekte verwenden dieselbe Sperre.

Das Sperrobjekt des synchronisierten Codeblocks

  • Im statischen Codeblock: Verwenden Sie das Class-Objekt der aktuellen Klasse
  • In nicht statischen Codeblöcken: Es ist üblich, dies zuerst zu berücksichtigen, aber achten Sie darauf, ob dies gleich ist

Der Geltungsbereich der Sperre ist zu klein: Sie kann keine Sicherheitsprobleme lösen, und alle Anweisungen, die auf gemeinsam genutzten Ressourcen arbeiten, müssen synchronisiert werden.

Der Geltungsbereich der Sperre ist zu groß: Sobald ein Thread die Sperre ergreift, können andere Threads nur warten, sodass der Geltungsbereich zu groß ist, die Effizienz verringert wird und die CPU-Ressourcen nicht sinnvoll genutzt werden können.

Thread-Sicherheitsprobleme des Singleton-Entwurfsmusters

1. Hungriger chinesischer Stil hat keine Thread-Sicherheitsprobleme

Hungriger chinesischer Stil: Erstellen Sie Objekte, sobald Sie auftauchen

2. Probleme mit der Sicherheit von Threads im Lazy-Stil

public class Singleton {
    
    
    private static Singleton ourInstance;

    public static Singleton getInstance() {
    
    
        //一旦创建了对象,之后再次获取对象,都不会再进入同步代码块,提升效率
        if (ourInstance == null) {
    
    
            //同步锁,锁住判断语句与创建对象并赋值的语句
            synchronized (Singleton.class) {
    
    
                if (ourInstance == null) {
    
    
                    ourInstance = new Singleton();
                }
            }
        }
        return ourInstance;
    }

    private Singleton() {
    
    
    }
}

Warten auf Weckmechanismus

Wenn ein Thread eine bestimmte Bedingung erfüllt, wechselt er in den Wartezustand ( wait() / wait(time) ), wartet darauf, dass andere Threads ihren angegebenen Code ausführen, und weckt ihn dann auf ( alert() ); oder Sie können die Wartezeit angeben time , Warten Sie, bis die Zeit automatisch aufgeweckt wird; wenn mehrere Threads warten, können Sie bei Bedarf NotifyAll() verwenden, um alle wartenden Threads aufzuwecken. Wait/Notify ist ein Koordinationsmechanismus zwischen Threads.

  1. wait: Der Thread ist nicht mehr aktiv, nimmt nicht mehr an der Planung teil und tritt in den Wartesatz ein, sodass er keine CPU-Ressourcen verschwendet und nicht um Sperren konkurriert. Zu diesem Zeitpunkt ist der Thread-Status WAITING oder TIMED_WAITING. Es wartet auch darauf, dass andere Threads eine spezielle Aktion ausführen , dh " benachrichtigen " oder wenn die Wartezeit abgelaufen ist, wird der auf dieses Objekt wartende Thread aus dem Wartesatz entlassen und tritt erneut in die Dispatch-Warteschlange (Ready-Warteschlange) ein. Mitte
  2. benachrichtigen: Auswählen eines Threads in der Wartegruppe des benachrichtigten Objekts zum Freigeben;
  3. NotifyAll: Gibt alle Threads auf dem Wartesatz des benachrichtigten Objekts frei.

Hinweis:
Nachdem der benachrichtigte Thread aufgeweckt wurde, kann er die Ausführung möglicherweise nicht sofort fortsetzen, da die Stelle, an der er unterbrochen wurde, im Synchronisationsblock war und in diesem Moment die Sperre nicht mehr hält, also muss er es versuchen die Sperre erneut erwerben (wahrscheinlich gegenüber einer anderen Thread-Konkurrenz), erst nach Erfolg kann die Ausführung an der Stelle fortgesetzt werden, an der die Wait-Methode ursprünglich aufgerufen wurde.

Zusammengefasst wie folgt:

  • Wenn die Sperre erworben werden kann, wechselt der Thread vom Zustand WAITING in den Zustand RUNNABLE (lauffähig);
  • Andernfalls wechselt der Thread vom Zustand WAITING in den Zustand BLOCKED (Warten auf Sperre).

Die Details, die beim Aufruf der Wait- und Notification-Methoden zu beachten sind

  1. Die Wait-Methode und die Notify-Methode müssen von demselben Sperrobjekt aufgerufen werden. Denn: das entsprechende Lock-Objekt kann den Thread nach der Wait-Methode aufwecken, die mit dem gleichen Lock-Objekt per Notify aufgerufen wurde.
  2. Die Wait-Methode und die Notify-Methode gehören zu den Methoden der Object-Klasse. Denn: Das Sperrobjekt kann ein beliebiges Objekt sein, und die Klasse eines beliebigen Objekts erbt die Objektklasse.
  3. Die Wartemethode und die Benachrichtigungsmethode müssen in einem Synchronisierungscodeblock oder einer Synchronisierungsfunktion verwendet werden, und diese beiden Methoden müssen über das Sperrobjekt aufgerufen werden.

Lock-Operation und Deadlock freigeben

1. Der Vorgang des Freigebens der Sperre

  • Die Ausführung des Synchronisationsverfahrens und des Synchronisationscodeblocks des aktuellen Threads endet.

  • Im Synchronisierungscodeblock oder in der Synchronisierungsmethode des aktuellen Threads tritt ein nicht behandelter Fehler oder eine Ausnahme auf, wodurch der aktuelle Thread abnormal beendet wird.

  • Der aktuelle Thread führt die wait()-Methode des Sperrobjekts im Synchronisationscodeblock und der Synchronisationsmethode aus, der aktuelle Thread wird angehalten und die Sperre freigegeben.

2. Sperre

Verschiedene Threads sperren das von der anderen Partei benötigte Synchronisationsüberwachungsobjekt und geben es nicht frei.Wenn sie darauf warten, dass die andere Partei zuerst aufgibt, wird ein Thread-Deadlock gebildet. Sobald ein Deadlock auftritt, ist das gesamte Programm weder abnormal noch gibt es irgendwelche Eingabeaufforderungen, aber alle Threads werden blockiert und können nicht fortgesetzt werden.

3. Interviewfrage: Der Unterschied zwischen den Methoden sleep() und wait()

(1) sleep() gibt die Sperre nicht frei, wait() gibt die Sperre frei

(2) sleep() gibt die Ruhezeit an, wait() kann die Zeit angeben oder unbegrenzt warten, bis Benachrichtigung oder NotifyAll

(3) sleep() ist eine statische Methode, die in der Thread-Klasse deklariert ist, und die wait-Methode ist in der Object-Klasse deklariert

Da wir die Methode wait() aufrufen, wird sie vom Sperrobjekt aufgerufen, und der Typ des Sperrobjekts ist ein beliebiger Objekttyp. Dann können die Methoden, die jeder Objekttyp haben soll, nur in der Klasse Object deklariert werden.

üben

Zum gleichzeitigen Drucken von Buchstaben sind zwei Threads erforderlich, und jeder Thread kann 3 Buchstaben fortlaufend drucken. Zwei Threads drucken abwechselnd, ein Thread druckt die Kleinbuchstabenform des Buchstabens und ein Thread druckt die Großbuchstabenform des Buchstabens, aber die Buchstaben sind fortlaufend. Nachdem der Buchstabe zu z geschlungen ist, gehen Sie zurück zu a.

Bildbeschreibung hier einfügen

public class PrintLetterDemo {
    
    
	public static void main(String[] args) {
    
    
		// 2、创建资源对象
		PrintLetter p = new PrintLetter();

		// 3、创建两个线程打印
		new Thread("小写字母") {
    
    
			public void run() {
    
    
				while (true) {
    
    
					p.printLower();
					try {
    
    
						Thread.sleep(1000);// 控制节奏
					} catch (InterruptedException e) {
    
    
						e.printStackTrace();
					}
				}
			}
		}.start();

		new Thread("大写字母") {
    
    
			public void run() {
    
    
				while (true) {
    
    
					p.printUpper();
					try {
    
    
						Thread.sleep(1000);// 控制节奏
					} catch (InterruptedException e) {
    
    
						e.printStackTrace();
					}
				}
			}
		}.start();
	}
}

// 1、定义资源类
class PrintLetter {
    
    
	private char letter = 'a';

	public synchronized void printLower() {
    
    
		for (int i = 1; i <= 3; i++) {
    
    
			System.out.println(Thread.currentThread().getName() + "->" + letter);
			letter++;
			if (letter > 'z') {
    
    
				letter = 'a';
			}
		}
		this.notify();
		try {
    
    
			this.wait();
		} catch (InterruptedException e) {
    
    
			e.printStackTrace();
		}
	}

	public synchronized void printUpper() {
    
    
		for (int i = 1; i <= 3; i++) {
    
    
			System.out.println(Thread.currentThread().getName() + "->" + (char) (letter - 32));
			letter++;
			if (letter > 'z') {
    
    
				letter = 'a';
			}
		}
		this.notify();
		try {
    
    
			this.wait();
		} catch (InterruptedException e) {
    
    
			e.printStackTrace();
		}
	}
} 

Ich denke du magst

Origin blog.csdn.net/qq_52370789/article/details/129367778
Empfohlen
Rangfolge