Производительность раздела синхронизации в Java - PullRequest
32 голосов
/ 15 декабря 2011

У меня возник небольшой спор по поводу производительности блока synchronized в Java. Это теоретический вопрос, который не влияет на применение в реальной жизни.

Рассмотрим однопоточное приложение, которое использует блокировки и синхронизирует разделы. Этот код работает медленнее, чем тот же код без синхронизации разделов? Если так, то почему? Мы не обсуждаем параллелизм, поскольку это только однопоточное приложение

Обновление

Нашел интересный бенчмарк тестировал его. Но это с 2001 года. В последней версии JDK

все могло кардинально измениться

Ответы [ 6 ]

47 голосов
/ 15 декабря 2011

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

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

Таким образом, даже если вы свободны от конфликта блокировок, JVM все равно придется выполнять служебную работу при сброседанные в основную память.

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

foo++;
bar++;

против:

foo++;
synchronized(obj)
{
    bar++;
}

В первом примере компилятор может свободно загружаться foo и bar одновременно, затем увеличьте их оба, затем сохраните оба.Во втором примере компилятор должен выполнить загрузку / добавление / сохранение для foo, а затем выполнить загрузку / добавление / сохранение для bar.Таким образом, синхронизация может повлиять на способность JRE оптимизировать инструкции.

(Отличная книга по модели памяти Java - это Параллелизм Java на практике Брайана Гетца .)

33 голосов
/ 17 января 2013

В HotSpot есть 3 типа блокировки

  1. Толстый : JVM использует мьютексы ОС для получения блокировки.
  2. Тонкий : JVM использует алгоритм CAS.
  3. Смещено : CAS является довольно дорогой операцией на некоторых архитектурах. Смещенная блокировка - это особый тип блокировки, оптимизированный для сценария, когда над объектом работает только один поток.

По умолчанию JVM использует блокировку thin . Позже, если JVM определит отсутствие конкуренции, тонкая блокировка преобразуется в смещенную блокировку. Операция, которая изменяет тип блокировки, довольно дорогая, поэтому JVM не применяет эту оптимизацию немедленно. Существует специальная опция JVM - XX: BiasedLockingStartupDelay = delay , которая сообщает JVM, когда следует применять этот тип оптимизации.

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

Ответ на вопрос: это зависит. Но если смещение, однопоточный код с блокировкой и без блокировки имеет в среднем одинаковую производительность.

20 голосов
/ 15 декабря 2011

Существуют некоторые издержки при получении не оспариваемой блокировки, но на современных JVM она очень мала.

Оптимизация во время выполнения ключа, которая имеет отношение к этому случаю, называется «Смещенная блокировка» и объясняетсяв Java SE 6 Performance White Paper .

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

9 голосов
/ 15 декабря 2011

Использование блокировок, когда вам это не нужно, замедлит работу вашего приложения. Это может быть слишком мало для измерения или может быть удивительно высоким.

ИМХО Зачастую лучшим подходом является использование кода без блокировки в однопоточной программе, чтобы было ясно, что этот код не предназначен для совместного использования в потоке. Это может быть более важным для обслуживания, чем любые проблемы с производительностью.

public static void main(String... args) throws IOException {
    for (int i = 0; i < 3; i++) {
        perfTest(new Vector<Integer>());
        perfTest(new ArrayList<Integer>());
    }
}

private static void perfTest(List<Integer> objects) {
    long start = System.nanoTime();
    final int runs = 100000000;
    for (int i = 0; i < runs; i += 20) {
        // add items.
        for (int j = 0; j < 20; j+=2)
            objects.add(i);
        // remove from the end.
        while (!objects.isEmpty())
            objects.remove(objects.size() - 1);
    }
    long time = System.nanoTime() - start;
    System.out.printf("%s each add/remove took an average of %.1f ns%n", objects.getClass().getSimpleName(),  (double) time/runs);
}

печать

Vector each add/remove took an average of 38.9 ns
ArrayList each add/remove took an average of 6.4 ns
Vector each add/remove took an average of 10.5 ns
ArrayList each add/remove took an average of 6.2 ns
Vector each add/remove took an average of 10.4 ns
ArrayList each add/remove took an average of 5.7 ns

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

Для 99% случаев ясность кода важнее производительности. Ясный, простой код часто также работает достаточно хорошо.

Кстати: я использую 4,6 ГГц i7 2600 с Oracle Java 7u1.


Для сравнения, если я сделаю следующее, где perfTest1,2,3 идентичны.

    perfTest1(new ArrayList<Integer>());
    perfTest2(new Vector<Integer>());
    perfTest3(Collections.synchronizedList(new ArrayList<Integer>()));

Я получаю

ArrayList each add/remove took an average of 2.6 ns
Vector each add/remove took an average of 7.5 ns
SynchronizedRandomAccessList each add/remove took an average of 8.9 ns

Если я использую общий метод perfTest, он не может встроить код так оптимально, и все они медленнее

ArrayList each add/remove took an average of 9.3 ns
Vector each add/remove took an average of 12.4 ns
SynchronizedRandomAccessList each add/remove took an average of 13.9 ns

Смена порядка тестов

ArrayList each add/remove took an average of 3.0 ns
Vector each add/remove took an average of 39.7 ns
ArrayList each add/remove took an average of 2.0 ns
Vector each add/remove took an average of 4.6 ns
ArrayList each add/remove took an average of 2.3 ns
Vector each add/remove took an average of 4.5 ns
ArrayList each add/remove took an average of 2.3 ns
Vector each add/remove took an average of 4.4 ns
ArrayList each add/remove took an average of 2.4 ns
Vector each add/remove took an average of 4.6 ns

по одному

ArrayList each add/remove took an average of 3.0 ns
ArrayList each add/remove took an average of 3.0 ns
ArrayList each add/remove took an average of 2.3 ns
ArrayList each add/remove took an average of 2.2 ns
ArrayList each add/remove took an average of 2.4 ns

и

Vector each add/remove took an average of 28.4 ns
Vector each add/remove took an average of 37.4 ns
Vector each add/remove took an average of 7.6 ns
Vector each add/remove took an average of 7.6 ns
Vector each add/remove took an average of 7.6 ns
0 голосов
/ 06 апреля 2017

Этот пример кода (100 потоков делают по 1 000 000 итераций в каждом) демонстрирует разницу в производительности между избеганием и не избеганием синхронизированного блока.

Выход:

Total time(Avoid Sync Block): 630ms
Total time(NOT Avoid Sync Block): 6360ms
Total time(Avoid Sync Block): 427ms
Total time(NOT Avoid Sync Block): 6636ms
Total time(Avoid Sync Block): 481ms
Total time(NOT Avoid Sync Block): 5882ms

Код:

import org.apache.commons.lang.time.StopWatch;

public class App {
    public static int countTheads = 100;
    public static int loopsPerThead = 1000000;
    public static int sleepOfFirst = 10;

    public static int runningCount = 0;
    public static Boolean flagSync = null;

    public static void main( String[] args )
    {        
        for (int j = 0; j < 3; j++) {     
            App.startAll(new App.AvoidSyncBlockRunner(), "(Avoid Sync Block)");
            App.startAll(new App.NotAvoidSyncBlockRunner(), "(NOT Avoid Sync Block)");
        }
    }

    public static void startAll(Runnable runnable, String description) {
        App.runningCount = 0;
        App.flagSync = null;
        Thread[] threads = new Thread[App.countTheads];

        StopWatch sw = new StopWatch();
        sw.start();
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(runnable);
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
        do {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } while (runningCount != 0);
        System.out.println("Total time"+description+": " + (sw.getTime() - App.sleepOfFirst) + "ms");
    }

    public static void commonBlock() {
        String a = "foo";
        a += "Baa";
    }

    public static synchronized void incrementCountRunning(int inc) {
        runningCount = runningCount + inc;
    }

    public static class NotAvoidSyncBlockRunner implements Runnable {

        public void run() {
            App.incrementCountRunning(1);
            for (int i = 0; i < App.loopsPerThead; i++) {
                synchronized (App.class) {
                    if (App.flagSync == null) {
                        try {
                            Thread.sleep(App.sleepOfFirst);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        App.flagSync = true;
                    }
                }
                App.commonBlock();
            }
            App.incrementCountRunning(-1);
        }
    }

    public static class AvoidSyncBlockRunner implements Runnable {

        public void run() {
            App.incrementCountRunning(1);
            for (int i = 0; i < App.loopsPerThead; i++) {
                // THIS "IF" MAY SEEM POINTLESS, BUT IT AVOIDS THE NEXT 
                //ITERATION OF ENTERING INTO THE SYNCHRONIZED BLOCK
                if (App.flagSync == null) {
                    synchronized (App.class) {
                        if (App.flagSync == null) {
                            try {
                                Thread.sleep(App.sleepOfFirst);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            App.flagSync = true;
                        }
                    }
                }
                App.commonBlock();
            }
            App.incrementCountRunning(-1);
        }
    }
}
0 голосов
/ 15 декабря 2011

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

...