Параллелизм Java позволяет нескольким потокам работать вместе в одной программе. Однако написание многопоточного кода может быть сложной задачей, поскольку одновременный доступ к общим ресурсам может привести к проблемам синхронизации, таким как состояния гонки и взаимоблокировки.
В этой статье мы рассмотрим, как локальные переменные и параметры методов в Java обеспечивают потокобезопасность и почему это важно в многопоточной среде.
1. Модель памяти Java
В Java локальные переменные, объекты и методы хранятся в разных областях памяти в зависимости от их области действия и жизненного цикла. Подробнее о том, как они хранятся, можно узнать в руководстве по модели памяти Java.

В среде выполнения каждый из потоков JVM имеет частный стек, созданный в то же время, когда создается поток. Кадры стека используются для хранения данных, включая локальные переменные, такие как примитивы и ссылки на объекты, параметры методов и частичные результаты вычислений для выполнения динамического связывания, возвращаемых значений для методов и диспетчеризации исключений.
Обратите внимание, что виртуальная машина Java использует локальные переменные для передачи параметров при вызове метода.
2. Почему локальные переменные потокобезопасны?
В Java локальные переменные по своей сути потокобезопасны, поскольку они хранятся в стековой памяти, которая представляет собой уникальное пространство памяти, выделяемое каждому потоку выполнения.
Когда поток вызывает метод, он создает новый стековый фрейм, содержащий локальные переменные для этого метода. Это означает, что каждый поток имеет свою собственную копию локальных переменных и может получать к ним доступ и изменять их независимо от других потоков.
Например, рассмотрим простую программу Java, которая использует локальные переменные в многопоточной среде:
public class MyRunnable implements Runnable {public void run() {int x = 0;while(x < 10) {System.out.println(Thread.currentThread().getName() + ": " + x);x++;}}public static void main(String[] args) {MyRunnable r = new MyRunnable();Thread t1 = new Thread(r);Thread t2 = new Thread(r);t1.start();t2.start();}}
В этой программе класс MyRunnable реализует интерфейс Runnable, что позволяет запускать его в отдельном потоке выполнения. Метод run() содержит локальную переменную x, которая инициализируется значением 0 и увеличивается на 1 в цикле. Метод main() создает два потока, которые запускают экземпляр MyRunnable одновременно.
Поскольку каждый поток имеет собственную стековую память, локальная переменная x в методе run() является потокобезопасной. Каждый поток имеет собственную копию x и может изменять ее независимо от другого потока. Это означает, что вывод программы недетерминирован и зависит от порядка выполнения потоков:
Thread-0: 0Thread-0: 1Thread-0: 2Thread-1: 0Thread-1: 1Thread-1: 2Thread-0: 3Thread-1: 3Thread-0: 4Thread-1: 4......
3. Потокобезопасность локальных переменных в потоках
В потоках Java переменные, используемые в потоковых операциях, как правило, потокобезопасны. Это происходит потому, что потоки работают с источником данных и возвращают новый поток в качестве выходных данных, не изменяя исходный источник данных. Таким образом, состояние исходного источника данных не изменяется, и переменные, используемые в потоковых операциях, не изменяются.
Однако существуют некоторые случаи, когда безопасность потока может быть нарушена. Например, если операция потока включает в себя изменение общей переменной состояния, то безопасность потока может быть нарушена. В таких случаях важно использовать синхронизацию или атомарные операции для обеспечения безопасности потока.
Давайте рассмотрим пример, чтобы проиллюстрировать это. Предположим, у нас есть список целых чисел, и мы хотим выполнить над ними некоторые операции с использованием потоков. У нас есть общая переменная состояния 'sum', которую мы хотим использовать для отслеживания суммы целых чисел.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);int sum = 0;sum = numbers.stream().mapToInt(Integer::intValue).peek(x -> sum += x).sum();
В этом примере мы используем переменную 'sum' внутри метода peek() для отслеживания суммы целых чисел. Однако, поскольку 'sum' является общей переменной состояния, этот код не является потокобезопасным. Поэтому вопрос в том, как сделать этот код потокобезопасным.
Чтобы сделать код потокобезопасным, можно использовать механизмы синхронизации, такие как синхронизированные блоки или класс AtomicInteger.
3.1 Использование синхронизированных блоков
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);int sum = 0;synchronized(numbers) {sum = numbers.stream().mapToInt(Integer::intValue).peek(x -> {synchronized(this) {sum += x;}}).sum();}
В этом измененном коде мы используем два синхронизированных блока, чтобы гарантировать синхронизацию доступа к списку чисел и переменной суммы между потоками.
- Внешний синхронизированный блок блокирует список номеров, что не позволяет другим потокам изменять его во время обработки потока.
- Внутренний синхронизированный блок блокирует переменную суммы, что гарантирует, что только один поток может изменять ее одновременно.
3.2 Использование параллельных типов
В качестве альтернативы мы можем использовать класс AtomicInteger, чтобы сделать переменную суммы потокобезопасной без использования синхронизированных блоков:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);AtomicInteger sum = new AtomicInteger(0);sum.addAndGet(numbers.stream().mapToInt(Integer::intValue).sum());
В этом измененном коде мы используем класс AtomicInteger, чтобы сделать переменную суммы потокобезопасной. Метод addAndGet() атомарно добавляет сумму чисел в потоке к переменной суммы, гарантируя, что никакие другие потоки не смогут изменить ее в то же время.
В целом, хотя переменные, используемые в потоках, как правило, потокобезопасны, важно помнить о переменных общего состояния и принимать необходимые меры предосторожности для обеспечения потокобезопасности при работе с ними.
4. Часто задаваемые вопросы
Почему локальные переменные потокобезопасны в Java?
Локальные переменные в Java потокобезопасны, поскольку они хранятся в стековой памяти, которая является частной для каждого потока. Это означает, что каждый поток имеет свою собственную копию локальных переменных, что исключает возможность одновременного доступа и изменения другими потоками.
Все ли переменные потокобезопасны в Java?
Нет, не все переменные в Java потокобезопасны. Переменные, которые совместно используются потоками, такие как переменные экземпляра и статические переменные, по умолчанию не потокобезопасны и требуют синхронизации для обеспечения потокобезопасности.
Как обеспечить потокобезопасность при использовании общих ресурсов в Java?
Для обеспечения потокобезопасности при использовании общих ресурсов в Java можно использовать механизмы синхронизации, такие как синхронизированные методы, синхронизированные блоки или пакет java.util.concurrent. Также можно использовать потокобезопасные коллекции, такие как ConcurrentHashMap или синхронизированные коллекции.
Можно ли обеспечить потокобезопасность в Java без синхронизации?
Да, безопасность потоков может быть достигнута в Java без синхронизации с помощью локальных переменных потока или неизменяемых объектов. Однако эти подходы могут не подходить для всех сценариев и могут потребовать другого подхода к проектированию или программированию.
5. Заключение
В этой статье мы рассмотрели, почему локальные переменные в Java потокобезопасны и как они хранятся в памяти. Мы также обсудили потокобезопасность переменных, используемых в потоках, и как обеспечить потокобезопасность при использовании общих ресурсов. Понимая эти концепции, вы сможете писать многопоточный код Java, который будет и эффективным, и мощным.