Являются ли Java-примитивные атомы атомными по замыслу или случайно? - PullRequest
41 голосов
/ 17 июня 2009

Являются ли 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 ]

59 голосов
/ 17 июня 2009

Все обращения к памяти в Java являются атомарными по умолчанию, за исключением long и double (которые могут быть атомарными, но не обязательно). Честно говоря, это не так: 1005 * очень , если честно, но я считаю, что это следствие.

С раздел 17.4.3 JLS:

Внутри последовательно выполнение, есть общий заказ все отдельные действия (такие как чтение и пишет) что согласуется с порядок программы, и каждый индивидуальное действие является атомным и сразу виден каждой теме.

, а затем в 17,7 :

Некоторые реализации могут найти его удобно разделить одну запись действие на 64-битном длинном или двойном значение в два действия записи на смежные 32-битные значения. За Ради эффективности, это поведение конкретная реализация; Виртуальная Java машины могут свободно выполнять запись в длинные и двойные значения атомарно или в двух частях.

Обратите внимание, что атомарность очень отличается от волатильности.

Когда один поток обновляет целое число до 5, гарантируется, что другой поток не увидит 1 или 4 или любое другое промежуточное состояние, но без какой-либо явной волатильности или блокировки, другой поток может видеть 0 навсегда.

Что касается усердной работы по получению атомарного доступа к байтам, вы правы: виртуальной машине, возможно, придется приложить немало усилий ... но она должна. С раздел 17.6 спецификации:

Некоторые процессоры не обеспечивают Возможность записи в один байт. Это было бы незаконно реализовать байт обновления массива на таком процессоре просто читая все слово, обновление соответствующего байта и затем написать все слово обратно объем памяти. Эта проблема иногда известный как разрыв слов, и на процессоры, которые не могут легко обновить один байт в изоляции какой-то другой потребуется подход.

Другими словами, JVM должна сделать все правильно.

27 голосов
/ 17 июня 2009
  • Никакое количество испытаний не может доказать безопасность потока - оно может только опровергнуть это;
  • Я нашел косвенную ссылку в JLS 17.7 , которая гласит

В некоторых реализациях может оказаться удобным разделить одно действие записи для 64-разрядного длинного или двойного значения на два действия записи для смежных 32-разрядных значений.

и далее вниз

Для целей модели памяти языка программирования Java одиночная запись в энергонезависимое длинное или двойное значение рассматривается как две отдельные записи: по одной на каждую 32-битную половину.

Кажется, это означает, что записи в целые числа являются атомарными.

4 голосов
/ 24 мая 2012

Я думаю, что это не работает, как вы ожидали:

private static int i = 0;
public void increment() {
   synchronized (i) { 
      i++; 
   }
}

целое число является неизменным, поэтому вы все время синхронизируете другой объект. int "i" автоматически помещается в объект Integer, затем вы устанавливаете блокировку для него. Если в этот метод входит другой поток, то int i автоматически помещается в другой объект Integer и вы устанавливаете блокировку для другого объекта, чем раньше.

4 голосов
/ 30 июня 2011

Я согласен с Джоном Скитом и хотел бы добавить, что многие люди путают понятия атомарности, изменчивости и безопасности потоков, потому что иногда термины используются взаимозаменяемо.
Например, рассмотрим это:

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?

3 голосов
/ 17 июня 2009

Чтение или запись из целого или любого меньшего типа должны быть атомарными, но, как заметил Роберт, длинные и двойные могут или не могут зависеть от реализации. Однако любая операция, которая использует чтение и запись, включая все операторы инкремента, не является атомарной. Таким образом, если у вас есть потоки, работающие с целым числом i = 0, один - i ++, а другой - i = 10, результат может быть 1, 10 или 11.

Для подобных операций вы должны взглянуть на AtomicInteger , который имеет методы для атомарного изменения значения при извлечении старого или для атомарного увеличения значения.

Наконец, потоки могут кэшировать значение переменной и не будут видеть изменения, внесенные в нее другими потоками. Чтобы оба потока всегда видели изменения, сделанные другим потоком, необходимо пометить переменную как volatile.

1 голос
/ 19 августа 2013

Это не атомарно:

i++;

Однако это:

i = 5;

Я думаю, что здесь возникает некоторая путаница.

0 голосов
/ 22 декабря 2018

Когда данные распределяются между потоками, необходима синхронизация. При работе с целым числом, которое может переходить из основной памяти в кэш процессора в многопроцессорной системе, поток может обновлять локальную копию целого числа, привязанного к конкретному процессору.

Изменчивое (см. Раздел Wiki in Java) ключевое слово в Java обеспечит любое обновление целого числа в памяти, а не в локальной копии.

Кроме того, для синхронизации обновлений с целым числом рассмотрите использование AtomicInteger. Эта реализация имеет метод (compareAndSet) , чтобы проверить, является ли значение тем, что ожидает поток, и установить его, если он делает. Если это не совпадает, то другой поток мог бы обновить значение. AtomicInteger будет выполнять чтение и обновление Integer в атомарной операции, с преимуществом отсутствия необходимости блокировать.

0 голосов
/ 21 декабря 2012

Атомное чтение и запись просто означает, что вы никогда не будете читать, например, первые 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
0 голосов
/ 17 июня 2009

Это несколько сложно и связано с размером слова в системе. Брюс Экель обсуждает это более подробно: Java Threads .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...