Являются ли примитивные java атомными по замыслу или случайно?
Являются ли Java примитивные целые числа (int) вообще атомными, в этом отношении? Некоторые эксперименты с двумя потоками, совместно использующими int, по-видимому, показывают, что они есть, но, конечно, отсутствие доказательств того, что они не являются, не означает, что они есть.
В частности, я провел следующий тест:
public class IntSafeChecker {
static int thing;
static boolean keepWatching = true;
// Watcher just looks for monotonically increasing values
static class Watcher extends Thread {
public void run() {
boolean hasBefore = false;
int thingBefore = 0;
while( keepWatching ) {
// observe the shared int
int thingNow = thing;
// fake the 1st value to keep test happy
if( hasBefore == false ) {
thingBefore = thingNow;
hasBefore = true;
}
// check for decreases (due to partially written values)
if( thingNow < thingBefore ) {
System.err.println("MAJOR TROUBLE!");
}
thingBefore = thingNow;
}
}
}
// Modifier just counts the shared int up to 1 billion
static class Modifier extends Thread {
public void run() {
int what = 0;
for(int i = 0; i < 1000000000; ++i) {
what += 1;
thing = what;
}
// kill the watcher when done
keepWatching = false;
}
}
public static void main(String[] args) {
Modifier m = new Modifier();
Watcher w = new Watcher();
m.start();
w.start();
}
}
(и это было попробовано только с java jre 1.6.0_07 на 32-битном ПК с Windows)
По сути, модификатор записывает последовательность подсчетов в общее целое число, в то время как наблюдатель проверяет, чтобы наблюдаемые значения никогда не уменьшались. На машине, где к 32-битному значению нужно было обращаться как к четырем отдельным байтам (или даже к двум 16-битным словам), была бы вероятность того, что Watcher перехватит общее целое число в несогласованном, наполовину обновленном состоянии и обнаружит уменьшение значения вместо увеличения. Это должно работать независимо от того, собраны / записаны ли (гипотетические) байты данных 1-й младший бит или 1-й младший бит, но в лучшем случае он только вероятностный.
Представляется весьма вероятным, учитывая современные широкие пути данных, что 32-битное значение может быть эффективно атомарным, даже если спецификация Java не требует этого. На самом деле, с 32-битной шиной данных может показаться, что вам придется работать усерднее, чтобы получить атомарный доступ к байтам, а не 32-битным.
Поиск в Google по "безопасности потоков примитивов java" приводит к большому количеству вещей в классах и объектах, безопасных для потоков, но поиск информации о примитивах, похоже, ищет иголку в стоге сена.
9 ответов
Все обращения к памяти в Java являются атомарными по умолчанию, за исключением long
а также double
(который может быть атомным, но не обязательно). Честно говоря, это не совсем понятно, но я считаю, что это следствие.
Из раздела 17.4.3 JLS:
В последовательном последовательном выполнении существует общий порядок для всех отдельных действий (таких как чтение и запись), который согласуется с порядком программы, и каждое отдельное действие является атомарным и сразу же видимым для каждого потока.
а затем в 17.7:
В некоторых реализациях может оказаться удобным разделить одно действие записи на 64-битном длинном или двойном значении на два действия записи на смежных 32-битных значениях. Ради эффективности это поведение зависит от конкретной реализации; Виртуальные машины Java могут выполнять запись в длинные и двойные значения атомарно или в две части.
Обратите внимание, что атомарность очень отличается от волатильности.
Когда один поток обновляет целое число до 5, это гарантирует, что другой поток не увидит 1 или 4 или любое другое промежуточное состояние, но без какой-либо явной волатильности или блокировки, другой поток может видеть 0 навсегда.
Что касается усердной работы по получению атомарного доступа к байтам, вы правы: виртуальной машине, возможно, придется приложить немало усилий... но она должна. Из раздела 17.6 спецификации:
Некоторые процессоры не предоставляют возможность записи в один байт. Было бы незаконно реализовывать обновления байтового массива на таком процессоре, просто читая все слово, обновляя соответствующий байт, а затем записывая все слово обратно в память. Эта проблема иногда называется разрывом слов, и для процессоров, которые не могут легко обновить отдельный байт отдельно, потребуется другой подход.
Другими словами, это зависит от JVM, чтобы понять это правильно.
- Никакое количество испытаний не может доказать безопасность потока - оно может только опровергнуть его;
- Я нашел косвенную ссылку в JLS 17.7, в которой говорится
В некоторых реализациях может оказаться удобным разделить одно действие записи на 64-битном длинном или двойном значении на два действия записи на смежных 32-битных значениях.
и дальше вниз
Для целей модели памяти языка программирования Java одна запись в энергонезависимое длинное или двойное значение рассматривается как две отдельные записи: по одной на каждую 32-битную половину.
Кажется, это подразумевает, что записи в ints являются атомарными.
Я думаю, что это не работает, как вы ожидали:
private static int i = 0;
public void increment() {
synchronized (i) {
i++;
}
}
целое число является неизменным, поэтому вы все время синхронизируете другой объект. int "i" автоматически помещается в объект Integer, затем вы устанавливаете блокировку для него. Если в этот метод входит другой поток, то int i автоматически помещается в другой объект Integer, и вы устанавливаете блокировку для другого объекта, чем прежде.
Чтение или запись из целого или любого меньшего типа должны быть атомарными, но, как заметил Роберт, длинные и двойные могут или не могут зависеть от реализации. Однако любая операция, которая использует чтение и запись, включая все операторы инкремента, не является атомарной. Таким образом, если у вас есть потоки, работающие с целым числом i=0, один - i++, а другой - i=10, результат может быть 1, 10 или 11.
Для подобных операций вы должны взглянуть на AtomicInteger, который имеет методы для атомарного изменения значения при извлечении старого или для атомарного увеличения значения.
Наконец, потоки могут кэшировать значение переменной и не будут видеть изменения, внесенные в нее другими потоками. Чтобы оба потока всегда видели изменения, внесенные другим потоком, необходимо пометить переменную как энергозависимую.
Я согласен с Джоном Скитом и хотел бы добавить, что многие люди путают понятия атомарности, изменчивости и безопасности потоков, потому что иногда термины используются взаимозаменяемо.
Например, рассмотрим это:
private static int i = 0;
public void increment() { i++; }
В то время как кто-то может утверждать, что эта операция атомарна, упомянутая гипотеза неверна.
Заявление i++;
выполняет три операции:
1) Читать
2) Обновление
3) Написать
Поэтому потоки, которые работают с этой переменной, должны быть синхронизированы следующим образом:
private static int i = 0;
private static final Object LOCK = new Object();
public void increment() {
synchronized(LOCK) {
i++;
}
}
или это:
private static int i = 0;
public static synchronized void increment() {
i++;
}
Обратите внимание, что для отдельного экземпляра объекта, вызывающего метод, к которому обращаются несколько потоков, и которые работают с совместно изменяемыми данными, необходимо учитывать тот факт, что параметры метода, локальная переменная и возвращаемое значение являются локальными для каждого потока.
Для получения дополнительной информации перейдите по этой ссылке:
http://www.javamex.com/tutorials/synchronization_volatile.shtml
Надеюсь это поможет.
ОБНОВЛЕНИЕ: Существует также случай, когда вы можете синхронизироваться на самом объекте класса. Больше информации здесь: Как синхронизировать статическую переменную между потоками, выполняющими разные экземпляры класса в Java?
Это не атомарно:
i++;
Тем не менее, это:
i = 5;
Я думаю, что здесь начинается некоторое замешательство.
Это несколько сложно и связано с размером слова в системе. Брюс Экель обсуждает это более подробно: Java Threads.
Когда данные распределяются между потоками, необходима синхронизация. При работе с целым числом, которое может переходить из основной памяти в кэш процессора в многопроцессорной системе, поток может обновлять локальную копию целого числа, привязанного к конкретному процессору.
Ключевое слово volatile (см. Раздел Wiki in Java) в Java гарантирует, что любое обновление Integer будет происходить в памяти, а не в локальной копии.
Кроме того, чтобы синхронизировать обновления с Integer, рассмотрите возможность использования AtomicInteger. Эта реализация имеет метод (compareAndSet), чтобы проверить, является ли значение тем, что ожидает поток, и установить его, если он делает. Если это не совпадает, то другой поток мог бы обновить значение. AtomicInteger будет выполнять чтение и обновление Integer в атомарной операции, с преимуществом отсутствия необходимости блокировать.
Атомное чтение и запись просто означают, что вы никогда не будете читать, например, первые 16 бит обновления int и еще один из старого значения.
Это ничего не говорит о том, КОГДА другие потоки видят эти записи.
Короче говоря, когда два потока несутся без барьеров памяти между ними, что-то теряется.
Раскрутите два или более потоков, которые увеличивают одно общее целое число, а также подсчитывают свои собственные приращения. Когда целое число достигает некоторого значения (например, INT_MAX. Хорошее и большое, чтобы все стало теплее), все останавливается и возвращает значение int и количество приращений, которые выполнял каждый поток.
import java.util.Stack;
public class Test{
static int ctr = Integer.MIN_VALUE;
final static int THREADS = 4;
private static void runone(){
ctr = 0;
Stack<Thread> threads = new Stack<>();
for(int i = 0; i < THREADS; i++){
Thread t = new Thread(new Runnable(){
long cycles = 0;
@Override
public void run(){
while(ctr != Integer.MAX_VALUE){
ctr++;
cycles++;
}
System.out.println("Cycles: " + cycles + ", ctr: " + ctr);
}
});
t.start();
threads.push(t);
}
while(!threads.isEmpty())
try{
threads.pop().join();
}catch(InterruptedException e){
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println();
}
public static void main(String args[]){
System.out.println("Int Range: " + ((long) Integer.MAX_VALUE - (long) Integer.MIN_VALUE));
System.out.println(" Int Max: " + Integer.MAX_VALUE);
System.out.println();
for(;;)
runone();
}
}
Вот результат этого теста на моем четырехъядерном блоке (не стесняйтесь играть с количеством потоков в коде, я просто соответствовал своему числу ядер, очевидно):
Int Range: 4294967295
Int Max: 2147483647
Cycles: 2145700893, ctr: 76261202
Cycles: 2147479716, ctr: 1825148133
Cycles: 2146138184, ctr: 1078605849
Cycles: 2147282173, ctr: 2147483647
Cycles: 2147421893, ctr: 127333260
Cycles: 2146759053, ctr: 220350845
Cycles: 2146742845, ctr: 450438551
Cycles: 2146537691, ctr: 2147483647
Cycles: 2110149932, ctr: 696604594
Cycles: 2146769437, ctr: 2147483647
Cycles: 2147095646, ctr: 2147483647
Cycles: 2147483647, ctr: 2147483647
Cycles: 2147483647, ctr: 330141890
Cycles: 2145029662, ctr: 2147483647
Cycles: 2143136845, ctr: 2147483647
Cycles: 2147007903, ctr: 2147483647
Cycles: 2147483647, ctr: 197621458
Cycles: 2076982910, ctr: 2147483647
Cycles: 2125642094, ctr: 2147483647
Cycles: 2125321197, ctr: 2147483647
Cycles: 2132759837, ctr: 330963474
Cycles: 2102475117, ctr: 2147483647
Cycles: 2147390638, ctr: 2147483647
Cycles: 2147483647, ctr: 2147483647