it-swarm-pt.tech

Desempenho da seção sincronizar em Java

Eu tive uma pequena disputa sobre o desempenho do bloco synchronized em Java. Esta é uma questão teórica, que não afeta a aplicação na vida real. 

Considere o aplicativo de encadeamento único, que usa bloqueios e sincroniza seções. Esse código funciona mais lento que o mesmo código sem sincronizar seções? Se sim, porque? Não discutimos a simultaneidade, pois é o único aplicativo de thread único

Atualizar

Achei interessante benchmark testando. Mas é de 2001. As coisas poderiam ter mudado dramaticamente na última versão do JDK

32
Anton

Existem 3 tipos de bloqueio no HotSpot

  1. Fat : A JVM depende de mutexes do sistema operacional para adquirir o bloqueio.
  2. Thin : JVM está usando o algoritmo CAS. 
  3. Tendenciosa : O CAS é uma operação bastante cara em algumas arquiteturas. Bloqueio com polarização - é um tipo especial de bloqueio otimizado para cenário quando apenas um thread está trabalhando no objeto.

Por padrão, a JVM usa thin locking. Posteriormente, se a JVM determinar que não há contenção, o bloqueio thin será convertido em polarizado locking. A operação que altera o tipo de bloqueio é bastante cara, portanto, a JVM não aplica essa otimização imediatamente. Há uma opção especial da JVM - XX: BiasedLockingStartupDelay = delay que informa à JVM quando esse tipo de otimização deve ser aplicado.

Uma vez influenciado, esse segmento pode subsequentemente bloquear e desbloquear o objeto sem recorrer a instruções atômicas caras. 

Responda à pergunta: depende. Mas, se for tendencioso, o único código encadeado com bloqueio e sem bloqueio terá o mesmo desempenho médio.

31
Anton

O código de encadeamento único ainda será executado mais lentamente ao usar blocos synchronized. Obviamente, você não terá outros threads paralisados ​​enquanto aguarda a conclusão de outros threads, no entanto, você terá que lidar com os outros efeitos da sincronização, ou seja, a coerência do cache.

Blocos sincronizados não são usados ​​apenas para simultaneidade , mas também visibility . Cada bloco sincronizado é uma barreira de memória: a JVM está livre para trabalhar em variáveis ​​em registradores, em vez de memória principal, na suposição de que vários encadeamentos não acessarão essa variável. Sem blocos de sincronização, esses dados poderiam ser armazenados no cache de uma CPU e threads diferentes em CPUs diferentes não veriam os mesmos dados. Usando um bloco de sincronização, você força a JVM a gravar esses dados na memória principal para visibilidade de outros encadeamentos.

Assim, mesmo que você esteja livre de contenção de bloqueio, a JVM ainda terá que fazer Housekeeping na limpeza de dados para a memória principal.

Além disso, isso tem restrições de otimização. A JVM está livre para reordenar instruções para fornecer otimização: considere um exemplo simples:

foo++;
bar++;

versus:

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

No primeiro exemplo, o compilador está livre para carregar foo e bar ao mesmo tempo, depois incrementa os dois e depois salva ambos. No segundo exemplo, o compilador deve executa o load/add/save em foo, então executa o load/add/save em bar. Assim, a sincronização pode afetar a capacidade do JRE de otimizar as instruções.

(Um excelente livro sobre o Modelo de Memória Java é o de Brian Goetz Java Concurrency In Practice .)

44
Edward Thomson

Existe alguma sobrecarga na aquisição de um bloqueio não contestado, mas em JVMs modernas é muito pequeno.

Uma chave de otimização de tempo de execução que é relevante para esse caso é chamada de "Bloqueio com Tendência" e é explicada no White Paper de Desempenho do Java SE 6 .

Se você quisesse ter alguns números de desempenho que fossem relevantes para a sua JVM e hardware, você poderia construir um micro-benchmark para tentar medir essa sobrecarga.

19
NPE

Usando bloqueios quando você não precisa diminuir a velocidade do seu aplicativo. Pode ser muito pequeno para medir ou pode ser surpreendentemente alto. 

IMHO Freqüentemente, a melhor abordagem é usar código livre de bloqueio em um único programa encadeado para deixar claro que esse código não deve ser compartilhado em encadeamento. Isso pode ser mais importante para manutenção do que qualquer problema de desempenho.

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);
}

impressões

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

Do ponto de vista do desempenho, se 4 ns for importante para você, você precisará usar a versão não sincronizada. 

Para 99% dos casos de uso, a clareza do código é mais importante que o desempenho. Códigos simples e claros geralmente são razoavelmente bons.

BTW: Estou usando um i7 2600 de 4,6 GHz com o Oracle Java 7u1.


Para comparação, se eu fizer o seguinte, onde perfTest1,2,3 são idênticos.

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

Eu recebo 

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

Se eu usar um método perfTest comum, ele não poderá alinhar o código da melhor forma e eles ficarão mais lentos

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

Trocando a ordem dos testes

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

um por vez

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

e

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
9
Peter Lawrey

Supondo que você esteja usando a VM HotSpot, acredito que a JVM é capaz de reconhecer que não há contenção para nenhum recurso dentro do bloco synchronized e tratá-lo como código "normal".

0
sworisbreathing

Este código de exemplo (com 100 threads produzindo 1.000.000 de iterações cada uma) demonstra a diferença de desempenho entre evitar e não evitar um bloco sincronizado.

Saída:

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

Código:

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
Hailton