Sincronismo com Thread

Estou implementando um programa que usa Threads para calcular o número de valores primos presente numa matriz, a intenção é usar uma variável global que seja acessível por qualquer thread e que isso cause uma inconsistência já que varias threads irão acessar ao mesmo tempo a mesma variável. Então por conta disso eu teria de implementar uma seção crítica com o Synchronized por exemplo, pare ele limitar uma thread por vez ao acesso a variável local. Porém acontece que mesmo sem o método Synchronized meu programa NÃO apresenta inconsistência, o que é contra intuitivo, gostaria que alguém me esclarecesse o que está havendo.

O programa precisa ser iniciado com alguns valores através do próprio menu:

  • o tamanho da matriz (opção 1) = 10
  • preenchimento automático da matriz (opção 2) = gera sempre a mesma matriz com 31 números primos
  • número de Threads (opção 3) = 4
  • iniciar criação de threads (opção 4)

Classe ThreadRunnable.java:

package com.threads.implementacao;

import java.util.logging.Level;
import java.util.logging.Logger;

public class ThreadRunnable implements Runnable {

    public static int somaNumeroDePrimos = 0;
    private int iInicial, jInicial, iFinal, jFinal;

    public ThreadRunnable(int iInicial, int jInicial, int iFinal, int jFinal) {
        this.iInicial = iInicial;
        this.jInicial = jInicial;
        this.iFinal = iFinal;
        this.jFinal = jFinal;

        Thread t = new Thread(this); // mesmo que Thread t = new Thread(new ThreadRunnable ()); fora dessa classe
        t.start();
    }

    @Override
    public void run() {
        int i = iInicial, j = jInicial;
      
        while(i != iFinal || j != jFinal+1){

            if(j == ThreadExecute.TAM){
                j = 0;
                i ++;
            }

            //synchronized(ThreadRunnable.class){
                if (isPrimo(ThreadExecute.matrizDeNumeros.get(i).get(j)))
                    somaNumeroDePrimos++;
            //}
            j ++;
        } 
    }

    public boolean isPrimo(int numero) {

        if (numero <= 1)
            return false;

        for (int divisor = 2; Math.pow(divisor, 2) <= numero; divisor++)
            if (numero % divisor == 0)
                return false; // se achar algum divisor menor ou igual do que a raiz quadrada do proprio
                              // número, entao nao é primo

        return true;
    }

    public static int getQuantidadeDePrimos(){
        return somaNumeroDePrimos;
    }
}

ThreadExecute.java:

package com.threads.implementacao;

import java.util.ArrayList;
import java.util.Random;
import java.util.Scanner;

public class ThreadExecute {

    public static int TAM = 0;
    public static int numeroDeThreads = 0;
    public static ArrayList<ArrayList<Integer>> matrizDeNumeros;

    private static final int rangeNumerosAleatorios = 11; //0 - 10
    private static final Scanner in = new Scanner(System.in);
    private static int somaNumeroDePrimos = 0;

    public static void menu() {
        int opcao = 0;

        while (opcao != 6) {

            System.out.println("1 - Definir tamanho da matriz");
            System.out.println("2 - Preencher a matriz com numeros aleatorios");
            System.out.println("3 - Definir número de Threads");
            System.out.println("4 - Executar");
            System.out.println("5 - Vizualizar quantidade de números primos e tempo de execução");
            System.out.println("6 - Encerrar");

            opcao = in.nextInt();

            switch (opcao) {

                case 1:
                    definirTamanhoDaMatriz();
                    break;

                case 2:
                    preencherMatriz();
                    imprimirMatriz();
                    break;

                case 3:
                    definirNumeroDeThreads();
                    break;

                case 4:
                    criarThreads();
                    break;

                case 5:
                    numerosPrimosETempo();
                    break;

                case 6:
                    break;

                default:
                    System.out.println("Opção inválida");
                    break;

            }
        }

    }

    public static void numerosPrimosETempo(){
        System.out.println("A matriz possui = " + ThreadRunnable.getQuantidadeDePrimos());
    }

    public static void definirTamanhoDaMatriz() {
        System.out.println("Informe o tamanho da matriz sendo que a matriz terá o formato TAMxTAM:");
        TAM = in.nextInt();
    }

    public static void preencherMatriz() {
        Random numero = new Random(2); //inicia semente para sempre gerar a mesma matriz
        ArrayList<Integer> linha;

        if (TAM != 0) {
            matrizDeNumeros = new ArrayList<ArrayList<Integer>>(TAM);
            for (int i = 0; i < TAM; i++) {
                linha = new ArrayList<Integer>(); 

                for (int j = 0; j < TAM; j++)
                    linha.add(numero.nextInt(rangeNumerosAleatorios));

                matrizDeNumeros.add(linha);
            }
        } else
            System.out.println("Tamanho da matriz não definida");
    }

    public static void definirNumeroDeThreads() {
        System.out.println("Informe o numero de Threads que se deseja dividir a matriz:");
        numeroDeThreads = in.nextInt();
    }

    public static void criarThreads() {
        float iInicial, jInicial, iFinal, jFinal, indice;
        float numerosPorThreads;

        if (numeroDeThreads != 0 && matrizDeNumeros.size() != 0) {
            numerosPorThreads = (TAM * TAM) / numeroDeThreads;

            for (int n = 0; n < numeroDeThreads; n++) {
                // converte um dos termos para float pois divisão entre inteiros sempre retorna
                // inteiro
                iInicial = (n * numerosPorThreads) / TAM;
                jInicial = (n * numerosPorThreads) % TAM;

                indice = ((n + 1) * numerosPorThreads) / TAM;
                iFinal = ((indice * 10) % 10) == 0 ? (int)indice - 1 : (int)indice;

                indice = ((n + 1) * numerosPorThreads) % TAM;
                jFinal = indice == 0 ? TAM - 1 : (int)indice - 1;
                // converte para indices inteiros
                ThreadRunnable thread = new ThreadRunnable((int)iInicial, (int)jInicial, (int)iFinal, (int)jFinal);
            }

        } else
            System.out.println("Numero de Threads não definida ou matriz não preenchida");

    }

    public static void imprimirMatriz() {
        for (int i = 0; i < matrizDeNumeros.size(); i++) {
            for (int j = 0; j < matrizDeNumeros.get(i).size(); j++)
                System.out.print(matrizDeNumeros.get(i).get(j) + " ");

            System.out.println("");
        }
    }

    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName());
        menu();
    }

}

Como você tem certeza que não está dando inconsistência?

Não estou dizendo que esteja. Eu não tenho certeza, pois não lembro se somaNumeroDePrimos++ é uma operação atômica em Java (lembro que atribuições são, mas não sei se é o caso aqui e acho que isso afeta o resultado, pois, mesmo sendo atômicas, um número repetitivo de atribuições concorrentes pode eventualmente produzir uma inconsistência). É onde poderia dar problema, já que o restante dos cálculos envolve as regiões que estão particionadas para cada thread, portanto independentes entre si.

Creio que não é atômico, nesse caso não tem como saber se está dando inconsistência pois você não está conferindo o cálculo do resultado. Experimente introduzir um for de alguns milhares de iterações na criação e execução de threads que permita repetir o mesmo cálculo n vezes, e veja se em todas o resultado sempre é o mesmo.

Ou ainda, faça isso introduzindo também uma conferência do cálculo que seja executada de maneira single-thread, para ver se os dois resultados batem.

Do jeito que está não há como saber se em algum acesso concorrente está deixando de ocorrer algum incremento dessa variável, além do mais se ocorrer poderá ser esporádico e somente acontecer uma vez a cada trocentas repetições, como é característica de operações multithread.

Por fim, quando for descomentar o seu bloco synchronized, tenha em mente que ele deve ficar dentro do if e não fora.