Явкие примитивные атомы по конструкции или случайно?

Являются ли 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 в атомарной операции, с преимуществом отсутствия необходимости блокировать.