Основы многопоточности

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

Давайте начнем с основных понятий: ядер центрального процессора, компьютерных процессов и потоков, а затем попробуем понять преимущества многопоточного программирования.

Компьютерный процесс в сравнении с потоком

Рис. 1 — потоки и процессы

Проще говоря, процесс  —  это программа в ходе своего выполнения. Когда мы выполняем программу или приложение, запускается процесс. Каждый процесс состоит из одного или нескольких потоков.

Поток  —  это не что иное, как сегмент процесса. Потоки  —  исполняемые сущности, которые выполняют задачи, стоящие перед исполняемым приложением. Процесс завершается, когда все потоки заканчивают выполнение.

Роль ядер процессора

Каждый поток в процессе  —  это задача, которую должен выполнить процессор. Большинство процессоров сегодня умеют выполнять одновременно две задачи на одном ядре, создавая дополнительное виртуальное ядро. Это называется одновременная многопоточность или многопоточность Hyper-Threading, если речь о процессоре от Intel. Эти процессоры называются многоядерными процессорами. Таким образом, двухъядерный процессор имеет 4 ядра: два физических и два виртуальных. Каждое ядро может одновременно выполнять только один поток.

Почему многопоточность?

Как упоминалось выше, один процесс содержит несколько потоков, и одно ядро процессора может выполнять только один поток за единицу времени. Если мы пишем программу, которая запускает потоки последовательно, то есть передает выполнение в очередь одного конкретного ядра процессора, мы не раскрываем весь потенциал многоядерности. Остальные ядра просто стоят без дела, в то время как существуют задачи, которые необходимо выполнить. Если мы напишем программу таким образом, что она создаст несколько потоков для отнимающих много времени независимых функций, то мы сможем использовать другие ядра процессора, которые в противном случае пылились бы без дела. Можно выполнять эти потоки параллельно, тем самым сократив общее время выполнения процесса.

Время запачкать руки

Мы напишем программу, где запустим две функции (выполним две задачи). Сначала мы будем выполнять функции последовательно, через один и тот же поток, а затем создадим отдельные потоки для каждой из них. При этом отметим время выполнения для обоих подходов и увидим кое-что волшебное:

package multithreadingPackage;
import java.util.concurrent.TimeUnit;
class Summation1 implements Runnable{
	@Override
	public void run() {
		summation();
	}
	public void summation(){
		long a=0;
		for (int i=0; i<=10000;i++) {
			for (int j=0; j<=10000;j++) {
				a++;
			}	
		}
		System.out.println("Sum1 is : "+ a);
	}
}

class Summation2 implements Runnable{
	@Override
	public void run() {
		summation();
	}
	public void summation(){
		long a=2;
		for (int i=0; i<=10000;i++) {
			for (int j=0; j<=10000;j++) {
				a++;
			}	
		}
		System.out.println("Sum2 is : "+ a);
	}
}

public class ThreadDemo{	
	public static void main(String[] args) {
		long startTime = System.currentTimeMillis();
		Summation1 sum1 = new Summation1();
		sum1.summation();
		Summation2 sum2 = new Summation2();
		sum2.summation();
		long endTime = System.currentTimeMillis();
		long timeElapsed = endTime - startTime;
		System.out.println("Execution time without multithreading: " + timeElapsed + " milliseconds");
		
		startTime = System.currentTimeMillis();
		Thread t1 = new Thread(new Summation1());
		Thread t2 = new Thread(new Summation2());
		t1.run();
		t2.run();
		while(t1.isAlive()||t2.isAlive()) {
			
		}
		endTime = System.currentTimeMillis();
		timeElapsed = endTime - startTime;
		System.out.println("Execution time with multithreading: " + timeElapsed + " milliseconds");
	}
}

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

Рис. 2 — Вывод кода

Вот, видите? Время выполнения сокращается до одной трети при использовании потоков по сравнению с последовательным выполнением функций. Такова сила многопоточности.

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

На мой взгляд, понимание основ чего-то  —  лучший способ это “что-то” освоить. Надеюсь, эта статья помогла вам получить представление о необходимости и полезности многопоточного программирования.
Существует гораздо больше информации о потоках: жизненный цикл потоков, проблемы синхронности и способы их решения и т. д. Обязательно ознакомьтесь с ними. Как говорится, тяжело в учении  —  легко в бою!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи: Rajat Gogna, “Basics of Multithreading”

Предыдущая статья5 основных фреймворков для Java-разработчиков
Следующая статьяGitHub Codespaces: быстрая разработка на ходу с Flutter