Подтвердить что ты не робот

Выполнение синхронизации раздела в Java

У меня был небольшой спор о производительности синхронизированного блока в Java. Это теоретический вопрос, который не влияет на приложение реальной жизни. Рассмотрим однопоточное приложение, в котором используются блокировки и синхронизация разделов. Этот код работает медленнее, чем тот же код без синхронизации разделов? Если да, то почему? Мы не обсуждаем concurrency, так как его единственное приложение с одним потоком

Upd

Нашел интересный тест, тестирующий его. Но это с 2001 года. В последней версии JDK ситуация может сильно измениться.

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

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

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

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

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

foo++;
bar++;

против

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

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

(Отличная книга по модели памяти Java - Brian Goetz Java Concurrency На практике.)

Ответ 3

Есть некоторые накладные расходы при приобретении неконкурентоспособной блокировки, но на современных JVM это очень мало.

Ключевая оптимизация времени выполнения, относящаяся к этому случаю, называется "Предвзятое блокирование" и объясняется в Java SE 6 Performance White Paper.

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

Ответ 4

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

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

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% случаев использования ясность кода важнее производительности. Ясный, простой код часто также неплохо работает.

BTW: Я использую 4.6GHz 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

Ответ 5

Этот примерный код (с 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);
        }
    }
}

Ответ 6

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