Java 101: Java istodobnost bez muke, 1. dio

Sa sve složenijom istodobnom aplikacijom, mnogi programeri otkrivaju da Java-ove mogućnosti navoja na niskim razinama nisu dovoljne za njihove programske potrebe. U tom slučaju možda je vrijeme da otkrijete Java Concurrency Utilities. Započnite s java.util.concurrentdetaljnim upoznavanjem Jeffa Friesena s okvirom Executor, vrstama sinkronizatora i paketom Java Concurrent Collections.

Java 101: Sljedeća generacija

Prvi članak u ovoj novoj seriji JavaWorld predstavlja Java datum i vrijeme API-ja .

Java platforma pruža mogućnosti navoja na niskim nivoima koje programerima omogućuju pisanje istodobnih aplikacija u kojima se različite niti izvršavaju istovremeno. Međutim, standardno urezivanje Java ima nekoliko loših strana:

  • Java je niske razine konkurentnosti primitivci ( synchronized, volatile, wait(), notify(), a notifyAll()) nije lako pravilno koristiti. Opasnosti od navoja poput zastoja, izgladnjivanja niti i uvjeta utrke, koji su rezultat nepravilne upotrebe primitiva, također je teško otkriti i otkloniti pogreške.
  • Oslanjanje na synchronizedkoordinaciju pristupa između niti dovodi do problema s izvedbom koji utječu na skalabilnost aplikacije, što je zahtjev mnogih modernih aplikacija.
  • Osnovne mogućnosti Java-a za rezanje niti su preniske . Razvojnim programerima često trebaju konstrukcije više razine poput semafora i spremišta niti, što Java-ove mogućnosti navoja niske razine ne nude. Kao rezultat toga, programeri će izraditi vlastite konstrukcije, što je i dugotrajno i sklono pogreškama.

Okvir JSR 166: Concurrency Utilities osmišljen je kako bi udovoljio potrebi za uređajem za rezanje navoja na visokoj razini. Pokrenut početkom 2002. godine, okvir je formaliziran i implementiran dvije godine kasnije u Javi 5. Poboljšanja su uslijedila u Javi 6, Javi 7 i nadolazećoj Javi 8.

Ova dvodijelna Java 101: Sljedeća generacija uvodi programere koji su upoznati s osnovnim Java navojem u pakete i okvir Java Concurrency Utilities. U prvom dijelu predstavljam pregled okvira Java Concurrency Utilities i predstavljam njegov okvir Executor, uslužne programe sinkronizatora i paket Java Concurrent Collections.

Razumijevanje Java niti

Prije nego što zaronite u ovu seriju, pobrinite se da ste upoznati s osnovama navoja. Započnite s uvodom Java 101 u Java-ove mogućnosti niskog navoja:

  • Dio 1: Upoznavanje niti i pokretačkih programa
  • Dio 2: Sinkronizacija niti
  • Dio 3: Zakazivanje niti, čekanje / obavještavanje i prekidanje niti
  • Dio 4: Skupine niti, volatilnost, lokalne varijable niti, mjerači vremena i odumiranje niti

Unutar uslužnih programa Java Concurrency

Okvir Java Concurrency Utilities biblioteka je tipova koji su dizajnirani da se koriste kao gradivni blokovi za stvaranje istodobnih klasa ili aplikacija. Ove su vrste bez navoja, temeljito su ispitane i nude visoke performanse.

Tipovi u uslužnim programima Java Concurrency Utilities organizirani su u male okvire; naime, Executor framework, sinkronizator, istodobne kolekcije, brave, atomske varijable i Fork / Join. Dalje su organizirani u glavni paket i par potpaketa:

  • java.util.concurrent sadrži tipove uslužnih programa visoke razine koji se obično koriste u istodobnom programiranju. Primjeri uključuju semafore, barijere, spremišta niti i istodobne hash-mape.
    • Java.util.concurrent.atomic podpaketa sadrži niske razine komunalnih klase koje podršku thread-safe programiranje zaključavanje bez o pojedinačnim varijablama.
    • Java.util.concurrent.locks podpaketa sadrži niske razine komunalnih vrste za zaključavanje i čekaju uvjetima koji se razlikuju od korištenja Java je sinkronizaciju i monitore niske razine.

Okvir Java Concurrency Utilities također izlaže hardverske upute za uspoređivanje i zamjenu (CAS) na niskoj razini , čije inačice obično podržavaju moderni procesori. CAS je puno lakši od Java-ovog mehanizma sinkronizacije zaslona i koristi se za implementaciju nekih visoko skalabilnih istodobnih klasa. java.util.concurrent.locks.ReentrantLockKlasa koja se temelji na CAS-u , učinkovitija je od ekvivalentnog synchronizedprimitiva zasnovan na monitoru . ReentrantLocknudi veću kontrolu nad zaključavanjem. (U 2. dijelu objasnit ću vam više o tome kako CAS djeluje java.util.concurrent.)

System.nanoTime ()

Okvir Java Concurrency Utilities uključuje long nanoTime(), koji je član java.lang.Systemklase. Ova metoda omogućuje pristup izvoru vremena nanosekunde granulacije za mjerenje relativnog vremena.

U sljedećim odjeljcima predstavit ću tri korisne značajke Java Concurrency Utilities, prvo objasnivši zašto su toliko važne za suvremenu istodobnost, a zatim demonstrirajući kako rade na povećanju brzine, pouzdanosti, učinkovitosti i skalabilnosti istodobnih Java aplikacija.

Okvir izvršitelja

U navoju navoja, zadatak je jedinica rada. Jedan od problema s navojem na niti na niskoj razini u Javi je taj što je predaja zadataka usko povezana s politikom izvršavanja zadataka, kao što pokazuje Popis 1.

Popis 1. Server.java (Verzija 1)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; class Server { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; new Thread(r).start(); } } static void doWork(Socket s) { } }

Gornji kôd opisuje jednostavnu poslužiteljsku aplikaciju (koja doWork(Socket)ostaje kratka za prazno). Nit poslužitelja više puta poziva socket.accept()da pričeka dolazni zahtjev, a zatim pokreće nit za servisiranje ovog zahtjeva kad stigne.

Budući da ova aplikacija stvara novu nit za svaki zahtjev, ne prilagođava se dobro kad se suoči s velikim brojem zahtjeva. Na primjer, svaka stvorena nit zahtijeva memoriju, a previše niti može iscrpiti dostupnu memoriju, prisiljavajući aplikaciju da se završi.

Taj biste problem mogli riješiti promjenom politike izvršavanja zadataka. Umjesto da uvijek kreirate novu nit, mogli biste upotrijebiti spremište niti u kojem bi fiksni broj niti servisirao dolazne zadatke. Međutim, za promjenu biste morali prepisati aplikaciju.

java.util.concurrentuključuje okvir Izvršitelj, mali okvir tipova koji razdvajaju predaju zadataka od politika izvršavanja zadataka. Korištenjem okvira Executor moguće je jednostavno podesiti politiku izvršavanja zadataka programa bez potrebe za značajnim prepisivanjem koda.

Unutar okvira izvršitelja

Okvir izvršitelja temelji se na Executorsučelju, koje izvršitelja opisuje kao bilo koji objekt sposoban za izvršavanje java.lang.Runnablezadataka. Ovo sučelje deklarira sljedeću osamljenu metodu za izvršavanje Runnablezadatka:

void execute(Runnable command)

RunnableZadatak predajete predavanjem execute(Runnable). Ako izvršitelj iz bilo kojeg razloga ne može izvršiti zadatak (na primjer, ako je izvršitelj isključen), ova će metoda baciti a RejectedExecutionException.

Ključni koncept je da je predaja zadatka odvojena od politike izvršavanja zadataka , koja je opisana Executorimplementacijom. Runnable zadatak je stoga u mogućnosti izvršiti putem novu temu, udruženih konac, pozivanje konac, i tako dalje.

Note that Executor is very limited. For example, you can't shut down an executor or determine whether an asynchronous task has finished. You also can't cancel a running task. For these and other reasons, the Executor framework provides an ExecutorService interface, which extends Executor.

Five of ExecutorService's methods are especially noteworthy:

  • boolean awaitTermination(long timeout, TimeUnit unit) blocks the calling thread until all tasks have completed execution after a shutdown request, the timeout occurs, or the current thread is interrupted, whichever happens first. The maximum time to wait is specified by timeout, and this value is expressed in the unit units specified by the TimeUnit enum; for example, TimeUnit.SECONDS. This method throws java.lang.InterruptedException when the current thread is interrupted. It returns true when the executor is terminated and false when the timeout elapses before termination.
  • boolean isShutdown() returns true when the executor has been shut down.
  • void shutdown() initiates an orderly shutdown in which previously submitted tasks are executed but no new tasks are accepted.
  • Future submit(Callable task) submits a value-returning task for execution and returns a Future representing the pending results of the task.
  • Future submit(Runnable task) submits a Runnable task for execution and returns a Future representing that task.

The Future interface represents the result of an asynchronous computation. The result is known as a future because it typically will not be available until some moment in the future. You can invoke methods to cancel a task, return a task's result (waiting indefinitely or for a timeout to elapse when the task hasn't finished), and determine if a task has been cancelled or has finished.

The Callable interface is similar to the Runnable interface in that it provides a single method describing a task to execute. Unlike Runnable's void run() method, Callable's V call() throws Exception method can return a value and throw an exception.

Executor factory methods

At some point, you'll want to obtain an executor. The Executor framework supplies the Executors utility class for this purpose. Executors offers several factory methods for obtaining different kinds of executors that offer specific thread-execution policies. Here are three examples:

  • ExecutorService newCachedThreadPool() creates a thread pool that creates new threads as needed, but which reuses previously constructed threads when they're available. Threads that haven't been used for 60 seconds are terminated and removed from the cache. This thread pool typically improves the performance of programs that execute many short-lived asynchronous tasks.
  • ExecutorService newSingleThreadExecutor() creates an executor that uses a single worker thread operating off an unbounded queue -- tasks are added to the queue and execute sequentially (no more than one task is active at any one time). If this thread terminates through failure during execution before shutdown of the executor, a new thread will be created to take its place when subsequent tasks need to be executed.
  • ExecutorService newFixedThreadPool(int nThreads) creates a thread pool that re-uses a fixed number of threads operating off a shared unbounded queue. At most nThreads threads are actively processing tasks. If additional tasks are submitted when all threads are active, they wait in the queue until a thread is available. If any thread terminates through failure during execution before shutdown, a new thread will be created to take its place when subsequent tasks need to be executed. The pool's threads exist until the executor is shut down.

The Executor framework offers additional types (such as the ScheduledExecutorService interface), but the types you are likely to work with most often are ExecutorService, Future, Callable, and Executors.

See the java.util.concurrent Javadoc to explore additional types.

Rad s okvirom izvršitelja

Otkrit ćete da je s izvršnim okvirom prilično jednostavno raditi. U popisu 2 koristio sam Executori Executorsda bih zamijenio primjer poslužitelja s popisa 1 alternativom koja se više temelji na spremištu niti.

Popis 2. Server.java (Verzija 2)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.Executor; import java.util.concurrent.Executors; class Server { static Executor pool = Executors.newFixedThreadPool(5); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; pool.execute(r); } } static void doWork(Socket s) { } }

Popis 2 koristi se newFixedThreadPool(int)za dobivanje izvršitelja temeljenog na spremištu niti koji ponovno koristi pet niti. Također se zamjenjuje new Thread(r).start();s pool.execute(r);za izvršavanje izvršivih zadataka putem bilo koje od ovih niti.

Popis 3 predstavlja još jedan primjer u kojem aplikacija čita sadržaj proizvoljne web stranice. Izvodi rezultirajuće retke ili poruku o pogrešci ako sadržaj nije dostupan u roku od najviše pet sekundi.