Здесь есть пара недоразумений. Вы, кажется, не совсем понимаете, что такое поток, что такое поле экземпляра и что такое статическое поле.
поле экземпляра — это область памяти, которая выделяется при создании экземпляра класса (т. е. область памяти выделяется для поля d
при VolatileExample v = new VolatileExample()
). Чтобы сослаться на это место в памяти из класса, вы делаете this.d
(затем вы можете писать и читать из этого места в памяти). Чтобы сослаться на это место в памяти извне класса, оно должно быть доступным (т. е. не private
), и тогда вы должны сделать v.d
. Как видите, каждый экземпляр класса получает свою собственную ячейку памяти для своего собственного поля d
. Итак, если у вас есть 2 разных экземпляра VolatileExample
, у каждого будет свое собственное независимое поле d
.
Статическое поле — это область памяти, которая выделяется после инициализации класса (что, если забыть о возможности использования нескольких ClassLoader
, происходит ровно один раз). Итак, вы можете подумать, что статическое поле — это своего рода глобальная переменная. Чтобы сослаться на это место в памяти, вы должны использовать VolatileExample.d
(доступность также применяется (т. е. если это private
, это можно сделать только внутри класса)).
Наконец, поток выполнения — это последовательность шагов, которые будут выполняться JVM. Вы не должны думать о потоке как о классе или экземпляре класса Thread
, это только запутает вас. Это так просто: последовательность шагов.
основная последовательность шагов определена в методе main(...)
. Это та последовательность шагов, которую JVM начнет выполнять при запуске вашей программы.
Если вы хотите запустить новый поток выполнения для одновременного запуска (т. е. вы хотите, чтобы отдельная последовательность шагов выполнялась одновременно), в Java вы делаете это, создавая экземпляр класса Thread
и вызывая его метод start()
.
Давайте немного изменим ваш код, чтобы было легче понять, что происходит:
public class VolatileExample extends Thread {
private int countDown = 2;
private volatile int d = 0;
public VolatileExample(String name) {
super(name);
}
public String toString() {
return super.getName() + ": countDown " + countDown;
}
public void run() {
while(true) {
d = d + 1;
System.out.println(this + ". Value of d is " + d);
if(--countDown == 0) return;
}
}
public static void main(String[] args) {
VolatileExample ve1 = new VolatileExample("first thread");
ve1.start();
VolatileExample ve2 = new VolatileExample("second thread");
ve2.start();
}
}
Строка VolatileExample ve1 = new VolatileExample("first thread");
создает экземпляр VolatileExample
. Это выделит несколько ячеек памяти: 4 байта для countdown
и 4 байта для d
. Затем вы запускаете новый поток выполнения: ve1.start();
. Этот поток выполнения будет получать доступ (чтение и запись) к ячейкам памяти, описанным ранее в этом абзаце.
Следующая строка, VolatileExample ve2 = new VolatileExample("second thread");
, создает другой экземпляр VolatileExample
, который выделяет 2 новых ячейки памяти: 4 байта для countdown
ve2 и 4 байта для d
ve2. Затем вы запускаете поток выполнения, который будет обращаться к ЭТИМ НОВЫМ ячейкам памяти, а не к тем, которые описаны в предыдущем абзаце.
Теперь, с volatile
или без него, вы видите, что у вас есть два разных поля d
: каждый поток работает с другим полем. Поэтому неразумно ожидать, что d
будет увеличено до 4, так как не существует единственного d
.
Если вы сделаете d
статическим полем, только тогда оба потока (предположительно) будут работать в одной и той же ячейке памяти. Только тогда volatile
вступит в игру, так как только тогда вы будете совместно использовать место в памяти между разными потоками.
Если вы создадите поле volatile
, вам гарантируется, что операции записи идут прямо в основную память, а операции чтения — прямо из основной памяти (т. займет больше времени, но будет гарантированно видимым для других потоков).
Однако это не гарантирует, что вы увидите значение 4, хранящееся в d
. Это потому, что volatile
решает проблему видимости, но не проблему атомарности: increment = read from main memory + operation on the value + write to main memory
. Как видите, 2 разных потока могут прочитать начальное значение (0), оперировать (локально) с ним (получив 1), затем записать его в основную память (оба в конечном итоге запишут 1) — 2 приращения будут воспринимается только как 1.
Чтобы решить эту проблему, вы должны сделать приращение атомарной операцией. Для этого вам нужно либо использовать механизм синхронизации — мьютекс (synchronized (...) { ... }
или явная блокировка) — либо класс, разработанный специально для этого: AtomicInteger
.
person
Bruno Reis
schedule
16.02.2012