1. Введение
Прежде чем мы представили потокобезопасность и способы ее достижения .
В этой статье мы рассмотрим локальные переменные и почему они потокобезопасны.
2. Память стека и потоки
Начнем с краткого обзора модели памяти JVM.
Самое главное, что JVM разделяет доступную память на стек и память кучи . Во-первых, он хранит все объекты в куче. Во- вторых, он хранит в стеке локальные примитивы и ссылки на локальные объекты .
Кроме того, важно понимать, что каждый поток, включая основной поток, имеет свой собственный стек. Поэтому другие потоки не используют наши локальные переменные, что делает их потокобезопасными .
3. Пример
Давайте теперь продолжим с небольшим примером кода, содержащим локальный примитив и (примитивное) поле:
public class LocalVariables implements Runnable {
private int field;
public static void main(String... args) {
LocalVariables target = new LocalVariables();
new Thread(target).start();
new Thread(target).start();
}
@Override
public void run() {
field = new SecureRandom().nextInt();
int local = new SecureRandom().nextInt();
System.out.println(field + ":" + local);
}
}
В пятой строке мы создаем экземпляр класса LocalVariables
. В следующих двух строках мы начинаем две нити. Оба будут выполнять метод запуска
одного и того же экземпляра.
Внутри метода run
мы обновляем поле field
класса LocalVariables
. Во-вторых, мы видим присваивание локальному примитиву. Наконец, мы выводим два поля на консоль.
Давайте посмотрим на расположение в памяти всех полей.
Во-первых, это поле
класса LocalVariables
. Поэтому он живет в куче. Во-вторых, локальная переменная number
является примитивом. Следовательно, он находится в стеке.
Оператор println
— это место, где что-то может пойти не так при запуске двух потоков.
Во-первых, поле
field имеет высокую вероятность возникновения проблем, поскольку и ссылка, и объект находятся в куче и совместно используются нашими потоками. С примитивным локальным
все будет в порядке, так как значение живет в стеке. Следовательно, JVM не разделяет локальные
данные между потоками.
Таким образом, при выполнении мы могли бы, например, получить следующий вывод:
821695124:1189444795
821695124:47842893
В этом случае мы видим, что у нас действительно произошло столкновение между двумя потоками . Мы утверждаем это, поскольку крайне маловероятно, что оба потока сгенерировали одно и то же случайное целое число.
4. Локальные переменные внутри Lambdas
Лямбда-выражения (и анонимные внутренние классы ) могут быть объявлены внутри метода и могут обращаться к локальным переменным метода. Однако без каких-либо дополнительных охранников это могло привести к большим неприятностям.
До JDK 8 существовало явное правило, согласно которому анонимные внутренние классы могли обращаться только к конечным
локальным переменным . В JDK 8 введена новая концепция эффективного финала, а правила стали менее строгими. Ранее мы сравнивали final и Effective final , а также обсуждали более подробно эффективный final при использовании лямбда -выражений .
Следствием этого правила является то, что поля, к которым осуществляется доступ внутри лямбда-выражений, должны быть окончательными или фактически окончательными (таким образом, они не изменяются), что делает их потокобезопасными из-за неизменности .
Мы можем увидеть это поведение на практике в следующем примере:
public static void main(String... args) {
String text = "";
// text = "675";
new Thread(() -> System.out.println(text))
.start();
}
В этом случае раскомментирование кода в строке 3 вызовет ошибку компиляции. Потому что тогда текст
локальной переменной больше не является окончательным.
5. Вывод
В этой статье мы рассмотрели потокобезопасность локальных переменных и увидели, что это следствие модели памяти JVM. Мы также рассмотрели использование локальных переменных в сочетании с лямбда-выражениями. JVM защищает их потокобезопасность, требуя неизменности.
Как всегда, полный исходный код статьи доступен на GitHub .