Java 101: Förstå Java-trådar, del 1: Introduktion av trådar och körbarheter

Den här artikeln är den första i en fyrdelad Java 101- serie som utforskar Java-trådar. Även om du kanske tror att trådning i Java skulle vara utmanande att förstå, har jag för avsikt att visa dig att trådar är lätta att förstå. I den här artikeln introducerar jag dig till Java-trådar och körbarheter. I efterföljande artiklar undersöker vi synkronisering (via lås), synkroniseringsproblem (som dödläge), vänta / meddelande-mekanismen, schemaläggning (med och utan prioritet), trådavbrott, timers, volatilitet, trådgrupper och trådvariabler .

Observera att den här artikeln (en del av JavaWorld-arkiven) uppdaterades med nya kodlistor och nedladdningsbar källkod i maj 2013.

Förstå Java-trådar - läs hela serien

  • Del 1: Introduktion av trådar och körbarheter
  • Del 2: Synkronisering
  • Del 3: Trådschemaläggning och vänta / meddela
  • Del 4: Trådgrupper och volatilitet

Vad är en tråd?

Begreppsmässigt är begreppet tråd inte svårt att förstå: det är en oberoende körväg genom programkod. När flera trådar körs skiljer sig en tråds väg genom samma kod vanligtvis från de andra. Anta till exempel att en tråd kör bytekodekvivalenten för en if-else-satsens ifdel, medan en annan tråd kör bytekodekvivalenten för elsedelen. Hur håller JVM reda på hur varje tråd körs? JVM ger varje tråd sin egen metod-samtalsstack. Förutom att spåra den aktuella bytekodinstruktionen spårar metod-samtalsstacken lokala variabler, parametrar som JVM skickar till en metod och metodens returvärde.

När flera trådar kör sekvenser för byte-kodinstruktioner i samma program, är den åtgärden känd som multithreading . Multithreading gynnar ett program på olika sätt:

  • Multitrådade GUI (grafiskt användargränssnitt) -baserade program förblir lyhörda för användare medan de utför andra uppgifter, till exempel ompaginering eller utskrift av ett dokument.
  • Trådade program slutar vanligtvis snabbare än deras icke-gängade motsvarigheter. Detta gäller särskilt trådar som körs på en multiprocessormaskin, där varje tråd har sin egen processor.

Java utför multithreading genom sin java.lang.Threadklass. Varje Threadobjekt beskriver en enda körningstråd. Att utförandet sker i Thread's run()metod. Eftersom standardmetoden inte run()gör något måste du underklassera Threadoch åsidosätta för run()att utföra användbart arbete. För en smak av trådar och multithreading i samband med Thread, undersök Lista 1:

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

Listning 1 presenterar källkoden för en applikation som består av klasser ThreadDemooch MyThread. Class ThreadDemodriver applikationen genom att skapa ett MyThreadobjekt, starta en tråd som associeras med det objektet och köra en kod för att skriva ut en tabell med rutor. I kontrast, MyThreadoverrides Thread's run()metod att skriva ut (på standard ut-ström) en rätvinklig triangel består av asterisk tecken.

Trådschemaläggning och JVM

De flesta (om inte alla) JVM-implementeringar använder den underliggande plattformens trådningsfunktioner. Eftersom dessa funktioner är plattformsspecifika, kan ordningen på dina multitrådade programs utdata skilja sig från ordningen på någon annans utdata. Skillnaden beror på schemaläggning, ett ämne jag utforskar senare i denna serie.

När du skriver för java ThreadDemoatt köra programmet skapar JVM en starttråd för körning som kör main()metoden. Genom att utföra mt.start ();berättar starttråden till JVM att skapa en andra körningstråd som utför byte-kodinstruktionerna som omfattar MyThreadobjektets run()metod. När start()metoden återvänder, kör starttråden sin forslinga för att skriva ut en tabell med rutor, medan den nya tråden kör run()metoden för att skriva ut den rätvinkliga triangeln.

Hur ser produktionen ut? Spring för ThreadDemoatt ta reda på det. Du kommer att märka att varje tråds utgång tenderar att spridas med den andras utdata. Det blir eftersom båda trådarna skickar utdata till samma standardutmatningsström.

Trådklassen

För att bli skicklig på att skriva flertrådad kod måste du först förstå de olika metoderna som utgör Threadklassen. Detta avsnitt utforskar många av dessa metoder. Specifikt lär du dig om metoder för att starta trådar, namnge trådar, sätta trådar i vila, bestämma om en tråd lever, koppla en tråd till en annan tråd och räkna upp alla aktiva trådar i den aktuella trådens trådgrupp och undergrupper. Jag diskuterar också Threadfelsökningshjälpmedel och användartrådar mot daemontrådar.

Jag kommer att presentera resten av Threadmetoderna i efterföljande artiklar, med undantag för Suns föråldrade metoder.

Föråldrade metoder

Sun har avskaffat en mängd olika Threadmetoder, till exempel suspend()och resume()eftersom de kan låsa dina program eller skada föremål. Som ett resultat bör du inte ringa dem i din kod. Se SDK-dokumentationen för lösningar på dessa metoder. Jag täcker inte föråldrade metoder i den här serien.

Konstruktionstrådar

Threadhar åtta konstruktörer. De enklaste är:

  • Thread(), som skapar ett Threadobjekt med ett standardnamn
  • Thread(String name), som skapar ett Threadobjekt med ett namn som nameargumentet anger

Nästa enklaste konstruktörer är Thread(Runnable target)och Thread(Runnable target, String name). Bortsett från Runnableparametrarna är dessa konstruktörer identiska med de ovannämnda konstruktörerna. Skillnaden: RunnableParametrarna identifierar objekt utanför Threadsom ger run()metoderna. (Du lär dig om Runnablesenare i denna artikel.) De sista fyra konstruktörer likna Thread(String name), Thread(Runnable target)och Thread(Runnable target, String name); emellertid innehåller de slutliga konstruktörerna också ett ThreadGroupargument för organisatoriska ändamål.

En av de fyra sista konstruktörerna Thread(ThreadGroup group, Runnable target, String name, long stackSize)är intressant genom att den låter dig ange önskad storlek på trådens metod-samtalsstack. Att kunna specificera att storleken visar sig vara till hjälp i program med metoder som använder rekursion - en exekveringsteknik där en metod upprepade gånger kallar sig själv - för att elegant lösa vissa problem. Genom att uttryckligen ställa in stapelstorleken kan du ibland förhindra StackOverflowErrors. En för stor storlek kan dock resultera i OutOfMemoryErrors. Sun betraktar också metod-samtalsstackens storlek som plattformsberoende. Beroende på plattform kan metod-samtalsstackens storlek ändras. Tänk därför noga på konsekvenserna av ditt program innan du skriver kod som ringer Thread(ThreadGroup group, Runnable target, String name, long stackSize).

Starta dina fordon

Trådar liknar fordon: de flyttar program från början till slut. Threadoch Threadunderklassobjekt är inte trådar. Istället beskriver de en tråds attribut, till exempel dess namn, och innehåller kod (via en run()metod) som tråden kör. När det är dags för en ny tråd att köra run()kallar en annan tråd Threadsin eller dess underklassobjekt start()metod. Till exempel, för att starta en andra tråd, main()ringer programmets starttråd - som körs - start(). Som svar fungerar JVM: s trådhanteringskod med plattformen för att säkerställa att tråden initialiseras ordentligt och anropar en Threadeller dess underklassobjekts run()metod.

Once start() completes, multiple threads execute. Because we tend to think in a linear fashion, we often find it difficult to understand the concurrent (simultaneous) activity that occurs when two or more threads are running. Therefore, you should examine a chart that shows where a thread is executing (its position) versus time. The figure below presents such a chart.

The chart shows several significant time periods:

  • The starting thread's initialization
  • The moment that thread begins to execute main()
  • The moment that thread begins to execute start()
  • The moment start() creates a new thread and returns to main()
  • The new thread's initialization
  • The moment the new thread begins to execute run()
  • The different moments each thread terminates

Note that the new thread's initialization, its execution of run(), and its termination happen simultaneously with the starting thread's execution. Also note that after a thread calls start(), subsequent calls to that method before the run() method exits cause start() to throw a java.lang.IllegalThreadStateException object.

What's in a name?

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.

För att demonstrera sleep(long millis)har jag skrivit en CalcPI1ansökan. Den applikationen startar en ny tråd som använder en matematisk algoritm för att beräkna värdet på den matematiska konstanten pi. Medan den nya tråden beräknas pausar starttråden i 10 millisekunder genom att ringa sleep(long millis). Efter att starttråden har vaknat skrivs ut pi-värdet, som den nya tråden lagrar i variabel pi. Listning 3 presenterar CalcPI1källkod:

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

Om du kör det här programmet ser du utdata som liknar (men förmodligen inte är identiska) med följande:

pi = -0.2146197014017295 Finished calculating PI