Programmēšana

Java 101: Java vienlaicīgums bez sāpēm, 2. daļa

Iepriekšējais 1 2 3 4 Page 3 Nākamais 3. lapa no 4

Atomu mainīgie

Vairāku pavedienu lietojumprogrammas, kas darbojas ar daudzkodolu procesoriem vai daudzprocesoru sistēmām, var nodrošināt labu aparatūras izmantošanu un būt ļoti pielāgojamām. Viņi var sasniegt šos mērķus, liekot saviem pavedieniem pavadīt lielāko daļu laika, veicot darbu, nevis gaidot, kamēr darbs tiks paveikts, vai gaidot, lai iegūtu slēdzenes, lai piekļūtu koplietojamām datu struktūrām.

Tomēr Java tradicionālais sinhronizācijas mehānisms, kas tiek ieviests savstarpēja izslēgšana (pavedienam, kas tur slēdzeni, kas aizsargā mainīgo kopu, ir ekskluzīva piekļuve tiem) un redzamība (Apsargāto mainīgo izmaiņas kļūst redzamas citiem pavedieniem, kas vēlāk iegūst slēdzeni), tas ietekmē aparatūras izmantošanu un mērogojamību šādi:

  • Mērķtiecīga sinhronizācija (vairāki pavedieni, kas pastāvīgi konkurē par slēdzeni), ir dārgi, un tāpēc cieš caurlaidspēja. Galvenais izdevumu iemesls ir bieža konteksta maiņa; konteksta slēdža darbība var ilgt daudz procesora ciklu. Turpretī nekontrolēta sinhronizācija mūsdienu JVM ir lēts.
  • Kad pavediens, kas tur slēdzeni, tiek aizkavēts (piemēram, ieplānošanas kavēšanās dēļ), neviens pavediens, kuram nepieciešama bloķēšana, neveido progresu un aparatūra netiek izmantota tik labi, kā citādi varētu būt.

Jūs varētu domāt, ka varat izmantot gaistošs kā sinhronizācijas alternatīva. Tomēr gaistošs mainīgie tikai atrisina redzamības problēmu. Tos nevar izmantot, lai droši ieviestu atomu lasīšanas, modificēšanas un rakstīšanas sekvences, kas nepieciešamas, lai droši ieviestu skaitītājus un citas entītijas, kurām nepieciešama savstarpēja izslēgšana.

Java 5 ieviesa sinhronizācijas alternatīvu, kas piedāvā savstarpēju izslēgšanu apvienojumā ar gaistošs. Šis atomu mainīgais alternatīva ir balstīta uz mikroprocesora salīdzināšanas un mijmaiņas instrukcijām, un to galvenokārt veido tipi java.util.concurrent.atomic iepakojums.

Izpratne par salīdzināšanu un maiņu

The salīdzināt un nomainīt (CAS) instrukcija ir nepārtraukta instrukcija, kas nolasa atmiņas vietu, salīdzina nolasīto vērtību ar paredzamo vērtību un saglabā jaunu vērtību atmiņas vietā, kad nolasītā vērtība atbilst gaidāmajai vērtībai. Pretējā gadījumā nekas netiek darīts. Faktiskā mikroprocesora instrukcija var nedaudz atšķirties (piemēram, atgriezt taisnību, ja CAS ir izdevies vai citādi aplams lasītās vērtības vietā).

Mikroprocesora CAS instrukcijas

Mūsdienu mikroprocesori piedāvā kaut kādas CAS instrukcijas. Piemēram, Intel mikroprocesori piedāvā cmpxchg instrukciju saime, savukārt PowerPC mikroprocesori piedāvā saiti ar saiti (piem., lwarx) un nosacīti veikalā (piemēram, stwcx) instrukcijas tam pašam mērķim.

CAS ļauj atbalstīt atomu lasīšanas, modificēšanas un rakstīšanas secības. Parasti jūs izmantojat CAS šādi:

  1. Izlasiet v vērtību no adreses X.
  2. Veiciet daudzpakāpju aprēķinu, lai iegūtu jaunu vērtību v2.
  3. Izmantojiet CAS, lai mainītu X vērtību no v uz v2. CAS gūst panākumus, kad, veicot šīs darbības, X vērtība nav mainījusies.

Lai redzētu, kā CAS piedāvā labāku veiktspēju (un mērogojamību) sinhronizācijas laikā, apsveriet skaitītāja piemēru, kas ļauj nolasīt tā pašreizējo vērtību un palielināt skaitītāju. Šī klase ievieš skaitītāju, pamatojoties uz sinhronizēts:

4. saraksts Counter.java (1. versija)

public class Counter {private int value; public synchronized int getValue () {return value; } publiski sinhronizēts int pieaugums () {return ++ value; }}

Liela konkurence par monitora bloķēšanu radīs pārmērīgu konteksta pārslēgšanos, kas var aizkavēt visus pavedienus un radīt lietojumprogrammu, kuras mērogs nav labs.

CAS alternatīva prasa ieviest salīdzināšanas un mijmaiņas instrukciju. Šī klase līdzinās CAS. Tas izmanto sinhronizēts faktiskās aparatūras instrukcijas vietā, lai vienkāršotu kodu:

Saraksts 5. EmulatedCAS.java

publiskā klase EmulatedCAS {private int value; public synchronized int getValue () {return value; } publiski sinhronizēta int salīdzinātAndSwap (int paredzētā vērtība, int jaunā vērtība) {int lasīt vērtība = vērtība; if (lasītVērtība == paredzamāVērtība) vērtība = jaunaVērtība; atgriezties readValue; }}

Šeit, vērtība identificē atmiņas vietu, kuru var iegūt, izmantojot getValue (). Arī salīdzināt un apmainīt () ievieš CAS algoritmu.

Turpmāk izmantotie klases veidi EmulatedCAS īstenotsinhronizēts skaitītājs (izlikties, ka EmulatedCAS neprasa sinhronizēts):

Saraksts 6. Counter.java (2. versija)

public class Counter {private EmulatedCAS value = new EmulatedCAS (); public int getValue () {return value.getValue (); } public int pieaugums () {int readValue = value.getValue (); while (value.compareAndSwap (readValue, readValue + 1)! = readValue) readValue = value.getValue (); atgriešanās readValue + 1; }}

Skaitītājs iekapsulē EmulatedCAS un deklarē metodes, kā iegūt un palielināt skaitītāja vērtību, izmantojot šīs instances palīdzību. getValue () izgūst instances "pašreizējo skaitītāja vērtību" un pieaugums () droši palielina skaitītāja vērtību.

pieaugums () atkārtoti piesauc salīdzināt un mainīt () līdz readValuevērtība nemainās. Tad šo vērtību var mainīt. Ja bloķēšana nav saistīta, tiek novērsta strīds un pārmērīga konteksta maiņa. Veiktspēja uzlabojas, un kods ir pielāgojamāks.

ReentrantLock un CAS

Jūs to iepriekš uzzinājāt ReentrantLock piedāvā labāku sniegumu nekā sinhronizēts zem lielas vītnes. Lai uzlabotu veiktspēju, ReentrantLockSinhronizāciju pārvalda abstrakta apakšklase java.util.concurrent.locks.AbstractQueuedSynchronizer klasē. Savukārt šī klase izmanto dokumentus bez dokumentiem sun.misc.Drošs klase un tās salīdzinātAndSwapInt () CAS metode.

Atomu mainīgo paketes izpēte

Jums nav jāīsteno salīdzināt un apmainīt () izmantojot portatīvo Java vietējo interfeisu. Tā vietā Java 5 piedāvā šo atbalstu, izmantojot java.util.concurrent.atomic: klašu rīkkopa, ko izmanto, lai programmētu atsevišķus mainīgos bez bloķēšanas un pavedieniem.

Pēc java.util.concurrent.atomicJavadoc, šīs klases

paplašināt jēdzienu gaistošs vērtības, laukus un masīva elementus tiem, kas nodrošina arī formas atomu nosacītu atjaunināšanas darbību boolean salīdzinātAndSet (paredzamaisValue, updateValue). Šī metode (kuras argumentu tipi dažādās klasēs atšķiras) atomu veidā nosaka mainīgo updateValue ja tai šobrīd pieder paredzamā vērtība, ziņojot par panākumiem.

Šī pakete piedāvā nodarbības Būla (AtomicBoolean), vesels skaitlis (AtomicInteger), garš vesels skaitlis (AtomicGong) un atsauce (AtomicReference) veidi. Tas piedāvā arī vesela skaitļa, gara vesela skaitļa un atsauces (AtomicIntegerArray, AtomicLongArray, un AtomicReferenceArray), marķētas un apzīmogotas atsauces klases, lai atomiāli atjauninātu vērtību pāri (AtomicMarkableReference un AtomicStampedReference), un vēl.

SalīdzinātAndSet () ieviešana

Java īsteno salīdzināt un iestatīt () izmantojot visātrāko pieejamo vietējo konstrukciju (piemēram, cmpxchg vai load-link / store-conditional) vai (sliktākajā gadījumā) vērpšanas slēdzenes.

Apsveriet AtomicInteger, kas ļauj atjaunināt int vērtība atomiski. Mēs varam izmantot šo klasi, lai ieviestu skaitītāju, kas parādīts 6. sarakstā. 7. saraksts parāda līdzvērtīgu avota kodu.

7. saraksts Counter.java (3. versija)

importēt java.util.concurrent.atomic.AtomicInteger; publiskā klase Skaitītājs {private AtomicInteger value = new AtomicInteger (); public int getValue () {return value.get (); } public int pieaugums () {int readValue = value.get (); while (! value.compareAndSet (readValue, readValue + 1)) readValue = value.get (); atgriešanās readValue + 1; }}

7. saraksts ir ļoti līdzīgs 6. sarakstam, izņemot to, ka tas aizstāj EmulatedCAS ar AtomicInteger. Starp citu, jūs varat vienkāršot pieaugums () jo AtomicInteger piegādā savu int getAndIncrement () metode (un līdzīgas metodes).

Dakšas / pievienošanās ietvars

Datoru aparatūra ir ievērojami attīstījusies kopš Java debijas 1995. gadā. Tajās dienās viena procesora sistēmas dominēja skaitļošanas ainavā un Java sinhronizācijas primitīvos, piemēram, sinhronizēts un gaistošs, kā arī tās vītņu bibliotēka ( Vītne klase), piemēram, parasti bija atbilstoši.

Daudzprocesoru sistēmas kļuva lētākas, un izstrādātājiem radās nepieciešamība izveidot Java lietojumprogrammas, kas efektīvi izmantotu šo sistēmu piedāvāto aparatūras paralēlismu. Tomēr viņi drīz atklāja, ka Java zemā līmeņa vītņošanas primitīvus un bibliotēku šajā kontekstā bija ļoti grūti izmantot, un no tā izrietošie risinājumi bieži vien bija kļūdaini.

Kas ir paralēlisms?

Paralēlisms ir vienlaicīga vairāku pavedienu / uzdevumu izpilde, izmantojot vairāku procesoru un procesoru kodolu kombināciju.

Java Concurrency Utilities ietvars vienkāršo šo lietojumprogrammu izstrādi; tomēr šī ietvara piedāvātos utilītus nepiedalās tūkstošiem procesoru vai procesoru kodolu. Mūsu daudzkodolu laikmetā mums ir nepieciešams risinājums, lai panāktu precīzāku paralēlismu, vai arī mēs riskējam saglabāt procesoru dīkstāvē pat tad, ja viņiem ir daudz darba.

Profesors Dags Lea savā rakstā iepazīstināja ar šīs problēmas risinājumu, ieviešot ideju par Java balstītu dakšu / pievienošanās sistēmu. Lea apraksta ietvaru, kas atbalsta "paralēlas programmēšanas stilu, kurā problēmas tiek atrisinātas, (rekursīvi) sadalot tās apakšuzdevumos, kas tiek risināti paralēli". Fork / Join ietvars galu galā tika iekļauts Java 7.

Fork / Join ietvara pārskats

Fork / Join ietvara pamatā ir īpašs izpildītāja pakalpojums īpaša veida uzdevumu izpildei. Tas sastāv no šādiem veidiem, kas atrodas java.util.concurrent iepakojums:

  • ForkJoinPool: an ExecutorService ieviešana, kas darbojas ForkJoinTasks. ForkJoinPool nodrošina uzdevumu iesniegšanas metodes, piemēram, void execute (uzdevums ForkJoinTask), kā arī vadības un uzraudzības metodes, piemēram, int getParallelism () un ilgi getStealCount ().
  • ForkJoinTask: abstrakta bāzes klase uzdevumiem, kas darbojas a ForkJoinPool kontekstā. ForkJoinTask apraksta pavedieniem līdzīgas vienības, kurām ir daudz mazāks svars nekā parastajiem pavedieniem. Daudzus uzdevumus un apakšuzdevumus var mitināt ļoti nedaudzi faktiskie pavedieni ForkJoinPool instancē.
  • ForkJoinWorkerThread: klase, kas apraksta pavedienu, kuru pārvalda a ForkJoinPool instancē. ForkJoinWorkerThread ir atbildīgs par izpildi ForkJoinTasks.
  • Rekursīvā darbība: abstrakta klase, kas apraksta rekursīvu rezultātu ForkJoinTask.
  • RecursiveTask: abstrakta klase, kas apraksta rekursīvu rezultātu nesēju ForkJoinTask.

The ForkJoinPool izpildītāja pakalpojums ir sākumpunkts tādu uzdevumu iesniegšanai, kurus parasti raksturo Rekursīvā darbība vai RecursiveTask. Aizkulisēs uzdevums tiek sadalīts mazākos uzdevumos, kas ir dakšveida (sadalīts starp dažādiem pavedieniem izpildei) no pūla. Uzdevums gaida līdz pievienojās (tā apakšuzdevumi tiek pabeigti, lai rezultātus varētu apvienot).

ForkJoinPool pārvalda strādnieku pavedienu kopu, kur katram darba pavedienam ir sava divkāršā darba rinda (deque). Kad uzdevums dakšā izvirza jaunu apakšuzdevumu, pavediens nospiež apakšuzdevumu uz tā izpildes galvu. Kad uzdevums mēģina apvienoties ar citu uzdevumu, kas vēl nav pabeigts, pavediens izvelk citu uzdevumu no tā gala un izpilda uzdevumu. Ja pavediena deque ir tukša, tā mēģina nozagt citu uzdevumu no cita pavediena deque astes. Šis darba zagšana uzvedība maksimizē caurlaidi, vienlaikus samazinot strīdu.

Izmantojot Fork / Join sistēmu

Fork / Join bija paredzēts efektīvai izpildei sadalīt un iekarot algoritmus, kas rekursīvi sadala problēmas apakšproblēmās, līdz tās ir pietiekami vienkāršas, lai tās varētu atrisināt tieši; piemēram, apvienošanas kārtība. Šo apakšproblēmu risinājumi tiek apvienoti, lai sniegtu sākotnējās problēmas risinājumu. Katru apakšproblēmu var izpildīt neatkarīgi no cita procesora vai kodola.

Lea rakstā ir parādīts šāds pseidokods, lai aprakstītu sadalīšanas un iekarošanas uzvedību:

Rezultāts atrisināt (Problēmas problēma) {ja (problēma ir maza) tieši atrisināt problēmu cits {Sadaliet problēmu neatkarīgās daļās, dakšu jauni apakšuzdevumi, lai atrisinātu katru daļu, apvienojiet visus apakšuzdevumus, sastādiet rezultātu no subresults}}

Pseidokods uzrāda a atrisināt metode, kas tiek saukta ar dažiem problēmu atrisināt un kas atgriež a Rezultāts kas satur problēmurisinājums. Ja problēmu ir pārāk mazs, lai to atrisinātu paralēli, tas tiek atrisināts tieši. (Paralelitātes izmantošanas izmaksas nelielai problēmai pārsniedz jebkuru gūto labumu.) Pretējā gadījumā problēma tiek sadalīta apakšuzdevumos: katrs apakšuzdevums patstāvīgi koncentrējas uz daļu no problēmas.

Darbība dakša palaiž jaunu dakšas / pievienošanās apakšuzdevumu, kas tiks izpildīts paralēli citiem apakšuzdevumiem. Darbība pievienoties aizkavē pašreizējo uzdevumu, līdz apakšējā uzdevuma izpilde ir pabeigta. Kādā brīdī problēmu būs pietiekami mazs, lai to varētu izpildīt secīgi, un tā rezultāts tiks apvienots ar citiem apakšrezultātiem, lai panāktu vispārēju risinājumu, kas tiek atgriezts zvanītājam.

Javadoc par Rekursīvā darbība un RecursiveTask klases piedāvā vairākus sadalīšanas un iekarošanas algoritmu piemērus, kas ieviesti kā dakšas / pievienošanās uzdevumi. Priekš Rekursīvā darbība piemēri sakārto garo veselu skaitļu masīvu, palielina katru masīva elementu un summē katra elementa kvadrāti masīvā dubultās. RecursiveTaskvientuļajā piemērā tiek aprēķināts Fibonači numurs.

8. saraksts parāda lietojumprogrammu, kas parāda šķirošanas piemēru ne-dakša / pievienošanās, kā arī dakšas / pievienošanās kontekstā. Tas arī sniedz informāciju par laiku, lai kontrastētu šķirošanas ātrumus.