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

Синхронизация по локальным переменным

Сегодня я столкнулся с методом constructServiceUrl() класса org.jasig.cas.client.util.CommonUtils. Я думал, что он очень странный:

final StringBuffer buffer = new StringBuffer();

synchronized (buffer)
{
    if (!serverName.startsWith("https://") && !serverName.startsWith("http://"))
    {
        buffer.append(request.isSecure() ? "https://" : "http://");
    }

    buffer.append(serverName);
    buffer.append(request.getRequestURI());

    if (CommonUtils.isNotBlank(request.getQueryString()))
    {
        final int location = request.getQueryString().indexOf(
                artifactParameterName + "=");

        if (location == 0)
        {
            final String returnValue = encode ? response.encodeURL(buffer.toString()) : buffer.toString();

            if (LOG.isDebugEnabled())
            {
                LOG.debug("serviceUrl generated: " + returnValue);
            }

            return returnValue;
        }

        buffer.append("?");

        if (location == -1)
        {
            buffer.append(request.getQueryString());
        }
        else if (location > 0)
        {
            final int actualLocation = request.getQueryString()
                    .indexOf("&" + artifactParameterName + "=");

            if (actualLocation == -1)
            {
                buffer.append(request.getQueryString());
            }
            else if (actualLocation > 0)
            {
                buffer.append(request.getQueryString().substring(0, actualLocation));
            }
        }
    }
}

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

4b9b3361

Ответ 1

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

Рассмотрим эти два фрагмента:

StringBuffer b = new StringBuffer();
for(int i = 0 ; i < 100; i++){
    b.append(i);
}

против

StringBuffer b = new StringBuffer();
synchronized(b){
  for(int i = 0 ; i < 100; i++){
     b.append(i);
  }
}

В первом случае StringBuffer должен получить и освободить блокировку 100 раз (потому что append - это синхронизированный метод), тогда как во втором случае блокировка приобретается и освобождается только один раз. Это может дать вам повышение производительности и, вероятно, поэтому автор сделал это. В некоторых случаях компилятор может выполнить эту блокировку укрупнения для вас (но не вокруг циклов конструкций, потому что вы можете в конечном итоге удерживать блокировку в течение длительных периодов времени время).

Кстати, компилятор может обнаружить, что объект не "ускользает" от метода, и поэтому удаляет процесс эквайринга и освобождения блокировок объекта (блокировка elision), так как ни один другой поток не может получить доступ к объекту в любом случае. Эта работа была выполнена в JDK7.


Update:

Я провел два быстрых теста:

1) БЕЗ ПРЕДОХРАНИТЕЛЯ:

В этом тесте я несколько раз не запускал методы для "разминки" JVM. Это означает, что компилятор Java Hotspot Server Compiler не получил возможности оптимизировать код, например. путем исключения блокировок для экранирующих объектов.

JDK                1.4.2_19    1.5.0_21    1.6.0_21    1.7.0_06
WITH-SYNC (ms)         3172        1108        3822        2786
WITHOUT-SYNC (ms)      3660         801         509         763
STRINGBUILDER (ms)      N/A         450         434         475

С JDK 1.4 код с внешним синхронизированным блоком выполняется быстрее. Однако с JDK 5 и выше выигрывает код без внешней синхронизации.

2) С WARM-UP:

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

JDK                1.4.2_19    1.5.0_21    1.6.0_21    1.7.0_06
WITH-SYNC (ms)         3190         614         565         587
WITHOUT-SYNC (ms)      3593         779         563         610
STRINGBUILDER (ms)      N/A         450         434         475

Еще раз, с JDK 1.4, код с внешним синхронизированным блоком выполняется быстрее. Однако с JDK 5 и выше оба метода работают одинаково хорошо.

Вот мой тестовый класс (не стесняйтесь улучшаться):

public class StringBufferTest {

    public static void unsync() {
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < 9999999; i++) {
            buffer.append(i);
            buffer.delete(0, buffer.length() - 1);
        }

    }

    public static void sync() {
        StringBuffer buffer = new StringBuffer();
        synchronized (buffer) {
            for (int i = 0; i < 9999999; i++) {
                buffer.append(i);
                buffer.delete(0, buffer.length() - 1);
            }
        }
    }

    public static void sb() {
        StringBuilder buffer = new StringBuilder();
        synchronized (buffer) {
            for (int i = 0; i < 9999999; i++) {
                buffer.append(i);
                buffer.delete(0, buffer.length() - 1);
            }
        }
    }    

    public static void main(String[] args) {

        System.out.println(System.getProperty("java.version"));

        // warm up
        for(int i = 0 ; i < 10 ; i++){
            unsync();
            sync();
            sb();
        }

        long start = System.currentTimeMillis();
        unsync();
        long end = System.currentTimeMillis();
        long duration = end - start;
        System.out.println("Unsync: " + duration);

        start = System.currentTimeMillis();
        sync();
        end = System.currentTimeMillis();
        duration = end - start;
        System.out.println("sync: " + duration);

        start = System.currentTimeMillis();
        sb();
        end = System.currentTimeMillis();
        duration = end - start;
        System.out.println("sb: " + duration);  
    }
}

Ответ 2

Неопытность, некомпетентность или, скорее, мертвый, но доброкачественный код, который остается после рефакторинга.

Вы правы, чтобы подвергнуть сомнению ценность этого - современные компиляторы будут использовать escape-анализ, чтобы определить, что объект, о котором идет речь, не может ссылаться на другой поток, и поэтому полностью исключает (удаляет) синхронизацию.

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

Ответ 3

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

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

Ответ 4

Это немного безумное... это ничего не делает, кроме добавления служебных данных. Не говоря уже о том, что вызовы StringBuffer уже синхронизированы, поэтому StringBuilder предпочтительнее для случаев, когда вы не будете иметь несколько потоков, обращающихся к одному экземпляру.

Ответ 5

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