Guia para sun.misc.Unsafe

Guia parasun.misc.Unsafe

1. Visão geral

Neste artigo, daremos uma olhada em uma classe fascinante fornecida pelo JDK -Unsafe do pacotesun.misc. Esta classe fornece mecanismos de baixo nível que foram projetados para serem usados ​​apenas pela biblioteca Java principal e não pelos usuários padrão.

Isso nos fornece mecanismos de baixo nível projetados principalmente para uso interno nas bibliotecas principais.

2. Obtendo uma instância deUnsafe

Em primeiro lugar, para podermos usar a classeUnsafe, precisamos obter uma instância - o que não é direto, visto que a classe foi projetada apenas para uso interno.

The way to obtain the instance is via the static method getUnsafe(). A ressalva é que, por padrão - isso lançará umSecurityException.

Felizmente,we can obtain the instance using reflection:

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);

3. Instanciando uma classe usandoUnsafe

Digamos que temos uma classe simples com um construtor que define um valor de variável quando o objeto é criado:

class InitializationOrdering {
    private long a;

    public InitializationOrdering() {
        this.a = 1;
    }

    public long getA() {
        return this.a;
    }
}

Quando inicializamos esse objeto usando o construtor, o métodogetA() retornará um valor de 1:

InitializationOrdering o1 = new InitializationOrdering();
assertEquals(o1.getA(), 1);

Mas podemos usar o métodoallocateInstance() usandoUnsafe.. Ele apenas alocará a memória para nossa classe e não invocará um construtor:

InitializationOrdering o3
  = (InitializationOrdering) unsafe.allocateInstance(InitializationOrdering.class);

assertEquals(o3.getA(), 0);

Observe que o construtor não foi chamado e, devido a esse fato, o métodogetA() retornou o valor padrão para o tipolong - que é 0.

4. Alterando Campos Privados

Digamos que temos uma classe que contém um valor privadosecret:

class SecretHolder {
    private int SECRET_VALUE = 0;

    public boolean secretIsDisclosed() {
        return SECRET_VALUE == 1;
    }
}

Usando o métodoputInt() deUnsafe,, podemos alterar um valor do campo privadoSECRET_VALUE, alterando / corrompendo o estado dessa instância:

SecretHolder secretHolder = new SecretHolder();

Field f = secretHolder.getClass().getDeclaredField("SECRET_VALUE");
unsafe.putInt(secretHolder, unsafe.objectFieldOffset(f), 1);

assertTrue(secretHolder.secretIsDisclosed());

Uma vez que obtemos um campo pela chamada de reflexão, podemos alterar seu valor para qualquer outro valorint usandoUnsafe.

5. Lançando uma exceção

O código que é chamado por meio deUnsafe não é examinado pelo compilador da mesma maneira que o código Java normal. Podemos usar o métodothrowException() para lançar qualquer exceção sem restringir o chamador para lidar com essa exceção, mesmo se for uma exceção verificada:

@Test(expected = IOException.class)
public void givenUnsafeThrowException_whenThrowCheckedException_thenNotNeedToCatchIt() {
    unsafe.throwException(new IOException());
}

Depois de lançar umIOException, que é verificado, não precisamos capturá-lo nem especificá-lo na declaração do método.

6. Memória Off-Heap

Se um aplicativo estiver ficando sem memória disponível na JVM, poderemos acabar forçando o processo do GC a executar com muita frequência. Idealmente, gostaríamos de uma região de memória especial, fora da pilha e não controlada pelo processo do GC.

O métodoallocateMemory() da classeUnsafe nos dá a capacidade de alocar objetos enormes fora do heap, o que significa quethis memory will not be seen and taken into account by the GC and the JVM.

Isso pode ser muito útil, mas precisamos lembrar que essa memória precisa ser gerenciada manualmente e recuperada corretamente comfreeMemory() quando não for mais necessária.

Digamos que queremos criar uma grande matriz de bytes de memória fora do heap. Podemos usar o métodoallocateMemory() para conseguir isso:

class OffHeapArray {
    private final static int BYTE = 1;
    private long size;
    private long address;

    public OffHeapArray(long size) throws NoSuchFieldException, IllegalAccessException {
        this.size = size;
        address = getUnsafe().allocateMemory(size * BYTE);
    }

    private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        return (Unsafe) f.get(null);
    }

    public void set(long i, byte value) throws NoSuchFieldException, IllegalAccessException {
        getUnsafe().putByte(address + i * BYTE, value);
    }

    public int get(long idx) throws NoSuchFieldException, IllegalAccessException {
        return getUnsafe().getByte(address + idx * BYTE);
    }

    public long size() {
        return size;
    }

    public void freeMemory() throws NoSuchFieldException, IllegalAccessException {
        getUnsafe().freeMemory(address);
    }
}

No construtor deOffHeapArray,, estamos inicializando a matriz de um determinadosize.. Estamos armazenando o endereço inicial da matriz no campoaddress. O métodoset() está pegando o índice e osvalue fornecidos que serão armazenados na matriz. O métodoget() está recuperando o valor do byte usando seu índice que é um deslocamento do endereço inicial da matriz.

Em seguida, podemos alocar essa matriz fora do heap usando seu construtor:

long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
OffHeapArray array = new OffHeapArray(SUPER_SIZE);

Podemos colocar N números de valores de bytes nessa matriz e depois recuperar esses valores, resumindo-os para testar se nosso endereçamento funciona corretamente:

int sum = 0;
for (int i = 0; i < 100; i++) {
    array.set((long) Integer.MAX_VALUE + i, (byte) 3);
    sum += array.get((long) Integer.MAX_VALUE + i);
}

assertEquals(array.size(), SUPER_SIZE);
assertEquals(sum, 300);

No final, precisamos liberar a memória de volta para o sistema operacional chamandofreeMemory().

7. CompareAndSwap Operação

As construções muito eficientes do pacotejava.concurrent, comoAtomicInteger,, estão usando os métodoscompareAndSwap() deUnsafe abaixo, para fornecer o melhor desempenho possível. Essa construção é amplamente usada nos algoritmos sem bloqueio que podem alavancar a instrução do processador CAS para fornecer grande aceleração em comparação com o mecanismo de sincronização pessimista padrão em Java.

Podemos construir o contador baseado em CAS usando o métodocompareAndSwapLong() deUnsafe:

class CASCounter {
    private Unsafe unsafe;
    private volatile long counter = 0;
    private long offset;

    private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        return (Unsafe) f.get(null);
    }

    public CASCounter() throws Exception {
        unsafe = getUnsafe();
        offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
    }

    public void increment() {
        long before = counter;
        while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
            before = counter;
        }
    }

    public long getCounter() {
        return counter;
    }
}

No construtorCASCounter estamos obtendo o endereço do campo contador, para podermos usá-lo posteriormente no métodoincrement(). Esse campo precisa ser declarado como volátil, para estar visível a todos os threads que estão escrevendo e lendo esse valor. Estamos usando o métodoobjectFieldOffset() para obter o endereço de memória do campooffset.

A parte mais importante desta classe é o métodoincrement(). Estamos usandocompareAndSwapLong() no loopwhile para incrementar o valor buscado anteriormente, verificando se o valor anterior mudou desde que o buscamos.

Se isso acontecer, estamos tentando novamente essa operação até conseguirmos. Não há bloqueio aqui, e é por isso que isso é chamado de algoritmo sem bloqueio.

Podemos testar nosso código incrementando o contador compartilhado de vários threads:

int NUM_OF_THREADS = 1_000;
int NUM_OF_INCREMENTS = 10_000;
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
CASCounter casCounter = new CASCounter();

IntStream.rangeClosed(0, NUM_OF_THREADS - 1)
  .forEach(i -> service.submit(() -> IntStream
    .rangeClosed(0, NUM_OF_INCREMENTS - 1)
    .forEach(j -> casCounter.increment())));

Em seguida, para afirmar que o estado do contador é adequado, podemos obter o valor do contador:

assertEquals(NUM_OF_INCREMENTS * NUM_OF_THREADS, casCounter.getCounter());

8. Park/Unpark

Existem dois métodos fascinantes na APIUnsafe que são usados ​​pela JVM para alternar encadeamentos de contexto. Quando o encadeamento está aguardando alguma ação, a JVM pode bloquear esse encadeamento usando o métodopark() da classeUnsafe.

É muito semelhante ao métodoObject.wait(), mas está chamando o código do sistema operacional nativo, tirando proveito de algumas especificações da arquitetura para obter o melhor desempenho.

When the thread is blocked and needs to be made runnable again, the JVM uses the unpark() method. Frequentemente veremos essas invocações de método em despejos de thread, especialmente nos aplicativos que usam pools de thread.

9. Conclusão

Neste artigo, examinamos a classeUnsafe e sua construção mais útil.

Vimos como acessar campos privados, como alocar memória fora da pilha e como usar a construção de comparar e trocar para implementar algoritmos sem bloqueio.

A implementação de todos esses exemplos e trechos de código pode ser encontradaover on GitHub - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.