[Java] Введение в многопоточность, Thread, join
На данный момент в большинстве компьютеров используются многоядерные процессоры. Поэтому может возникнуть естественный вопрос - как считать или выполнять что-то на нескольких ядрах ради ускорения.
Более того, многопоточность имеет гораздо более широкое применение чем просто
ради ускорения вычислений, в частности многопоточность возможна и в случае одноядерных компьютеров, т.к. обычно вы
хотите использовать несколько приложений параллельно (например браузер, файловый менеджер и блокнот), и хотя каждое из этих приложений
является полноценной программой в которой код исполняется подряд - компьютер даже в случае одного ядра старается дать вам возможность
кофмортно работать в разных приложениях одновременно. Это тоже достигается за счет многопоточности - каждое приложение работает в своем вычислительном “потоке” (на английском 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.