Мутирующий экземпляр или локальные переменные объекта в Lambda java 8
Я знаю, что по причинам concurrency я не могу обновить значение локальной переменной в лямбда в Java 8. Так что это незаконно:
double d = 0;
orders.forEach( (o) -> {
d+= o.getTotal();
});
Но как насчет обновления переменной экземпляра или изменения состояния локального объекта? Например, приложение Swing имеет кнопку и метку, объявленную как переменные экземпляра, когда я нажимаю кнопку Я хочу скрыть метку
jButton1.addActionListener(( e) -> {
jLabel.setVisible(false);
});
Я не получаю ошибок компилятора и отлично работает, но... правильно ли изменить состояние объекта в лямбда?, У меня будут проблемы с concurrency или что-то плохое в будущем?
Вот еще один пример. Представьте, что следующий код находится в методе doGet сервлета
Будет ли у меня какая-то проблема здесь?, Если ответ да: почему?
String key = request.getParameter("key");
Map<String, String> resultMap = new HashMap<>();
Map<String, String> map = new HashMap<>();
//Load map
map.forEach((k, v) -> {
if (k.equals(key)) {
resultMap.put(k, v);
}
});
response.getWriter().print(resultMap);
Что я хочу знать: когда правильно мутировать состояние экземпляра объекта в лямбда?
Ответы
Ответ 1
В общем: да, вы можете получить проблемы concurrency, но только те, которые у вас уже есть. Лэмбдафинг это не сделает код не-потоковым, где он был раньше, или наоборот. В примере, который вы указываете, ваш код (возможно) потокобезопасен, потому что ActionListener
только когда-либо вызывается в потоке диспетчеризации событий. Если вы наблюдаете однопоточное правило Swing, ни один другой поток никогда не обращается к jLabel
, и если это так, то на нем не может быть потока. Но этот вопрос ортогонален использованию лямбда.
Ответ 2
Ваши предположения неверны.
Вы можете изменить только эффективно конечные переменные в lambdas, потому что lambdas являются синтаксическим сахаром * над анонимными внутренними классами.
* Они на самом деле больше, чем синтаксический сахар, но здесь это не актуально.
И в анонимных внутренних классах вы можете изменять только эффективно конечные переменные, поэтому то же самое верно для lambdas.
Вы можете делать все, что хотите, с помощью lambdas, пока компилятор разрешает это, на часть поведения сейчас:
- Если вы изменяете состояние, которое зависит от другого состояния, в параллельной настройке, тогда у вас возникают проблемы.
- Если вы изменяете состояние, которое зависит от другого состояния, в линейной настройке, тогда все в порядке.
- Если вы измените состояние, которое не зависит от чего-либо еще, тогда все будет хорошо.
Некоторые примеры:
class MutableNonSafeInt {
private int i = 0;
public void increase() {
i++;
}
public int get() {
return i;
}
}
MutableNonSafeInt integer = new MutableNonSafeInt();
IntStream.range(0, 1000000)
.forEach(i -> integer.increase());
System.out.println(integer.get());
Это будет печатать 1000000, как ожидалось, независимо от того, что произойдет, даже если это зависит от предыдущего состояния.
Теперь распараллелите поток:
MutableNonSafeInt integer = new MutableNonSafeInt();
IntStream.range(0, 1000000)
.parallel()
.forEach(i -> integer.increase());
System.out.println(integer.get());
Теперь он печатает различные целые числа, такие как 199205 или 249165, потому что другие потоки не всегда видят изменения, внесенные разными потоками, потому что синхронизация отсутствует.
Но скажите, что теперь мы избавляемся от нашего фиктивного класса и используем AtomicInteger
, который является потокобезопасным, мы получаем следующее:
AtomicInteger integer = new AtomicInteger(0);
IntStream.range(0, 1000000)
.parallel()
.forEach(i -> integer.getAndIncrement());
System.out.println(integer.get());
Теперь он корректно печатает 1000000 снова.
Однако синхронизация является дорогостоящей, и мы потеряли почти все преимущества распараллеливания здесь.
Ответ 3
в случае, если 'forEach' распространяется на разные потоки/ядра, у вас могут возникнуть проблемы concurrency. рассмотрите возможность использования атомических или параллельных структур (например, ConcurrentHashMap)