Java 101: Razumijevanje Java niti, 1. dio: Upoznavanje niti i pokretačkih programa

Ovaj je članak prvi u četverodijelnoj seriji Java 101 koja istražuje Java niti. Iako možda mislite da bi navoja na nitima u Javi bilo izazovno shvatiti, namjeravam vam pokazati da su niti lako razumljive. U ovom članku upoznajem vas s Java nitima i pokretačkim programima. U sljedećim člancima istražit ćemo sinkronizaciju (putem zaključavanja), probleme sa sinkronizacijom (poput zastoja), mehanizam čekanja / obavještavanja, raspoređivanje (s i bez prioriteta), prekidanje niti, tajmere, volatilnost, skupine niti i lokalne varijable niti .

Imajte na umu da je ovaj članak (dio arhiva JavaWorld) ažuriran novim popisima kodova i izvornim kodom za preuzimanje u svibnju 2013.

Razumijevanje Java niti - pročitajte cijelu seriju

  • Dio 1: Upoznavanje niti i pokretačkih programa
  • Dio 2: Sinkronizacija
  • Dio 3: Zakazivanje niti i pričekajte / obavijestite
  • Dio 4: Skupine niti i volatilnost

Što je nit?

Konceptualno, pojam niti nije teško shvatiti: to je neovisan put izvršenja kroz programski kod. Kada se izvrši više niti, put jedne niti kroz isti kod obično se razlikuje od ostalih. Na primjer, pretpostavimo da jedna nit izvršava bajtni kod ekvivalent dijela if-else ifdijela izraza , dok druga nit izvršava bajtni kod ekvivalentni elsedijelu. Kako JVM prati izvršenje svake niti? JVM daje svakoj niti vlastiti stog poziva. Osim praćenja trenutne upute bajt koda, stog poziva metode prati lokalne varijable, parametre koje JVM prosljeđuje metodi i povratnu vrijednost metode.

Kada više niti izvršava sekvence naredbi bajt-koda u istom programu, ta je radnja poznata kao višestruko uvođenje u niti . Multithreading koristi programu na razne načine:

  • Programi zasnovani na višenitnom GUI-u (grafičko korisničko sučelje) i dalje reagiraju na korisnike tijekom izvođenja drugih zadataka, poput ponovnog postavljanja stranice ili ispisa dokumenta.
  • Programi s navojem obično završavaju brže od svojih nenavojnih kolega. To se posebno odnosi na niti koje se izvode na višeprocesorskom stroju, gdje svaka nit ima svoj procesor.

Java ostvaruje multithreading kroz svoju java.lang.Threadklasu. Svaki Threadobjekt opisuje jednu nit izvršenja. To izvršenje se događa u Thread' run()metodi. Budući da zadana run()metoda ne čini ništa, morate izvršiti potklasu Threadi nadjačati run()da biste obavili koristan posao. Da biste okusili niti i višenitnost u kontekstu Thread, proučite Popis 1:

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

Popis 1 predstavlja izvorni kod aplikaciji koja se sastoji od klasa ThreadDemoi MyThread. Klasa ThreadDemopokreće aplikaciju stvaranjem MyThreadobjekta, pokretanjem niti koja se povezuje s tim objektom i izvršavanjem nekog koda za ispis tablice kvadrata. Nasuprot tome, MyThreadnadjačava Thread„s run()metodom za ispis (na standardnoj izlazni tok) u desni kut trokuta sastoji od zvjezdicom znakova.

Zakazivanje niti i JVM

Većina (ako ne i sve) implementacije JVM-a koriste mogućnosti uvođenja niti u osnovnu platformu. Budući da su te mogućnosti specifične za platformu, redoslijed izlaza vaših višenitnih programa može se razlikovati od redoslijeda izlaza nekoga drugog. Ta razlika proizlazi iz zakazivanja, teme koju istražujem kasnije u ovoj seriji.

Kada upišete java ThreadDemoza pokretanje aplikacije, JVM kreira početnu nit izvršenja koja izvršava main()metodu. Do izvršenja mt.start ();, početna nit govori JVM stvoriti drugu nit izvršenja koja se izvršava upute bajt koda koji obuhvaća MyThreadobjekta run()metodu. Kada se start()metoda vrati, početna nit izvršava svoju forpetlju za ispis tablice kvadrata, dok nova nit izvršava run()metodu za ispis pravokutnog trokuta.

Kako izgleda izlaz? Trči ThreadDemoda saznaš. Primijetit ćete da izlaz svake niti ima tendenciju da se presijeca s izlazom druge niti. To rezultira jer obje niti šalju svoj izlaz u isti standardni izlazni tok.

Klasa Thread

Da biste postali vještiji u pisanju višenitnog koda, prvo morate razumjeti razne metode koje čine Threadklasu. Ovaj odjeljak istražuje mnoge od tih metoda. Točnije, naučite o metodama za pokretanje niti, imenovanje niti, stavljanje niti u stanje mirovanja, utvrđivanje je li nit živa, spajanje jedne niti s drugom niti i nabrajanje svih aktivnih niti u grupi niti podskupinama trenutne niti. Također raspravljam o Threadsredstvima za otklanjanje pogrešaka i korisničkim nitima u odnosu na demonske niti.

Preostali dio Threadmetoda predstavit ću u sljedećim člancima, osim Sunčevih zastarjelih metoda.

Zastarele metode

Sun je zastario razne Threadmetode, kao što su suspend()i resume(), jer mogu zaključati vaše programe ili oštetiti predmete. Kao rezultat toga, ne biste ih trebali zvati u svom kodu. Potražite zaobilaženje tih metoda u dokumentaciji SDK-a. Ne pokrivam zastarjele metode u ovoj seriji.

Konstruiranje niti

Threadima osam konstruktora. Najjednostavniji su:

  • Thread(), koji stvara Threadobjekt sa zadanim imenom
  • Thread(String name), koji stvara Threadobjekt s imenom koje nameargument navodi

Sljedeći najjednostavniji konstruktori su Thread(Runnable target)i Thread(Runnable target, String name). Osim Runnableparametara, ti su konstruktori identični navedenim konstruktorima. Razlika: RunnableParametri identificiraju vanjske objekte Threadkoji pružaju run()metode. (Možete naučiti o Runnablekasnije u ovom članku.) Final Four graditelji nalikuju Thread(String name), Thread(Runnable target)i Thread(Runnable target, String name); međutim, konačni konstruktori također uključuju ThreadGroupargument u organizacijske svrhe.

Jedan od posljednja četiri konstruktora, Thread(ThreadGroup group, Runnable target, String name, long stackSize)zanimljiv je po tome što vam omogućuje da odredite željenu veličinu stoga poziva-metoda niti. Mogućnost navođenja te veličine pokazuje se korisnom u programima s metodama koje koriste rekurziju - tehniku ​​izvršavanja kojom se metoda opetovano poziva - za elegantno rješavanje određenih problema. Izričitim postavljanjem veličine snopa ponekad možete spriječiti StackOverflowErrors. Međutim, prevelika veličina može rezultirati OutOfMemoryErrors. Također, Sun smatra da je veličina stoga poziva-metoda ovisna o platformi. Ovisno o platformi, veličina stoga poziva-metoda može se promijeniti. Stoga dobro razmislite o posljedicama vašeg programa prije pisanja koda koji poziva Thread(ThreadGroup group, Runnable target, String name, long stackSize).

Pokrenite svoja vozila

Niti nalikuju vozilima: premještaju programe od početka do kraja. Threada Threadobjekti potklase nisu niti. Umjesto toga, oni opisuju atribute niti, poput imena, i sadrže kôd (putem run()metode) koji nit izvršava. Kada dođe vrijeme za izvršavanje nove niti run(), druga nit poziva metodu Thread'ili njenog podrazreda start(). Na primjer, za pokretanje druge niti, početna nit aplikacije - koja se izvršava - main()poziva start(). Kao odgovor na to, JVM-ov kod za rukovanje nitima radi s platformom kako bi osigurao da se nit pravilno inicijalizira i poziva metodu Threadobjekta ili njegove podklase run().

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.

Da sleep(long millis)bih demonstrirao , napisao sam CalcPI1prijavu. Ta aplikacija pokreće novu nit koja koristi matematički algoritam za izračunavanje vrijednosti matematičke konstante pi. Dok nova nit izračunava, početna nit zaustavlja se 10 milisekundi pozivanjem sleep(long millis). Nakon što se početna nit probudi, ispisuje vrijednost pi, koju nova nit pohranjuje u varijablu pi. Popis 3 predstavlja CalcPI1izvorni kod:

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

Ako pokrenete ovaj program, vidjet ćete izlaz sličan (ali vjerojatno ne identičan) sljedećem:

pi = -0.2146197014017295 Finished calculating PI