Следует ли синхронизировать метод запуска? Почему или почему нет?

Я всегда думал, что синхронизация метода запуска в java-классе, который реализует Runnable, избыточна. Я пытаюсь понять, почему люди это делают:

public class ThreadedClass implements Runnable{
    //other stuff
    public synchronized void run(){
        while(true)
             //do some stuff in a thread
        }
    }
}

Это кажется излишним и ненужным, поскольку они получают блокировку объекта для другого потока. Или, скорее, они прямо указывают, что только один поток имеет доступ к методу run(). Но поскольку это метод запуска, разве это не отдельный поток? Следовательно, только он может получить доступ к себе и ему не нужен отдельный механизм блокировки?

Я нашел в Интернете предложение о том, что, синхронизируя метод запуска, вы потенциально можете создать очередь потоков де-факто, например, выполнив следующее:

 public void createThreadQueue(){
    ThreadedClass a = new ThreadedClass();
    new Thread(a, "First one").start();
    new Thread(a, "Second one, waiting on the first one").start();
    new Thread(a, "Third one, waiting on the other two...").start();
 }

Я бы никогда не сделал этого лично, но возникает вопрос, зачем кому-то синхронизировать метод запуска. Есть идеи, почему или почему не следует синхронизировать метод запуска?


person MHP    schedule 05.09.2011    source источник
comment
очередь неисправна (монитор объектов несправедлив, и второй поток может запуститься раньше первого), единственная причина, которую я могу себе представить, заключается в том, чтобы гарантировать, что когда исполняемый объект дважды отправляется исполнителю/потоку, он не создавать гонки   -  person ratchet freak    schedule 06.09.2011
comment
@irreputable Мой профессор сделал это в примере. Я бы никогда лично - кроме того, что я жду, чтобы увидеть, есть ли какая-нибудь блестящая причина для этого, на которую еще никто не указал.   -  person MHP    schedule 06.09.2011
comment
@ratchet Хороший вопрос. Я предполагаю, что вы хотели бы, чтобы синхронизированный запуск выполнялся только в том случае, если есть странная причина, по которой другой поток может быть выполнен на том же объекте. Но даже тогда я бы решил это по-другому, я думаю.   -  person MHP    schedule 06.09.2011
comment
@MHP атомарное логическое значение hasRun и if(!hasRun.CompareAndSwap(false,true))return; в запуске лучше (поскольку оно не блокирует поток и гарантирует, что запуск выполняется только один раз), но требует дополнительного кода и отдельной переменной   -  person ratchet freak    schedule 06.09.2011
comment
Это довольно странно. Я немного беспокоюсь об обучении, которое ты получаешь. Вы, очевидно, можете видеть сквозь туман, но, вероятно, это не относится ко всем в вашем классе. Раздражает ситуация, когда нужно делать домашнюю работу: ты делаешь обычное дело или то, что делает твой профессор?   -  person toto2    schedule 06.09.2011


Ответы (7)


Синхронизация метода run() метода Runnable совершенно бессмысленна, если только вы не хотите разделить Runnable между несколькими потоками, и вы хотите упорядочить выполнение этих потоков. Что в принципе является противоречием в терминах.

Теоретически существует еще один гораздо более сложный сценарий, в котором вы можете захотеть синхронизировать метод run(), который снова включает совместное использование Runnable несколькими потоками, но также использует wait() и notify(). Я никогда не сталкивался с этим за более чем 21 год Java.

person user207421    schedule 06.09.2011
comment
@KenyakornKetsombut Как именно? Вы только что столкнулись с каким другим примером? Доказательства, пожалуйста, не просто анекдот. Это наука, между прочим. - person user207421; 06.01.2017
comment
Что, если Runnable должен прочитать общий объект? У меня только что был NPE в блоке с if (sharedObject != null) { threadObject = sharedObject.getField(); }, потому что sharedObject был аннулирован другим потоком между двумя инструкциями. Было бы глупо объявить мой метод run синхронизированным? Это не будет противоречить цели (работа в фоновом потоке), это просто заблокирует запись в общий объект во время работы потока. - person Benoit Duffez; 01.03.2018
comment
@BenoitDuffez Тогда код должен синхронизироваться с общим объектом во время доступа к нему. Не на Runnable за все время. - person user207421; 11.09.2018
comment
@user207421 user207421 Хотя это правильное замечание, я бы добавил, что код должен синхронизироваться с объектом блокировки специального назначения — например, с закрытым полем, объявленным рядом с методом run (), поскольку общий объект может быть аннулирован другим процессом или задачей как @BenoitDuffez указал, что вы не можете использовать synchronized() на чем-то, что может быть null. - person leonidos79; 21.05.2020

Существует 1 преимущество использования synchronized void blah() по сравнению с void blah() { synchronized(this) {, и это то, что ваш результирующий байт-код будет на 1 байт короче, поскольку синхронизация будет частью сигнатуры метода, а не операцией сама по себе. Это может повлиять на возможность встраивания метода JIT-компилятором. В остальном нет никакой разницы.

Лучший вариант — использовать внутренний private final Object lock = new Object(), чтобы кто-то не мог заблокировать ваш монитор. Он достигает того же результата без обратной стороны злой внешней блокировки. У вас есть этот дополнительный байт, но он редко имеет значение.

Так что я бы сказал нет, не используйте ключевое слово synchronized в подписи. Вместо этого используйте что-то вроде

public class ThreadedClass implements Runnable{
    private final Object lock = new Object();

    public void run(){
        synchronized(lock) {
            while(true)
                 //do some stuff in a thread
            }
        }
    }
}

Изменить в ответ на комментарий:

Рассмотрим, что делает синхронизация: она предотвращает вход других потоков в тот же блок кода. Итак, представьте, что у вас есть класс, подобный приведенному ниже. Допустим, текущий размер равен 10. Кто-то пытается выполнить добавление, и это приводит к изменению размера резервного массива. Пока они изменяют размер массива, кто-то вызывает makeExactSize(5) в другом потоке. И вдруг вы пытаетесь получить доступ к data[6], и он вылетает из вас. Синхронизация должна предотвратить это. В многопоточных программах просто НУЖНА синхронизация.

class Stack {
    int[] data = new int[10];
    int pos = 0;

    void add(int inc) {
        if(pos == data.length) {
            int[] tmp = new int[pos*2];
            for(int i = 0; i < pos; i++) tmp[i] = data[i];
            data = tmp;
        }
        data[pos++] = inc;
    }

    int remove() {
        return data[pos--];
    }

    void makeExactSize(int size) {
        int[] tmp = new int[size];
        for(int i = 0; i < size; i++) tmp[i] = data[i];
        data = tmp;
    }
}
person corsiKa    schedule 05.09.2011
comment
Мне нравится энтузиазм со ссылкой на байт-код, но я действительно имею в виду, есть ли вообще какая-либо польза от синхронизации метода. Мой профессор всегда пишет synchronized void run(), а я никогда этого не делал и не нуждался в этом. Но этот бит байт-кода интересен... - person MHP; 06.09.2011
comment
Ответ на это длиннее комментария. Я отредактирую это в ответе. - person corsiKa; 06.09.2011
comment
на самом деле синхронизация в подписи означает, что JIT/JVM может продолжать удерживать блокировку при вызове нескольких синхронизированных методов (т. е. JVM не освобождает блокировку (и немедленно повторно получает ее), когда следующая операция вызывает следующий синхронизированный метод) - person ratchet freak; 06.09.2011
comment
Хорошо, это правда - повторное получение уже удерживаемой блокировки должно быть дешевле, но тогда я почему-то сомневаюсь, что это заметно в большинстве ситуаций, и я еще не видел ситуации, когда мне нужен синхронизированный метод для экземпляра потока (обычно у вас есть структуры данных более высокого порядка для связи imo) - person Voo; 06.09.2011
comment
@voo Я имел в виду, что вызов t.doThis();t.doThat();, когда они оба объявлены синхронизированными, позволяет JVM удерживать блокировку между вызовами этого и этого - person ratchet freak; 06.09.2011
comment
@ratchet У меня сложилось впечатление, что JIT способна оптимизировать даже блокировки, объявленные в методе, и блокировки, находящиеся в частном порядке (в отличие от монитора вызывающего объекта). Я могу ошибаться, так как не могу сразу предоставить источник для него. - person corsiKa; 06.09.2011
comment
@ratchet freak Да, я тебя понял. Но, по крайней мере, если методы встроены, это можно было бы сделать в любом случае, и тогда я никогда не сталкивался с ситуацией, когда я хотел бы синхронизировать поток в своем собственном экземпляре. Обычно вы хотите, чтобы несколько потоков синхронизировались на какой-то структуре более высокого уровня (например, в примере с глоукодером стек вряд ли будет реализовывать run(), но некоторые потоки будут работать с ним, и им потребуется синхронизация). Может быть, я упускаю какой-то обычный сценарий, но я не могу его представить. - person Voo; 06.09.2011
comment
те же преимущества блокировки, что и в ответе, описаны здесь: docs.oracle. com/javase/tutorial/essential/concurrency/ - person Elia12345; 30.01.2017

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

Почему нет? Это не стандартно. Если вы программируете как часть команды, когда какой-то другой участник увидит ваш синхронизированный run, он, вероятно, потратит 30 минут, пытаясь понять, что такого особенного в вашем run или во фреймворке, который вы используете для запуска Runnable.

person toto2    schedule 06.09.2011

По моему опыту, бесполезно добавлять ключевое слово «synchronized» в метод run(). Если нам нужно синхронизировать несколько потоков или нам нужна потокобезопасная очередь, мы можем использовать более подходящие компоненты, такие как ConcurrentLinkedQueue.

person James Gan    schedule 06.09.2011

Ну, теоретически вы могли бы без проблем вызвать сам метод запуска (в конце концов, он общедоступен). Но это не значит, что нужно это делать. Таким образом, в основном нет причин делать это, кроме добавления незначительных накладных расходов в поток, вызывающий run(). Ну, за исключением случаев, когда вы используете экземпляр несколько раз, вызывая new Thread - хотя я а) не уверен, что это законно с потоковым API и б) кажется совершенно бесполезным.

Также ваш createThreadQueue не работает. synchronized в нестатическом методе синхронизируется с экземпляром объекта (т.е. this), поэтому все три потока будут выполняться параллельно.

person Voo    schedule 05.09.2011
comment
... поэтому все три потока будут выполняться последовательно. Это своего рода то, что я имел в виду под очередью потоков де-факто ... они просто запускались бы после завершения предыдущего. Или я вас неправильно понял? (И, как я уже сказал, если бы мне нужно было что-то подобное, я бы никогда не написал это так... это просто предположение.) - person MHP; 06.09.2011
comment
Нет, я просто запутался. Здесь должно быть написано «Выполнить параллельно» или что-то в этом роде. Я исправлю это. По сути, если бы метод run содержал только оператор print(), вы могли бы использовать все возможные комбинации этих трех предложений. - person Voo; 06.09.2011
comment
Э-э, на самом деле неправильно прочитал пример, так что да, это сработает - хотя несколько странная конструкция. Вы можете получить точно такой же результат с помощью простого for(int i = 0; i < N; i++) a.run(); без накладных расходов. - person Voo; 06.09.2011

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

class Kat{

public static void main(String... args){
  Thread t1;
  // MyUsualRunnable is usual stuff, only this will allow concurrency
  MyUsualRunnable m0 = new MyUsualRunnable();
  for(int i = 0; i < 5; i++){
  t1 = new Thread(m0);//*imp*  here all threads created are passed the same runnable instance
  t1.start();
  }

  // run() method is synchronized , concurrency killed
  // uncomment below block and run to see the difference

  MySynchRunnable1 m1 = new MySynchRunnable1();
  for(int i = 0; i < 5; i++){
  t1 = new Thread(m1);//*imp*  here all threads created are passed the same runnable instance, m1
  // if new insances of runnable above were created for each loop then synchronizing will have no effect

  t1.start();
}

  // run() method has synchronized block which lock on runnable instance , concurrency killed
  // uncomment below block and run to see the difference
  /*
  MySynchRunnable2 m2 = new MySynchRunnable2();
  for(int i = 0; i < 5; i++){
  // if new insances of runnable above were created for each loop then synchronizing will have no effect
  t1 = new Thread(m2);//*imp*  here all threads created are passed the same runnable instance, m2
  t1.start();
}*/

}
}

class MyUsualRunnable implements Runnable{
  @Override
  public void  run(){
    try {Thread.sleep(1000);} catch (InterruptedException e) {}
}
}

class MySynchRunnable1 implements Runnable{
  // this is implicit synchronization
  //on the runnable instance as the run()
  // method is synchronized
  @Override
  public synchronized void  run(){
    try {Thread.sleep(1000);} catch (InterruptedException e) {}
}
}

class MySynchRunnable2 implements Runnable{
  // this is explicit synchronization
  //on the runnable instance
  //inside the synchronized block
  // MySynchRunnable2 is totally equivalent to MySynchRunnable1
  // usually we never synchronize on this or synchronize the run() method
  @Override
  public void  run(){
    synchronized(this){
    try {Thread.sleep(1000);} catch (InterruptedException e) {}
  }
}
}
person somshivam    schedule 07.08.2016

на самом деле очень легко обосновать "синхронизировать или не синхронизировать"

если ваш вызов метода может мутировать внутреннее состояние вашего объекта, тогда "синхронизировать", иначе нет необходимости

простой пример

public class Counter {

  private int count = 0; 

  public void incr() {
    count++;
  }

  public int getCount() {
    return count;
  }
}

в приведенном выше примере необходимо синхронизировать incr(), так как он изменит значение count, тогда как синхронизация getCount() не требуется.

однако есть еще один угловой случай, если счетчик равен java.lang.Long, Double, Object, тогда вам нужно объявить как

private volatile long count = 0;

чтобы убедиться, что обновление ref является атомарным

в основном это то, о чем вам нужно думать в 99% случаев при работе с многопоточностью.

person Dapeng    schedule 06.09.2011
comment
Не отвечает на вопрос. Вопрос конкретно о методе run(), который имеет особые отношения с вызывающим потоком. - person user207421; 06.09.2011