Явкие примитивные атомы по конструкции или случайно?
Являются ли 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-ПК)
По существу, модификатор записывает последовательность счетчиков в общее целое число, а Watcher проверяет, что наблюдаемые значения никогда не уменьшаются. На машине, где 32-битное значение должно было быть доступно в виде четырех отдельных байтов (или даже двух 16-ти битных слов), была бы вероятность того, что Watcher поймает общее целое в несогласованном, частично обновленном состоянии и обнаружит уменьшение значения а не увеличиваться. Это должно сработать, если (гипотетические) байты данных собираются/записываются LSB 1st или MSB 1st, но в лучшем случае это скорее всего.
Казалось бы, очень вероятно, что сегодня широкие пути данных, что 32-битное значение может быть эффективно атомным, даже если спецификация java не требует этого. Фактически, с 32-битной шиной данных, похоже, вам придется больше работать, чтобы получить атомный доступ к байтам, чем к 32-битным ints.
Googling on "java primitive thread safety" включает в себя множество вещей на потокобезопасных классах и объектах, но поиск информации о примитивах, похоже, ищет иглу пословиц в стоге сена.
Ответы
Ответ 1
Все обращения к памяти в Java по умолчанию являются атомарными, за исключением long
и double
(которые могут быть атомарными, но не обязательно). Это не очень ясно, если честно, но я считаю, что импликация.
От раздел 17.4.3 JLS:
В последовательном согласовании исполнение, есть полный порядок над все индивидуальные действия (например, чтение и пишет), что согласуется с порядок программы, и каждый индивидуальное действие является атомарным и немедленно видимый для каждого потока.
а затем в 17.7:
Некоторые реализации могут найти это удобно разделить одну запись действие на 64-битный или двойной значение в два действия записи на смежные 32-битные значения. Для эффективность, это поведение конкретная реализация; Виртуальный виртуальный машины могут выполнять записи в длинные и двойные значения атомарно или в двух частях.
Обратите внимание, что атомарность сильно отличается от волатильности.
Когда один поток обновляет целое число до 5, он гарантирует, что другой поток не увидит 1 или 4 или какое-либо другое промежуточное состояние, но без какой-либо явной волатильности или блокировки другой поток может видеть 0 навсегда.
Что касается работы над получением атомарного доступа к байтам, вы правы: VM, возможно, придется много попробовать... но это действительно так. Из раздел 17.6 спецификации:
Некоторые процессоры не предоставляют способность писать в один байт. Это было бы незаконным для реализации байта обновления массива на таком процессоре просто читая целое слово, обновление соответствующего байта и затем написание всего слова назад Память. Иногда эта проблема известный как разрывание слов, и на процессоров, которые не могут легко обновить одиночный байт в изоляции некоторые другие подход будет необходим.
Другими словами, это до JVM, чтобы понять это.
Ответ 2
- Никакое тестирование не может доказать безопасность потока - это может только опровергнуть его;
- Я нашел косвенную ссылку в JLS 17.7, в которой говорится
В некоторых реализациях может оказаться удобным разделить одно действие записи на 64-битное длинное или двойное значение на два действия записи при смежных 32-битных значениях.
и далее вниз
Для целей модели памяти языка программирования Java одна запись в энергонезависимое длинное или двойное значение рассматривается как две отдельные записи: одна для каждой 32-разрядной половины.
Это, по-видимому, означает, что записи в ints являются атомарными.
Ответ 3
Я согласен с Джоном Скитом, и я хотел бы добавить, что многие люди путают понятие атомарности, волатильности и безопасности потоков, потому что иногда термины используются взаимозаменяемо.
Например, рассмотрите следующее:
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
Надеюсь, что это поможет.
UPDATE. Также существует случай, когда вы можете синхронизировать сам объект класса. Подробнее здесь: Как синхронизировать статическую переменную среди потоков, работающих с разными экземплярами класса в java?
Ответ 4
Я думаю, что он не работает так, как вы ожидали:
private static int i = 0;
public void increment() {
synchronized (i) {
i++;
}
}
integer неизменен, поэтому вы все время синхронизируете на другом объекте.
int "i" автоматически добавляется к объекту Integer, после чего вы устанавливаете его блокировку.
Если в этот метод входит другой поток, то int я автоматически добавляется к другому объекту Integer и вы устанавливаете блокировку на другом объекте, а затем раньше.
Ответ 5
Чтение или запись из целого числа или любого более мелкого типа должно быть атомарным, но, как заметил Роберт, длинные и удвоенные значения могут или не могут зависеть от реализации. Однако любая операция, которая использует как чтение, так и запись, включая все операторы приращения, не является атомарной. Таким образом, если у вас есть потоки, работающие с целым числом я = 0, то я ++, а другой я = 10, результат может быть 1, 10 или 11.
Для таких операций вы должны посмотреть AtomicInteger, в котором есть методы для атомарного изменения значения при извлечении старого или атомарно увеличивайте значение.
Наконец, потоки могут кэшировать значение переменной и не будут видеть изменений, внесенных в нее из других потоков. Чтобы убедиться, что оба потока всегда видят изменения, сделанные другим потоком, вам необходимо пометить эту переменную как неустойчивую.
Ответ 6
Это не атомный:
i++;
Однако это:
i = 5;
Я думаю, что здесь возникает какая-то путаница.
Ответ 7
Это несколько сложно, и связано с системной словесностью. Брюс Эккель обсуждает это более подробно: Темы Java.
Ответ 8
Атомные чтения и записи просто означают, что вы никогда не будете читать, например. первые 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
Ответ 9
Когда данные распределяются между потоками, необходима синхронизация. При работе с целым числом, которое может переходить из основной памяти в кэш процессора в многопроцессорной системе, поток может обновлять локальную копию целого числа, привязанного к конкретному процессору.
Ключевое слово volatile (см. Раздел Wiki in Java) в Java гарантирует, что любое обновление целого числа будет происходить в памяти, а не в локальной копии.
Кроме того, чтобы синхронизировать обновления с Integer, рассмотрите возможность использования AtomicInteger. Эта реализация имеет метод (compareAndSet), чтобы проверить, является ли значение тем, что ожидает поток, и установить его, если он делает. Если это не совпадает, то другой поток мог бы обновить значение. AtomicInteger будет выполнять чтение и обновление Integer в атомарной операции, с преимуществом отсутствия необходимости блокировать.