Любая причина, чтобы не шлепать ключевое слово «synchronized» везде? - PullRequest
12 голосов
/ 11 июня 2009

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

Я не вижу, чтобы кто-нибудь еще писал код с synchronized. Так есть ли какая-то причина, по которой я не должен иметь везде "synchronized"?

Что если я не слишком беспокоюсь о производительности (т. Е. Метод вызывается не чаще, чем раз в несколько секунд)?

Ответы [ 9 ]

36 голосов
/ 11 июня 2009

Если вы осуществляете беспорядочную синхронизацию, вы также рискуете создать тупик .

Предположим, у меня есть два класса Foo и Bar, оба имеют синхронизированный метод doSomething(). Предположим далее, что у каждого класса есть синхронизированный метод, принимающий экземпляр другого класса в качестве параметра.

public class Foo {

    synchronized void doSomething() {
        //code to doSomething
    }

    synchronized void doSomethingWithBar(Bar b) {
        b.doSomething();
    }

}

public class Bar {

    synchronized void doSomething() {
        //code to doSomething
    }

    synchronized void doSomethingWithFoo(Foo f) {
        f.doSomething();
    }
}

Вы можете видеть, что если у вас есть экземпляр Foo и экземпляр Bar, оба пытаются одновременно выполнить свои методы doSomethingWith* друг для друга, может произойти взаимоблокировка.

Чтобы вызвать тупик, вы можете вставить спящий режим в оба метода doSomethingWith* (используя Foo в качестве примера):

synchronized void doSomethingWithBar(Bar b) {
    try {
        Thread.sleep(10000);
    } catch (InterruptedException ie) {
        ie.printStackTrace();
    }

    b.doSomething();
}

В вашем методе main вы запускаете два потока для завершения примера:

public static void main(String[] args) {
    final Foo f = new Foo();
    final Bar b = new Bar();
    new Thread(new Runnable() {
        public void run() {
            f.doSomethingWithBar(b);
        }
    }).start();

    new Thread(new Runnable() {
        public void run() {
            b.doSomethingWithFoo(f);
        }
    }).start();
}
27 голосов
/ 11 июня 2009

Конечно - производительность. Мониторы имеют стоимость.

Ответ не является ни удалением, ни добавлением синхронизации случайным образом. Лучше почитать что-нибудь вроде книг Брайана Гетца «Параллелизм на практике на практике» или книги Дуга Ли по «Нити Java», чтобы понять, как это сделать правильно. И, конечно, хорошо изучите новые параллельные пакеты.

Многопоточность - это намного больше, чем синхронизированное ключевое слово.

13 голосов
/ 11 июня 2009

Если вы думаете, что повсеместно использовать ключевое слово «synchronized» - это хорошее решение, даже игнорируя производительность, то вы точно не понимаете, что происходит, и вам не следует его использовать. Я настоятельно рекомендую прочитать тему, прежде чем углубляться в нее без указания направления.

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

7 голосов
/ 11 июня 2009

Точка исполнения уже указана.

Кроме того, помните, что все потоки получают разные копии стека. Поэтому, если метод использует только переменные, созданные внутри этого метода, и нет доступа к внешнему миру (например, файловым дескрипторам, сокетам, соединениям с БД и т. Д.), Тогда нет возможности возникновения проблем с многопоточностью.

6 голосов
/ 11 июня 2009

Намного лучше, чем помещать synchronized везде, - это тщательно продумать, какие инварианты нужны вашим классам, а затем синхронизировать их настолько, чтобы обеспечить эти инварианты. Если вы чрезмерно синхронизируете, вы создаете два риска:

  1. тупиковый
  2. проблемы с живостью

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

Если вы хотите использовать ключевое слово везде для безопасности, тогда я рекомендую использовать final вместо synchronized. :) Все, что вы можете сделать final, способствует лучшей безопасности потоков и упрощает рассуждения о том, что инварианты должны поддерживаться замками. Для примера, скажем, у вас есть этот тривиальный класс:

public class SimpleClass {
    private volatile int min, max;
    private volatile Date startTime;
    // Assume there are many other class members

    public SimpleClass(final int min, final int max) {
        this.min = min;
        this.max = max;
    }
    public void setMin(final int min) {
        // set min and verify that min <= max
    }
    public void setMax(final int max) {
        // set max and verify that min <= max
    }
    public Date getStartTime() {
        return startTime;
    }
}

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

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

2 голосов
/ 11 июня 2009

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

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

Для начала лучше всего использовать рабочие потоки. Изолируйте определенные долгосрочные задачи так, чтобы отдельные потоки занимали один блок неизменяемого ввода и - когда закончите - ставили результат в очередь сообщений основного потока (это, конечно, должно быть синхронизировано). Основной поток может опрашивать или явно ждать сигнала, если ему больше нечего делать. Конкретные детали зависят от ваших требований к производительности и дизайна приложения.

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

2 голосов
/ 11 июня 2009

Просто это почти наверняка замедлит ход событий.

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

Это занимает время. Вы, вероятно, не заметите функциональных изменений, но, если производительность важна, вам нужно остерегаться.

1 голос
/ 11 июня 2009

Допустим, у вас есть этот пример


synchronized (lock){
    doSomething();
}

Если у вас заблокировано несколько потоков, вы не можете их прервать. Лучше использовать Lock объекты вместо "синхронизированных", потому что у вас есть лучший контроль над прерываниями, которые могут произойти. Конечно, как и все предложенные, есть также некоторые проблемы с производительностью при «синхронизированном», и если вы злоупотребите ими, вы можете иметь взаимоблокировки, livelocks и т. Д.

0 голосов
/ 13 июня 2010

Если производительность не слишком большая проблема, почему бы вам не использовать один поток, а затем безопасно удалить все «синхронизированные»? , но гораздо более сложным и подверженным ошибкам способом.

Спросите себя, почему вы используете более одного потока?

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