Páginas

segunda-feira, 4 de agosto de 2014

Números Aleatórios em Java

Nesta postagem vamos ver alguns métodos de geração de números aleatórios ou randômicos em Java (o termo randômico é correto mas prefiro aleatório), entender alguns termos relacionados ao assunto, como pseudoaleatório e determinístico, verificar alguns conceitos de previsibilidade e segurança envolvidos nestes valores gerados e algumas formas de geração de valores dependendo do objetivo.

Algoritmos pseudoaleatórios

Em Java nós temos geradores de números pseudoaleatórios, mas o que significa isso? Significa que uma sequência de números gerados por estes algoritmos aparentam ser aleatórios mas não são, na verdade eles são baseados em um algoritmo que possui uma lógica que gera números em uma sequência extraída de uma valor inicial, a semente.

Semente (seed)

Todos algoritmos de geração pseudoaleatória partem de um conjunto de valores iniciais, chamado semente, para realizar os cálculos de obtenção de cada número gerado em uma sequência determinística.

Determinístico

Os algoritmos pseudoaleatórios são determinísticos pois, conhecendo-se o algoritmo e a semente utilizada é possível replicar (determinar) uma sequência de valores gerada previamente. Ou, dependendo do caso, conhecendo-se o algoritmo e a uma parte da sequência é possível encontrar a semente através de ataque de força bruta.

RNG - Random Number Generator ou Gerador de Números Aleatórios

Os "fabricantes" de números aleatórios. Em Java é uma classe que implemente um algoritmo gerador de valores aleatórios. Passaremos a utilizar somente RNG daqui em diante.

java.lang.Math.random

Dentro da classe utilitária java.lang.Math existe o método estático random que é capaz de gerar números pseudoaleatórios do tipo double compreendidos entre 0,0 (inclusive) e 1,0. Este método de geração de números aleatórios é o método mais utilizado, principalmente por iniciantes na linguagem, mas também por ser mais "simples" de trabalhar.
Na primeira chamada ao método Math.random será criada uma instância estática de um RNG padrão (new Random()) com semente baseada no momento da execução (System.currentTimeMillis()), e após isso, todas as chamadas usarão o mesmo RNG.
Este método de geração tem a vantagem de manter apenas uma instância de um RNG durante toda a execução do seu sistema sem a necessidade de implementações adicionais. Mas as chamadas são sincronizadas, o que leva a degradação de performance em sistemas multithread que necessitem realizar chamadas concorrentes e em grande volume.
public class NumerosAleatorios {

  public static void main(String[] args) {
    // Gerando números reais de 0,0 até 0,999999...
    for(int i = 0; i < 50; i++) {
      System.out.print(Math.random() + " ");
    }
    System.out.print("\n");
    // Gerando números inteiros de 15 até 24
    for(int i = 0; i < 50; i++) {
      int n = (int)(Math.random() * 10) + 15;
      System.out.print(n + " ");
    }
  }
}

java.util.Random

Como vimos acima, o RNG padrão do Java é obtido através da classe Random. Ao trabalharmos diretamente com esta classe temos acesso a diversos métodos de geração de valores aleatórios, bem como a possibilidade de definir a semente. A chamada Math.random é um atalho para Random.nextDouble que retornará um valor double, mas podemos obter, também, valores inteiros e boolean entre outros.
Como esta classe permite que possamos definir ou redefinir a semente o teste que iremos ver logo abaixo, sempre que executado, retornará o mesmo resultado, exatamente a mesma sequência de valores, expondo assim o determinismo do algoritmo de uma forma mais clara.
public class NumerosAleatorios {

  public static void main(String[] args) {
    Random rng;

    rng = new Random(1234567890);
    // Gerando uma sequência de números de 0 até 19
    for(int i = 0; i < 50; i++) {
      System.out.print(rng.nextInt(20) + " ");
    }

    System.out.print("\n");
    rng.setSeed(1234567890);
    // Gerando a mesma sequência de números de 0 até 19
    for(int i = 0; i < 50; i++) {
      System.out.print(rng.nextInt(20) + " ");
    }
  }
}
Como pode ser observado, este método possui uma forma muito mais intuitiva de geração de números inteiros dentro de um conjunto pré definido, e por questões de clareza é preferível ao método anterior. Mas é importante lembrar de criar os controles para a manutenção da instância do seu RNG, ou seja, não é recomendável criar um RNG a cada vez que precisar gerar um valor aleatório.
E, da mesma forma que no método anterior, caso seu sistema faça uso intenso de multithreading e geração aleatória a sua instância do RNG pode ficar sobrecarregada e gerar contenção, mas isto é facilmente contornável com a manutenção de mais de uma instância de RNG, o que não é possível com o uso de Math.random.

java.util.concurrent.ThreadLocalRandom

Caso seu sistema faça uso de multithread e geração aleatória é possível fazer uso desta classe especial que o Java fornece a partir da versão 1.7. Resumidamente, este classe é uma instância da classe Random que é isolada em uma thread, que não pode ser instanciada e nem pode ter sua semente definida. Para fazer uso dela basta chamar ThreadLocalRandom.current() e o seu método preferido.

java.security.SecureRandom

Esta classe respeita alguns conceitos de segurança criptográfica e tenta obter uma sequência não determinística e forte o bastante. O RNG obtido por esta classe vai depender da implementação do runtime Java e poderá fazer uso de recursos criptográficos do sistema operacional ou do hardware onde está sendo executado, por exemplo, no Linux o runtime poderá utilizar o gerador do SO baseado na chamada /dev/random.

Valores aleatórios versus Segurança

Tudo depende do objetivo que os valores aleatórios são necessários. Para sistemas simples, onde algum nível de aleatoriedade é requisito, como em jogos, e a previsibilidade destes valores não afeta o comportamento geral dos usuários, então os métodos não seguros são suficientes. Já em sistemas onde a previsibilidade dos valores é uma fator essencial na segurança, então um método seguro deve ser utilizado.
Mas não fique tentado a utilizar apenas métodos seguros, pois esta segurança tem um preço, e o preço é no desempenho, métodos que dependam do SO podem sofrer contenção fora do controle do seu sistema.
Bem, meu objetivo é um entendimento geral dos recursos, então, caso você precise de geração em nível criptográfico recomendo uma leitura mais aprofundada nos artigos disponíveis na internet ou em livros, mas algumas dicas a serem seguidas:
- Utilize a classe SecureRandom com o Java mais recente possível;
- Redefina a semente com alguma frequência, ou por tempo ou valores obtidos;
- Redefina a semente com valores de difícil previsão;
- Não armazene rastros que possam levar às sementes, como logs.
Obs.: Utilize a classe SecureRandom somente se for essencial, as outras dicas acima também podem ser utilizadas com a classe Random.
Para exemplificar, vai um código de um gerador de inteiros com redefinição de semente baseado em tempo.
import java.nio.ByteBuffer;
import java.security.SecureRandom;

public class SecureRNG {

  private static SecureRandom sRNG = new SecureRandom();
  private static long lastTimeSeeded = 0;
  private static final long SEED_TIMEOUT = 10000; // reseed após 10 segundos

  // A redefinição da semente será feita na primeira chamada após o timeout definido.
  // O momento exato é de difícil previsão, vai depender do código e, normalmente, de uma ação do usuário.
  private synchronized static void reseed() {
    lastTimeSeeded = System.currentTimeMillis();
    // nanoTime é de difícil previsão pois trabalha com aproximação
    final long nanoTime = System.nanoTime();
    // memória livre para a JRE é de difícil previsão pois depende do ambiente e varia de momento para momento
    final long freeMemory = Runtime.getRuntime().freeMemory();

    // monta um array de 16 bytes com os valores anteriores
    final byte[] seed = ByteBuffer.allocate(16).putLong(nanoTime).putLong(freeMemory).array();

    // a redefinição da semente na classe SecureRandom trabalha de forma adicional e não por substituição
    sRNG.setSeed(seed);
  }

  public synchronized static int getInt(final int n) {
    if(System.currentTimeMillis() > (lastTimeSeeded + SEED_TIMEOUT)) {
      reseed();
    }

    return sRNG.nextInt(n);
  }

}

Nenhum comentário:

Postar um comentário

Olá! Antes de postar seu comentário, por favor, observe que comentários técnicos, elogios e sugestões são antecipadamente agradecidos, esclarecimentos sobre os conceitos envolvidos na postagem serão respondidos da melhor forma possível, mas pedidos de ajuda técnica ou suporte individual deverão ser feitos através do formulário de contato. Grato!