Java 101: Java-threads begrijpen, deel 1: Introductie van threads en uitvoerbare bestanden

Dit artikel is het eerste in een vierdelige Java 101- serie waarin Java-threads worden onderzocht. Hoewel je misschien denkt dat threading in Java een uitdaging zou zijn om te begrijpen, wil ik je laten zien dat threads gemakkelijk te begrijpen zijn. In dit artikel laat ik je kennismaken met Java-threads en runnables. In volgende artikelen zullen we synchronisatie (via vergrendelingen), synchronisatieproblemen (zoals deadlock), het wacht- / meldingsmechanisme, planning (met en zonder prioriteit), threadonderbreking, timers, vluchtigheid, threadgroepen en lokale variabelen van threads onderzoeken. .

Merk op dat dit artikel (onderdeel van de JavaWorld-archieven) in mei 2013 is bijgewerkt met nieuwe codelijsten en downloadbare broncode.

Java-threads begrijpen - lees de hele serie

  • Deel 1: Introductie van threads en runnables
  • Deel 2: synchronisatie
  • Deel 3: Discussie plannen en wachten / melden
  • Deel 4: Discussiegroepen en vluchtigheid

Wat is een thread?

Conceptueel gezien is de notie van een thread niet moeilijk te vatten: het is een onafhankelijk uitvoeringspad via programmacode. Wanneer meerdere threads worden uitgevoerd, verschilt het pad van de ene thread door dezelfde code meestal van de andere. Stel dat een thread het bytecode-equivalent van het ifdeel van een if-else-instructie uitvoert, terwijl een andere thread het bytecode-equivalent van het elseonderdeel uitvoert . Hoe houdt de JVM de uitvoering van elke thread bij? De JVM geeft elke thread zijn eigen methodeaanroepstack. Naast het volgen van de huidige bytecode-instructie, volgt de method-call-stack lokale variabelen, parameters die de JVM doorgeeft aan een methode en de retourwaarde van de methode.

Wanneer meerdere threads bytecode-instructiereeksen uitvoeren in hetzelfde programma, wordt die actie multithreading genoemd . Multithreading biedt op verschillende manieren voordelen voor een programma:

  • Op multithreaded GUI (grafische gebruikersinterface) gebaseerde programma's blijven reageren op gebruikers tijdens het uitvoeren van andere taken, zoals het opnieuw pagineren of afdrukken van een document.
  • Programma's met schroefdraad eindigen doorgaans sneller dan hun tegenhangers zonder schroefdraad. Dit geldt met name voor threads die worden uitgevoerd op een machine met meerdere processors, waarbij elke thread zijn eigen processor heeft.

Java bereikt multithreading via zijn java.lang.Threadklasse. Elk Threadobject beschrijft een enkele uitvoeringsdraad. Die uitvoering vindt plaats in Threadde run()methode van. Omdat de standaardmethode run()niets doet, moet u een subklasse maken Threaden overschrijven run()om nuttig werk te kunnen doen. Voor een voorproefje van threads en multithreading in de context van Thread, bekijk Listing 1:

Lijst 1. ThreadDemo.java

// ThreadDemo.java class ThreadDemo { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); for (int i = 0; i < 50; i++) System.out.println ("i = " + i + ", i * i = " + i * i); } } class MyThread extends Thread { public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print ('*'); System.out.print ('\n'); } } }

Listing 1 presenteert broncode aan een applicatie die bestaat uit klassen ThreadDemoen MyThread. Klasse ThreadDemodrijft de toepassing aan door een MyThreadobject te maken, een thread te starten die aan dat object is gekoppeld, en wat code uit te voeren om een ​​tabel met vierkanten af ​​te drukken. Daarentegen MyThreadoverschrijft Threadde run()methode de methode om (op de standaard uitvoerstroom) een rechthoekige driehoek met asterisk-tekens af te drukken.

Thread-planning en de JVM

De meeste (zo niet alle) JVM-implementaties gebruiken de threading-mogelijkheden van het onderliggende platform. Omdat deze mogelijkheden platformspecifiek zijn, kan de volgorde van de uitvoer van uw multithread-programma's verschillen van de volgorde van de uitvoer van iemand anders. Dat verschil is het gevolg van planning, een onderwerp dat ik later in deze serie nader.

Wanneer u typt java ThreadDemoom de toepassing uit te voeren, maakt de JVM een startthread van uitvoering, die de main()methode uitvoert . Door uit te voeren mt.start ();, vertelt de startthread de JVM om een ​​tweede uitvoeringsdraad te maken die de bytecode-instructies uitvoert die de methode van het MyThreadobject bevatten run(). Wanneer de start()methode terugkeert, voert de startdraad zijn forlus uit om een ​​tabel met vierkanten af ​​te drukken, terwijl de nieuwe draad de run()methode uitvoert om de rechthoekige driehoek af te drukken.

Hoe ziet de uitvoer eruit? Ren ThreadDemoom erachter te komen. U zult merken dat de uitvoer van elke thread de neiging heeft om te wisselen met de uitvoer van de andere. Dat komt doordat beide threads hun uitvoer naar dezelfde standaard uitvoerstroom sturen.

De Thread-klasse

Om bekwaam te worden in het schrijven van multithreaded code, moet u eerst de verschillende methoden van de Threadklasse begrijpen . In dit gedeelte worden veel van deze methoden besproken. Specifiek leert u over methoden voor het starten van threads, het benoemen van threads, het in slaapstand brengen van threads, bepalen of een thread actief is, het verbinden van een thread met een andere thread en het opsommen van alle actieve threads in de threadgroep en subgroepen van de huidige thread. Ik bespreek ook Threadde foutopsporingshulpmiddelen en gebruikersthreads versus daemon-threads.

Ik zal de rest van Threadde methoden in volgende artikelen presenteren , met uitzondering van de verouderde methoden van Sun.

Verouderde methoden

Sun heeft verschillende Threadmethoden afgeschaft , zoals suspend()en resume(), omdat ze uw programma's kunnen vergrendelen of objecten kunnen beschadigen. Daarom moet u ze niet in uw code bellen. Raadpleeg de SDK-documentatie voor tijdelijke oplossingen voor deze methoden. Ik behandel geen verouderde methoden in deze serie.

Draden construeren

Threadheeft acht constructeurs. De eenvoudigste zijn:

  • Thread(), waarmee een Threadobject met een standaardnaam wordt gemaakt
  • Thread(String name), waarmee een Threadobject wordt gemaakt met een naam die door het nameargument wordt gespecificeerd

De volgende eenvoudigste constructeurs zijn Thread(Runnable target)en Thread(Runnable target, String name). Afgezien van de Runnableparameters zijn die constructors identiek aan de bovengenoemde constructors. Het verschil: de Runnableparameters identificeren objecten buiten Threaddie de run()methoden bieden. (Je leert Runnableverderop in dit artikel). De laatste vier constructeurs lijken Thread(String name), Thread(Runnable target)en Thread(Runnable target, String name); de uiteindelijke constructeurs bevatten echter ook een ThreadGroupargument voor organisatorische doeleinden.

Een van de laatste vier constructors Thread(ThreadGroup group, Runnable target, String name, long stackSize)is interessant omdat je hiermee de gewenste grootte van de method-call-stack van de thread kunt specificeren. In staat zijn om die grootte te specificeren, blijkt nuttig in programma's met methoden die recursie gebruiken - een uitvoeringstechniek waarbij een methode zichzelf herhaaldelijk aanroept - om bepaalde problemen op elegante wijze op te lossen. Door expliciet de stapelgrootte in te stellen, kunt u soms voorkomen dat StackOverflowErrors. Een te groot formaat kan echter resulteren in OutOfMemoryErrors. Sun beschouwt de grootte van de method-call-stack ook als platformafhankelijk. Afhankelijk van het platform kan de grootte van de method-call-stack veranderen. Denk daarom goed na over de gevolgen voor uw programma voordat u code schrijft die aanroept Thread(ThreadGroup group, Runnable target, String name, long stackSize).

Start uw voertuigen

Threads lijken op voertuigen: ze verplaatsen programma's van begin tot eind. Threaden Threadsubklasse-objecten zijn geen threads. In plaats daarvan beschrijven ze de attributen van een thread, zoals de naam, en bevatten ze code (via een run()methode) die de thread uitvoert. Wanneer het tijd is om een ​​nieuwe thread uit te voeren run(), roept een andere thread Threadde start()methode 's of zijn subklasseobject aan . Als u bijvoorbeeld een tweede thread wilt starten, main()roept de startthread van de toepassing, die wordt uitgevoerd, op start(). Als reactie hierop werkt de thread-afhandelingscode van de JVM samen met het platform om ervoor te zorgen dat de thread correct wordt geïnitialiseerd en Threadde run()methode van een of zijn subklasseobject aanroept.

Eenmaal start()voltooid, worden meerdere threads uitgevoerd. Omdat we de neiging hebben om lineair te denken, vinden we het vaak moeilijk om de gelijktijdige (gelijktijdige) activiteit te begrijpen die optreedt wanneer twee of meer threads actief zijn. Daarom moet u een grafiek bekijken die laat zien waar een thread wordt uitgevoerd (zijn positie) versus de tijd. In onderstaande figuur is zo'n grafiek weergegeven.

De grafiek toont verschillende belangrijke tijdsperioden:

  • De initialisatie van de begindraad
  • Op het moment dat die thread begint te lopen main()
  • Op het moment dat die thread begint te lopen start()
  • Het moment start()creëert een nieuwe draad en keert terug naarmain()
  • De initialisatie van de nieuwe thread
  • Op het moment dat de nieuwe thread begint te worden uitgevoerd run()
  • De verschillende momenten waarop elke thread eindigt

Merk op dat de initialisatie van de nieuwe thread, de uitvoering ervan run()en de beëindiging ervan gelijktijdig plaatsvinden met de uitvoering van de startthread. Merk ook op dat na aanroepen van een thread start(), volgende aanroepen van die methode voordat de run()methode wordt afgesloten, start()een java.lang.IllegalThreadStateExceptionobject veroorzaken.

Wat zit er in een naam?

During a debugging session, distinguishing one thread from another in a user-friendly fashion proves helpful. To differentiate among threads, Java associates a name with a thread. That name defaults to Thread, a hyphen character, and a zero-based integer number. You can accept Java's default thread names or you can choose your own. To accommodate custom names, Thread provides constructors that take name arguments and a setName(String name) method. Thread also provides a getName() method that returns the current name. Listing 2 demonstrates how to establish a custom name via the Thread(String name) constructor and retrieve the current name in the run() method by calling getName():

Listing 2. NameThatThread.java

// NameThatThread.java class NameThatThread { public static void main (String [] args) { MyThread mt; if (args.length == 0) mt = new MyThread (); else mt = new MyThread (args [0]); mt.start (); } } class MyThread extends Thread { MyThread () { // The compiler creates the byte code equivalent of super (); } MyThread (String name) { super (name); // Pass name to Thread superclass } public void run () { System.out.println ("My name is: " + getName ()); } }

You can pass an optional name argument to MyThread on the command line. For example, java NameThatThread X establishes X as the thread's name. If you fail to specify a name, you'll see the following output:

My name is: Thread-1

If you prefer, you can change the super (name); call in the MyThread (String name) constructor to a call to setName (String name)—as in setName (name);. That latter method call achieves the same objective—establishing the thread's name—as super (name);. I leave that as an exercise for you.

Naming main

Java assigns the name main to the thread that runs the main() method, the starting thread. You typically see that name in the Exception in thread "main" message that the JVM's default exception handler prints when the starting thread throws an exception object.

To sleep or not to sleep

Later in this column, I will introduce you to animation— repeatedly drawing on one surface images that slightly differ from each other to achieve a movement illusion. To accomplish animation, a thread must pause during its display of two consecutive images. Calling Thread's static sleep(long millis) method forces a thread to pause for millis milliseconds. Another thread could possibly interrupt the sleeping thread. If that happens, the sleeping thread awakes and throws an InterruptedException object from the sleep(long millis) method. As a result, code that calls sleep(long millis) must appear within a try block—or the code's method must include InterruptedException in its throws clause.

Om te demonstreren sleep(long millis), heb ik een CalcPI1aanvraag geschreven . Die applicatie start een nieuwe thread die een wiskundig algoritme gebruikt om de waarde van de wiskundige constante pi te berekenen. Terwijl de nieuwe thread berekent, pauzeert de startthread 10 milliseconden door te bellen sleep(long millis). Nadat de startthread ontwaakt, wordt de pi-waarde afgedrukt, die de nieuwe thread in variabele opslaat pi. Listing 3 presenteert CalcPI1de broncode:

Lijst 3. CalcPI1.java

// CalcPI1.java class CalcPI1 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { Thread.sleep (10); // Sleep for 10 milliseconds } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run () { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } }

Als u dit programma uitvoert, ziet u een vergelijkbare (maar waarschijnlijk niet identieke) uitvoer als de volgende:

pi = -0.2146197014017295 Finished calculating PI