Автор оригинала: Martin van Wingerden.
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); } }
В пятой строке мы создаем экземпляр одной копии класса Локальные переменные . В следующих двух строках мы начинаем два потока. Оба будут выполнять метод run одного и того же экземпляра.
Внутри метода run мы обновляем поле field класса Local Variables|/. Во-вторых, мы видим назначение локальному примитиву. Наконец, мы выводим эти два поля на консоль.
Давайте взглянем на расположение в памяти всех полей.
Во-первых, поле | является полем класса Локальных переменных . Поэтому он живет на куче. Во-вторых, локальная переменная number является примитивом. Следовательно, он находится в стеке.
Оператор println – это место, где все может пойти не так при запуске двух потоков.
Во-первых, поле field имеет высокую вероятность возникновения проблем, поскольку и ссылка, и объект находятся в куче и совместно используются нашими потоками. Примитив local будет в порядке, так как значение находится в стеке. Следовательно, JVM не разделяет local между потоками.
Поэтому при выполнении мы могли бы, например, получить следующий результат:
821695124:1189444795 821695124:47842893
В этом случае мы можем видеть, что у нас действительно было столкновение между двумя потоками . Мы подтверждаем это, поскольку крайне маловероятно, что оба потока сгенерировали одно и то же случайное целое число.
4. Локальные Переменные Внутри Лямбд
Лямбды (и анонимные внутренние классы ) могут быть объявлены внутри метода и могут обращаться к локальным переменным метода. Однако без каких-либо дополнительных охранников это может привести к большим неприятностям.
До JDK 8 существовало явное правило, согласно которому анонимные внутренние классы могли обращаться только к конечным локальным переменным . JDK 8 ввел новую концепцию эффективного финала, и правила были сделаны менее строгими. Мы уже сравнивали final и effectively final раньше, а также подробнее обсуждали effectively final при использовании лямбд .
Следствием этого правила является то, что поля, к которым обращаются внутри лямбд, должны быть окончательными или фактически окончательными (таким образом, они не изменяются), что делает их потокобезопасными из-за неизменности .
Мы можем увидеть это поведение на практике в следующем примере:
public static void main(String... args) { String text = ""; // text = "675"; new Thread(() -> System.out.println(text)) .start(); }
В этом случае раскомментирование кода в строке 3 приведет к ошибке компиляции. Потому что тогда локальная переменная text больше не является фактически окончательной.
5. Заключение
В этой статье мы рассмотрели потокобезопасность локальных переменных и увидели, что это является следствием модели памяти JVM. Мы также рассмотрели использование локальных переменных в сочетании с лямбдами. JVM защищает свою потокобезопасность,требуя неизменности.
Как всегда, полный исходный код статьи доступен на GitHub .