как это произошло до того, как отношения установились на этих двух потоках в Android

Ниже приведены примеры кода пользовательского класса App и кода класса MainActivity:

public class App extends Application {
  private static String TAG = "APP";
  private int i;

  @Override
  public void onCreate() {
    super.onCreate();
    Log.d(TAG, Thread.currentThread().getName());
    HandlerThread t = new HandlerThread("init-thread");
    t.start();

    i = -100;

    Handler handler = new Handler(t.getLooper());

    handler.post(new Runnable() {
        @Override
        public void run() {
            i = 100;
        }
    });

    handler.post(new Runnable() {
        @Override
        public void run() {
            MainActivity.MainHandler h = new MainActivity.MainHandler(Looper.getMainLooper(), App.this);
            h.sendEmptyMessage(0);
        }
    });
  }

  public int getI() {
    return i;
  }
}

И класс MainActivity:

public class MainActivity extends Activity {
  private static String TAG = "ACT-1";

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }

  @Override
  protected void onResume() {
    super.onResume();
    App app = (App) getApplication();
    Log.e(TAG, "i: " + app.getI()); //prints 100
  }

  public static class MainHandler extends Handler {
    private Application application;
    public MainHandler(Looper looper, Application app) {
        super(looper);
        this.application = app;
    }

    @Override
    public void handleMessage(Message msg) {
        App app = (App) application;
        Log.e(TAG, "MSG.what: " + msg.what);
        Log.e(TAG, "i: " + app.getI()); //prints 100
    }
  }
}

То, что я пытаюсь сделать, это изменить значение «i» на 100 в INIT-THREAD и из основного потока, пытаясь прочитать значение обратно.

Я ожидал, что значение «i» в onResume и handleMessage будет равно -100, потому что они выполняются в MAIN потоке, но напечатанное в журнале значение на самом деле равно 100.

В некотором смысле я пытаюсь воспроизвести классическую ошибку, которую все делают в обычных java-программах, но Android, кажется, разумно избегает этого.

Поэтому мне интересно понять, как Android достигает отношения «происходит до» между двумя потоками.


person Raja    schedule 04.10.2014    source источник


Ответы (3)


При установке значения i в этой программе нет отношения "происходит до". Программа содержит гонку данных и находится в ошибке.

Тот факт, что вы видели, как он дает какой-то конкретный результат в паре тестовых прогонов, вообще ничего не доказывает. Хотя поведение кода не определено, при его запуске он будет что-то делать. На любом конкретном оборудовании он может даже делать это большую часть времени.

Быстро взглянув на код, я не вижу никаких операций чтения-изменения-перезаписи, поэтому я думаю, что создание i volatile сделало бы программу правильной. Однако ни одно из утверждений комментариев о значении, которое печатает конкретный оператор Log, не будет точным.

person G. Blake Meike    schedule 04.10.2014
comment
На самом деле существует отношение «происходит до» из-за обработчика Android. Прочитайте мой ответ ниже. - person Petrakeas; 29.09.2015

Вот бывает-перед спецификацией:

Глава 17 Спецификации языка Java определяет отношение «происходит до» операций с памятью, таких как чтение и запись общих переменных. Результаты записи одним потоком гарантированно будут видны при чтении другим потоком только в том случае, если операция записи происходит до операции чтения.

  1. Синхронизированные и изменчивые конструкции, а также методы Thread.start() и Thread.join() могут формировать отношения «происходит до». В частности: Каждое действие в потоке происходит перед каждым действием в этом потоке, которое происходит позже в порядке выполнения программы.
  2. Разблокировка (синхронизированный блок или выход метода) монитора происходит перед каждой последующей блокировкой (синхронизированный блок или вход метода) того же самого монитора. А поскольку отношение «происходит до» является транзитивным, все действия потока до разблокировки происходят до всех действий, следующих за любым потоком, блокирующим этот монитор.
  3. Запись в изменчивое поле происходит перед каждым последующим чтением того же поля. Запись и чтение изменчивых полей имеют такие же эффекты согласованности памяти, как вход и выход из мониторов, но не влекут за собой блокировку взаимного исключения.
  4. Вызов запуска потока происходит до любого действия в запущенном потоке.
  5. Все действия в потоке происходят до того, как любой другой поток успешно вернется из соединения с этим потоком.

ссылка: http://developer.android.com/reference/java/util/concurrent/package-summary.html

Я прокомментировал код, чтобы объяснить:

public class App extends Application {
  private static String TAG = "APP";
  private int i;

  @Override
  public void onCreate() {
    super.onCreate();
    Log.d(TAG, Thread.currentThread().getName());
    HandlerThread t = new HandlerThread("init-thread");
    t.start();

    i = -100;

    Handler handler = new Handler(t.getLooper());

    handler.post(new Runnable() {
        @Override
        public void run() {
            // before next line, i == -100
            // because if you look into handler.post,
            // it is using synchronized block to enqueue this Runnable.
            // And when this Runnable is dispatched,
            // it is using synchronized block of the same monitor.
            // So from 2. you can conclude the i = -100; happens-before here.
            i = 100;
        }
    });

    handler.post(new Runnable() {
        @Override
        public void run() {
            MainActivity.MainHandler h = new MainActivity.MainHandler(Looper.getMainLooper(), App.this);
            h.sendEmptyMessage(0);
        }
    });
  }

  public int getI() {
    return i;
  }
}

До сих пор там бывает-раньше отношения за i. Позже не бывает до отношения:

public class MainActivity extends Activity {
  private static String TAG = "ACT-1";

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }

  @Override
  protected void onResume() {
    super.onResume();
    App app = (App) getApplication();
    Log.e(TAG, "i: " + app.getI()); //prints 100
    // happens-before not guaranteed:
    // there is no happens-before operation between background thread that
    // sets i to 100 and main thread here is running.
  }

  public static class MainHandler extends Handler {
    private Application application;
    public MainHandler(Looper looper, Application app) {
        super(looper);
        this.application = app;
    }

    @Override
    public void handleMessage(Message msg) {
        App app = (App) application;
        Log.e(TAG, "MSG.what: " + msg.what);
        Log.e(TAG, "i: " + app.getI()); //prints 100
        // happens-before not guaranteed for the same reason
    }
  }
}

Согласно спецификации, как и сказал Блейк, самое простое решение, устраняющее гонку, — это изменить i на volatile. «Однако это не сделало бы какие-либо утверждения комментариев о значении, которое печатает конкретный оператор журнала, точными».

person Helin Wang    schedule 01.04.2015
comment
Почти полностью согласен с комментариями @Helin. Обработчики — это безопасный метод публикации, поэтому, если бы метод запуска в onCreate считывал значение i, он гарантированно читал бы -100. Однако, как я уже сказал, между настройкой i в методе запуска и его чтением в onResume не существует никакой связи. Мне нужно было бы потратить немного больше времени, чтобы быть полностью уверенным в чтении в handleMessage - person G. Blake Meike; 30.09.2015
comment
@ Блейк не должен handler.post гарантировать, что i = -100; произойдет раньше самого себя? Я знаю, что порядок кода не отражает порядок выполнения, но разве событие синхронизации (например, блокировка, handler.post) не гарантирует код до того, как эта строка произойдет — до этой строки? (Я сам не очень ясно здесь). - person Helin Wang; 30.09.2015

Причина, по которой ваш код работает, заключается в том, что метод Handler#post обеспечивает связь между основным потоком и init-thread.

Если вы посмотрите внутрь реализации, в какой-то момент MessageQueue#enqueueMessage имеет синхронизированный блок, использующий self в качестве монитора. Тот же монитор используется, когда MessageQueue (в своем собственном потоке) читает и выполняет помещенные в очередь сообщения/выполняемые объекты.

person Petrakeas    schedule 29.09.2015