Переменные в Java делятся на две категории: локальные переменные и переменные класса. Локальные переменные относятся к переменным, определенным внутри метода, например, к переменным, определенным в методе запуска. Для этих переменных не существует проблем совместного использования между потоками. Поэтому они не требуют синхронизации данных. Переменные класса — это переменные, определенные в классе, и их областью действия является весь класс. Такие переменные могут использоваться несколькими потоками. Поэтому нам необходимо выполнить синхронизацию данных по таким переменным.
Синхронизация данных означает, что только один поток может одновременно обращаться к синхронизированным переменным класса. После того, как текущий поток получил доступ к этим переменным, другие потоки могут продолжать обращаться к ним. Упомянутый здесь доступ относится к доступу с помощью операций записи. Если все потоки, обращающиеся к переменным класса, являются операциями чтения, синхронизация данных обычно не требуется. Что же произойдет, если синхронизация данных не будет выполнена для переменных общего класса? Давайте сначала посмотрим, что происходит со следующим кодом:
Скопируйте код кода следующим образом:
тест упаковки;
публичный класс MyThread расширяет поток
{
общественный статический int n = 0;
публичный недействительный запуск()
{
интервал м = п;
урожай();
м++;
п = м;
}
public static void main(String[] args) выдает исключение
{
MyThread myThread = новый MyThread ();
Потоки [] = новая тема [100];
for (int i = 0; i < threads.length; i++)
потоки [я] = новый поток (myThread);
for (int i = 0; i < threads.length; i++)
потоки[i].start();
for (int i = 0; i < threads.length; i++)
потоки[i].join();
System.out.println("n = " + MyThread.n);
}
}
Возможные результаты выполнения приведенного выше кода следующие:
Скопируйте код кода следующим образом:
п=59
Многие читатели могут быть удивлены, увидев этот результат. Эта программа, очевидно, запускает 100 потоков, а затем каждый поток увеличивает статическую переменную n на 1. Наконец, используйте метод join, чтобы запустить все 100 потоков, а затем выведите значение n. В норме результат должен быть n = 100. Но результат меньше 100.
На самом деле виновником такого результата являются «грязные данные», о которых мы часто упоминаем. Оператор yield() в методе run является инициатором «грязных данных» (без добавления оператора урожайности также могут быть сгенерированы «грязные данные», но это будет не так очевидно. Только заменив 100 на большее число, получится такое часто случается. Генерация «грязных данных» (вызов урожайности в этом примере призван усилить эффект «грязных данных»). Функция метода yield заключается в том, чтобы приостановить поток, то есть заставить поток, вызывающий метод доходности, временно отдать ресурсы ЦП, давая ЦП возможность выполнить другие потоки. Чтобы проиллюстрировать, как эта программа генерирует «грязные данные», предположим, что создаются только два потока: поток1 и поток2. Поскольку первым вызывается метод запуска потока 1, первым обычно запускается метод запуска потока 1. Когда метод run потока thread1 переходит к первой строке (int m = n;), значение n присваивается m. Когда выполняется метод выхода второй строки, поток 1 временно прекращает выполнение. Когда поток 1 приостанавливается, поток 2 начинает работу после получения ресурсов ЦП (ранее поток 2 находился в состоянии готовности, когда поток 2 выполняет первую строку (int When). m = n;), поскольку n по-прежнему равно 0, когда поток 1 выполняется для выхода, значение, полученное m в потоке 2, также равно 0. Это приводит к тому, что значения m потоков thread1 и thread2 равны 0. После выполнения метода доходности все они начинают с 0 и добавляют 1. Поэтому независимо от того, кто выполнит его первым, окончательное значение n равно 1, но этому n присваивается значение потоками 1 и потоком 2 соответственно. Кто-то может спросить, а если будет только n++, будут ли генерироваться "грязные данные"? Ответ: да. Итак, n++ — это всего лишь оператор, так как же передать процессор другим потокам во время выполнения? На самом деле, это всего лишь поверхностное явление. После того как n++ компилируется в промежуточный язык (также называемый байт-кодом) компилятором Java, он уже не является языком. Давайте посмотрим, в какой промежуточный язык Java будет скомпилирован следующий Java-код.
Скопируйте код кода следующим образом:
публичный недействительный запуск()
{
н++;
}
Скомпилированный код промежуточного языка
Скопируйте код кода следующим образом:
публичный недействительный запуск()
{
aload_0
обмануть
получить поле
iconst_1
ядобавить
путфилд
возвращаться
}
Вы можете видеть, что в методе run есть только оператор n++, но после компиляции остается 7 операторов промежуточного языка. Нам не нужно знать, каковы функции этих операторов, просто посмотрите на операторы в строках 005, 007 и 008. Строка 005 — это getfield. Согласно ее английскому значению, мы знаем, что хотим получить определенное значение. Поскольку здесь есть только одно n, нет никаких сомнений в том, что мы хотим получить значение n. Нетрудно догадаться, что iadd в строке 007 предназначен для прибавления 1 к полученному значению n. Я думаю, вы уже догадались о значении поля putfield в строке 008. Оно отвечает за обновление n после добавления 1 обратно в переменную класса n. Говоря об этом, у вас все еще могут возникнуть сомнения. При выполнении n++ достаточно просто прибавить n к 1. Почему это требует столько хлопот? Фактически, это связано с проблемой модели памяти Java.
Модель памяти Java разделена на основную область хранения и рабочую область памяти. В основной памяти хранятся все экземпляры на языке Java. То есть после того, как мы используем new для создания объекта, объект и его внутренние методы, переменные и т. д. сохраняются в этой области, и в этой области сохраняется n в классе MyThread. Основное хранилище может использоваться всеми потоками. Рабочая область хранения — это стек потоков, о котором мы говорили ранее. В этой области хранятся переменные, определенные в методе run, и метод, вызванный методом run, то есть переменные метода. Когда поток хочет изменить переменные в основной области памяти, он не изменяет эти переменные напрямую, а копирует их в рабочую область памяти текущего потока. После завершения модификации значение переменной перезаписывается соответствующим. значение в основной области памяти.
Поняв модель памяти Java, нетрудно понять, почему n++ не является атомарной операцией. Он должен пройти процесс копирования, добавления 1 и перезаписи. Этот процесс аналогичен тому, который моделируется в классе MyThread. Как вы можете себе представить, если поток thread1 по какой-либо причине будет прерван при выполнении getfield, произойдет ситуация, аналогичная результату выполнения класса MyThread. Чтобы полностью решить эту проблему, мы должны использовать некоторый метод для синхронизации n, то есть только один поток может обрабатывать n одновременно, что также называется атомарной операцией над n.