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

Более того, многопоточность имеет гораздо более широкое применение чем просто ради ускорения вычислений, в частности многопоточность возможна и в случае одноядерных компьютеров, т.к. обычно вы хотите использовать несколько приложений параллельно (например браузер, файловый менеджер и блокнот), и хотя каждое из этих приложений является полноценной программой в которой код исполняется подряд - компьютер даже в случае одного ядра старается дать вам возможность кофмортно работать в разных приложениях одновременно. Это тоже достигается за счет многопоточности - каждое приложение работает в своем вычислительном “потоке” (на английском Thread, на русском так же встречается термин “нить”).

В Java все - объекты, поэтому для создания потоков - существует класс Thread.

Thread

Раньше мы наследовали MyPanel от JPanel чтобы рисовать в ней что хочется посредством переопределения метода paintComponent.

Так и сейчас мы наследуем MyThread от Thread чтобы выполнить какую-нибудь полезную вычислительную работу в отдельном потоке, на этот раз надо переопределить метод run (напоминаю что после указания что мы наследуемся от Thread, т.е. extends Thread, вы можете воспользоваться помощью IDEA - поставьте каретку внутри класса, и выполните Alt+Insert->Override Methods...->выберите run):

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread starts running...");
        for (int i = 0; i < 10; ++i) {
            System.out.println("  step #" + i);
        }
        System.out.println("MyThread finished!");
    }
}

Теперь давайте создадим объект нашего нового класса и запустим этот метод:

MyThread thread1 = new MyThread();
thread1.run();

Как и ожидалось вывод выглядит так:

MyThread starts running...
  step #0
  ...
  step #9
MyThread finished!

Давайте проверим что будет если создать два таких потока:

MyThread thread1 = new MyThread();
System.out.println("Thread1.run()...");
thread1.run();

MyThread thread2 = new MyThread();
System.out.println("Thread2.run()...");
thread2.run();

Получили:

Thread1.run()...
MyThread starts running...
  step #0
  ...
  step #9
MyThread finished!
Thread2.run()...
MyThread starts running...
  step #0
  ...
  step #9
MyThread finished!

Но вывод не выглядит параллельным! Строчки выводящиеся в консоль строго упорядочены а не перемешаны как следовало бы ожидать в случае многопоточного выполнения!

Это так потому что наш код все еще однопоточный, ведь мы из основного потока программы вызвали ничем не особенный метод run (пусть и методом класса с красивым названием “поток”).

Чтобы запустить выполнение в отдельном потоке - нужно вызвать уже реализованный в Thread метод start. И вот этот метод в свою очередь запустит отдельный поток, и этот поток уже у себя, уже параллельно от основной программы и других потоков выполнит наконец ваш метод run.

Т.е. вызов start это “запустить метод run в отдельном потоке”.

Давайте попробуем:

MyThread thread1 = new MyThread();
System.out.println("Thread1.start()...");
thread1.start();

MyThread thread2 = new MyThread();
System.out.println("Thread2.start()...");
thread2.start();

System.out.println("All threads are started!");

Получили примерно такой вывод (он от раза к разу может отличаться):

Thread1.start()...
Thread2.start()...
All threads are started!
MyThread starts running...
MyThread starts running...
  step #0
  step #1
  step #0
  step #1
  step #2
  step #3
  step #4
  step #5
  step #6
  step #7
  step #2
  step #8
  step #3
  step #4
  step #5
  step #9
  step #6
MyThread finished!
  step #7
  step #8
  step #9
MyThread finished!

Первыми строками в консоль попадают сообщения ThreadX.start()... и All threads are started!, что означает что основной поток не дожидается исполнения метода run в отдельном потоке. Основной поток делает лишь выполняет threadX.start() что эквивалентно надо запустить метод thread1.run() в отдельном потоке.

А что если мы хотим дождаться окончания выполнения метода run в этих двух параллельных потоках? Тогда на помощь приходит метод join, который заставляет текущий поток (например основной изначальный поток программы) приостановиться и дождаться когда другой поток (тот у которого вызван метод join) закончит свою работу:

MyThread thread1 = new MyThread();
System.out.println("Thread1.start()...");
thread1.start();

MyThread thread2 = new MyThread();
System.out.println("Thread2.start()...");
thread2.start();

System.out.println("All threads are started!");
System.out.pritnln("Waiting for them (using join)...");
thread1.join();
thread2.join();
System.out.pritnln("Threads finished their work! :)");

И получили вывод:

Thread1.start()...
Thread2.start()...
All threads are started!
Waiting for them (using join)...
MyThread starts running...
MyThread starts running...
  step #0
  step #1
  ...
  step #5
  step #9
MyThread finished!
  step #6
  step #7
  step #8
  step #9
MyThread finished!
Threads finished their work! :)

Обритите внимание что из основного потока программы из пяти сообщений четыре мы видим в самом начале вывода, а пятое - в самом конце, т.к. оно написано в коде после выполнения join, т.е. после того как мы дождались завершения выполнения обоих потоков.

Runnable

Как и с обработкой событий нажатия мышки или кнопок не всегда удобно создавать полноценный отдельный класс в отдельном файле лишь для того чтобы что-то посчитать в новом потоке.

Иногда может быть симпатичнее реализовать метод run прямо там где поток запускается.

Для этого достаточно создавать объект Thread и передать в него реализацию интерфейса Runnable (как и раньше было с ActionListener):

Thread thread1 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Running...");
    }
});

thread1.start();

Упражнение

Добавьте в ваш клеточный автомат многопоточность (в месте где рассчитываете состояние клеток в будущий момент времени).

Вы можете считать в 2, 4 или даже произвольное число потоков. Ведь вам достаточно в каждом рассчете новых состояний считать все не в один поток, а в \(N\) потоков, где каждый поток обсчитывает отведенную ему зону (например первую из \(N\) частей строчек/столбцов).

Здорово если вы считаете во столько потоков, сколько потоков поддерживает процессор на котором в данный момент запущена ваша программа. В Java число одновременно выполняемых на процессоре потоков можно узнать через int n = Runtime.getRuntime().availableProcessors();. Вы можете запустить и больше потоков, но дальше вы не получите ускорения.

Замеряйте время выполнения рассчета до и после многопоточности (через System.currentTimeMillis()), вероятно оно считается примерно 0 миллисекунд, в таком случае увеличьте размер поля до 3000x3000.