1. Въведение
Java 8 ни дава ламбда и като асоциация понятието за ефективно крайни променливи. Някога замисляли ли сте се защо локалните променливи, уловени в ламбда, трябва да бъдат окончателни или ефективно окончателни?
Е, JLS ни дава малко подсказка, когато казва „Ограничението за ефективно крайни променливи забранява достъпа до динамично променящи се локални променливи, чието улавяне вероятно би довело до проблеми с паралелността“. Но какво означава това?
В следващите раздели ще разгледаме по-задълбочено това ограничение и ще видим защо Java го е въвела. Ще покажем примери, за да покажем как влияе на еднонишковите и едновременните приложения , а също така ще развенчаем общ анти-модел за заобикаляне на това ограничение.
2. Улавяне на Ламбда
Ламбда изразите могат да използват променливи, дефинирани във външен обхват. Ние наричаме тези ламбди като улавяне на ламбди . Те могат да улавят статични променливи, променливи на екземпляра и локални променливи, но само локалните променливи трябва да бъдат окончателни или ефективно окончателни.
В по-ранните версии на Java се сблъскахме с това, когато анонимен вътрешен клас улови променлива local към метода, който я заобикаля - трябваше да добавим последната ключова дума преди локалната променлива, за да бъде компилаторът щастлив.
Като малко синтактична захар, сега компилаторът може да разпознае ситуации, когато, докато окончателно Ключовата дума не е налице, позоваването е не променя изобщо, което означава, че е ефективно окончателно. Можем да кажем, че променливата е ефективно окончателна, ако компилаторът не се оплаче, ако я обявим за окончателна.
3. Локални променливи при улавяне на ламбда
Просто казано, това няма да се компилира:
Supplier incrementer(int start) { return () -> start++; }
start е локална променлива и ние се опитваме да я модифицираме вътре в ламбда израз.
Основната причина това да не се компилира е, че ламбда улавя стойността на старта , което означава да направи копие от него. Принуждаването на променливата да бъде окончателно избягва да създава впечатлението, че увеличаването на старта вътре в ламбда може действително да промени параметъра на метода за стартиране .
Но защо прави копие? Е, забележете, че връщаме ламбда от нашия метод. По този начин, ламбда няма да се стартира, докато след параметъра на метода start не се съберат боклуците. Java трябва да направи копие на start, за да може тази ламбда да живее извън този метод.
3.1. Едновременни въпроси
За забавление, нека си представим за момент, че Java е позволила на локалните променливи по някакъв начин да останат свързани със своите уловени стойности.
Какво да правим тук:
public void localVariableMultithreading() { boolean run = true; executor.execute(() -> { while (run) { // do operation } }); run = false; }
Макар това да изглежда невинно, то има коварния проблем „видимост“. Припомнете си, че всяка нишка получава свой собствен комин, и така как ще се гарантира, че нашата докато контур вижда промяната в навечерието променлива в друга купа? Отговорът в други контексти може да бъде използването на синхронизирани блокове или променливата ключова дума.
Тъй като обаче Java налага ефективното окончателно ограничение, не трябва да се тревожим за сложности като тази.
4. Статични или инстанционни променливи при улавяне на ламбда
Примерите преди това могат да повдигнат някои въпроси, ако ги сравним с използването на статични или променливи на екземпляра в ламбда израз.
Можем да направим първия си пример за компилация само чрез преобразуване на нашата начална променлива в променлива на екземпляр:
private int start = 0; Supplier incrementer() { return () -> start++; }
Но защо можем да променим стойността на старта тук?
Просто казано, става въпрос за това къде се съхраняват променливите на членовете. Локалните променливи са в стека, но променливите-членове са в купчината. Тъй като имаме работа с куп памет, компилаторът може да гарантира, че ламбда ще има достъп до последната стойност на старта.
Можем да поправим втория си пример, като направим същото:
private volatile boolean run = true; public void instanceVariableMultithreading() { executor.execute(() -> { while (run) { // do operation } }); run = false; }
В навечерието променлива вече е видим за ламбда дори и когато е изпълнено в друга нишка, тъй като ние добавя летлив ключова дума.
Най-общо казано, когато улавяме променлива на екземпляр, бихме могли да мислим за това като за улавяне на крайната променлива this . Както и да е, фактът, че компилаторът не се оплаква, не означава, че не трябва да взимаме предпазни мерки, особено в среди с много нишки.
5. Избягвайте заобикаляне
За да заобиколи ограничението за локални променливи, някой може да помисли да използва притежатели на променливи, за да модифицира стойността на локална променлива.
Нека видим пример, който използва масив за съхраняване на променлива в еднонишко приложение:
public int workaroundSingleThread() { int[] holder = new int[] { 2 }; IntStream sums = IntStream .of(1, 2, 3) .map(val -> val + holder[0]); holder[0] = 0; return sums.sum(); }
Можем да мислим, че потокът сумира 2 за всяка стойност, но всъщност сумира 0, тъй като това е най-новата налична стойност при изпълнение на ламбда.
Нека да отидем още една стъпка и да изпълним сумата в друга нишка:
public void workaroundMultithreading() { int[] holder = new int[] { 2 }; Runnable runnable = () -> System.out.println(IntStream .of(1, 2, 3) .map(val -> val + holder[0]) .sum()); new Thread(runnable).start(); // simulating some processing try { Thread.sleep(new Random().nextInt(3) * 1000L); } catch (InterruptedException e) { throw new RuntimeException(e); } holder[0] = 0; }
Каква стойност обобщаваме тук? Зависи колко време отнема нашата симулирана обработка. Ако е достатъчно кратък, за да позволи на изпълнението на метода да завърши, преди да се изпълни другата нишка, тя ще отпечата 6, в противен случай ще отпечата 12.
Като цяло този вид заобикалящи мерки са склонни към грешки и могат да доведат до непредсказуеми резултати, така че винаги трябва да ги избягваме.
6. Заключение
В тази статия обяснихме защо ламбда изразите могат да използват само крайни или ефективно крайни локални променливи. Както видяхме, това ограничение идва от различното естество на тези променливи и от това как Java ги съхранява в паметта. Също така показахме опасностите от използването на общо решение.
Както винаги, пълният изходен код за примерите е достъпен в GitHub.