Перейти к основному содержимому

Почему локальные переменные потокобезопасны в Java

· 3 мин. чтения

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 .