Prečo musia byť lokálne premenné použité v lambdách konečné alebo efektívne konečné?

1. Úvod

Java 8 nám dáva lambdy a podľa asociácie aj pojem efektívne konečné premenné. Zaujímalo vás niekedy, prečo musia byť lokálne premenné zachytené v lambdách konečné alebo efektívne konečné?

JLS nám dáva trochu nápovedu, keď hovorí: „Obmedzenie na efektívne koncové premenné zakazuje prístup k dynamicky sa meniacim miestnym premenným, ktorých zachytenie by pravdepodobne spôsobilo problémy so súbežnosťou.“ Čo to však znamená?

V ďalších častiach sa budeme podrobnejšie zaoberať týmto obmedzením a uvidíme, prečo ho Java zaviedla. Ukážeme príklady na demonštráciu ako ovplyvňuje jednovláknové a súbežné aplikácie, a tiež budeme odhaliť spoločný anti-vzor pre obídenie tohto obmedzenia.

2. Zachytenie Lambdas

Lambda výrazy môžu používať premenné definované vo vonkajšom rozsahu. Tieto lambdy označujeme ako odchyt lambd. Môžu zachytávať statické premenné, inštančné premenné a lokálne premenné, ale iba lokálne premenné musia byť konečné alebo skutočne konečné.

V starších verziách Java sme na to narazili, keď anonymná vnútorná trieda zachytila ​​lokálnu premennú k metóde, ktorá ju obklopila - bolo treba pridať konečné kľúčové slovo pred lokálnou premennou, aby bol prekladač spokojný.

Ako trochu syntaktického cukru teraz kompilátor dokáže rozpoznať situácie, keď zatiaľ čo konečné kľúčové slovo nie je prítomné, referencia sa vôbec nemení, čo znamená, že je efektívne konečné. Dalo by sa povedať, že premenná je skutočne konečná, ak by sa kompilátor nesťažoval, keby sme ju vyhlásili za konečnú.

3. Lokálne premenné v zajatí lambdas

Jednoducho povedané, toto sa nebude kompilovať:

Inkrementér dodávateľa (int start) {return () -> start ++; }

začať je lokálna premenná a pokúšame sa ju upraviť vo vnútri výrazu lambda.

Základným dôvodom, ktorý sa nebude kompilovať, je skutočnosť, že je to lambda zachytávajúci hodnotu začať, čo znamená vytvoriť jeho kópiu. Ak sa vynútite, aby bola premenná konečná, zabráňte tomu, aby sa zvyšoval dojem začať vo vnútri lambda mohol skutočne upraviť začať parameter metódy.

Ale prečo to robí kópiu? Všimnite si, že vraciame lambdu z našej metódy. Preto lambda nebude bežať, až po začať parameter metódy zhromažďuje odpadky. Java musí vytvoriť kópiu súboru začať aby táto lambda žila mimo tejto metódy.

3.1. Problémy so súbežnosťou

Pre zábavu si na chvíľu predstavíme tú Javu urobil umožniť miestnym premenným, aby nejako zostali spojené so svojimi zaznamenanými hodnotami.

Čo by sme tu mali robiť:

public void localVariableMultithreading () {boolean run = true; executor.execute (() -> {while (run) {// do operation}}); run = false; }

Aj keď to vyzerá nevinne, má to zákerný problém „viditeľnosti“. Pripomeňme, že každé vlákno má svoj vlastný zásobník, a ako teda zaistíme, aby naše vlákno bolo bezpečné? zatiaľ čo slučka vidí zmena na bežať premenná v druhom zásobníku? Odpoveď v iných kontextoch môže byť užitočná synchronizované bloky alebo prchavý kľúčové slovo.

Avšak pretože Java zavádza efektívne konečné obmedzenie, nemusíme sa obávať zložitosti, ako je táto.

4. Statické alebo inštančné premenné pri zachytávaní lambdas

Príklady uvedené vyššie môžu vyvolať určité otázky, ak ich porovnáme s použitím statických alebo inštančných premenných vo výraze lambda.

Náš prvý príklad môžeme zostaviť iba prevedením nášho začať premennú do premennej inštancie:

private int start = 0; Dodávateľ incrementer () {return () -> štart ++; }

Ale prečo môžeme zmeniť hodnotu začať tu?

Zjednodušene je to o tom, kde sú uložené členské premenné. Lokálne premenné sú v zásobníku, ale členské premenné sú v halde. Pretože máme do činenia s haldy pamäte, kompilátor môže zaručiť, že lambda bude mať prístup k najnovšej hodnote začať.

Náš druhý príklad môžeme opraviť tak, že urobíme to isté:

private volatile boolean run = true; public void instanceVariableMultithreading () {executor.execute (() -> {while (run) {// do operation}}); run = false; }

The bežať premenná je teraz viditeľná pre lambdu, aj keď je spustená v inom vlákne, pretože sme pridali prchavý kľúčové slovo.

Všeobecne povedané, pri zachytávaní premennej inštancie by sme si o nej mohli myslieť, že zachytávame konečnú premennú toto. Každopádne to, že sa prekladač nesťažuje, ešte neznamená, že by sme nemali robiť preventívne opatrenia, najmä v prostrediach s viacerými vláknami.

5. Nepoužívajte alternatívne riešenia

Aby niekto obišiel obmedzenie miestnych premenných, niekoho môže napadnúť použitie držiteľov premenných na úpravu hodnoty miestnej premennej.

Pozrime sa na príklad, ktorý používa pole na uloženie premennej v aplikácii s jedným vláknom:

public int workaroundSingleThread () {int [] držiteľ = nový int [] {2}; Súčty IntStream = IntStream .of (1, 2, 3) .map (val -> val + držiak [0]); držiak [0] = 0; návratové sumy.sum (); }

Mohli by sme si myslieť, že prúd sčítava 2 ku každej hodnote, ale v skutočnosti sa to sčíta na 0, pretože toto je najnovšia dostupná hodnota, keď je vykonaná lambda.

Poďme o krok ďalej a vykonajme súčet v inom vlákne:

public void workaroundMultithreading () {int [] držiteľ = nový int [] {2}; Runnable runnable = () -> System.out.println (IntStream .of (1, 2, 3) .map (val -> val + držiak [0]) .sum ()); new Thread (runnable) .start (); // simulovanie nejakého spracovania try {Thread.sleep (new Random (). nextInt (3) * 1000L); } catch (InterruptedException e) {throw new RuntimeException (e); } držiak [0] = 0; }

Akú hodnotu tu sumarizujeme? Závisí to od toho, ako dlho trvá naše simulované spracovanie. Ak je to dosť krátke na to, aby sa vykonanie metódy mohlo ukončiť pred vykonaním druhého vlákna, vytlačí sa 6, v opačnom prípade sa vytlačí 12.

Všeobecne sú tieto druhy riešení náchylné na chyby a môžu priniesť nepredvídateľné výsledky, preto by sme sa im mali vždy vyhnúť.

6. Záver

V tomto článku sme vysvetlili, prečo môžu výrazy lambda používať iba konečné alebo efektívne konečné miestne premenné. Ako sme videli, toto obmedzenie pochádza z rozdielnej povahy týchto premenných a z toho, ako ich Java ukladá do pamäte. Ukázali sme tiež nebezpečenstvo použitia spoločného riešenia.

Úplný zdrojový kód príkladov je ako vždy k dispozícii na serveri GitHub.


$config[zx-auto] not found$config[zx-overlay] not found