При использовании синхронизации мои потоки работают один за другим. То же самое происходит, когда у меня есть только один поток, который выполняет инструкции один за другим, так какой смысл создавать несколько потоков.
class Myclass1 extends Thread
{
public void run()
{
System.out.println("Running in class 1");
}
}
class Myclass2 extends Thread
{
public void run()
{
System.out.println("Running in class 2");
}
}
class Driver
{
synchronized public static void main(String[]args)
{
Myclass1 obj1=new Myclass1();
Myclass2 obj2=new Myclass2();
obj1.start();
obj2.start();
System.out.println("Hello");
}
}
Re, «При использовании синхронизации...» Для ясности: в вашем коде нет синхронизации. * Ваша процедура main
синхронизируется с объектом Driver.class
, но поскольку никакой другой поток в программе никогда не синхронизируется с тем же объектом, это фактически то же самое, что вообще нет синхронизации. [*В методе println
, вызываемом вашим кодом, имеется синхронизация, поэтому, даже если вы видите три строки, напечатанные в разном порядке, вы никогда не увидите выходные данные двух или более потоков, смешанные в одну строку.]
Многопоточность все еще работает.
synchronized
в коде, которым вы поделились, по сути не используется. Метод драйвера main
всегда запускается основным потоком. Поскольку ваш код не обращается к основному методу из разных потоков, ключевое слово Synchronized здесь избыточно. Ключевое слово используется для блокировки метода/блока, чтобы новый метод не мог одновременно получить к нему доступ.Ваш пример слишком прост, чтобы продемонстрировать полезность потоков.
По сути, вы печатаете 3 сообщения: два в дочерних потоках и третье после запуска потоков. Когда я запускаю код, я получаю различные заказы на сообщения. Однако именно этого я и ожидал, поскольку код никак не ограничивает порядок сообщений. (Действительно, возможны все 6 перестановок.)
Потоки полезны, когда вашей программе необходимо выполнить несколько нетривиальных вычислений. Например, предположим, что вы пытаетесь найти одну миллионную цифру квадратного корня из N для N в диапазоне от 1 до 20. И предположим, что у вас уже есть метод, который вычисляет это для одного значения N:
public static int computeDigit(int n) {
// Compute the 1,000,000th digit of squareroot(n)
return ...
}
И предположим, что на это вычисление уходит до 10 секунд (я предполагаю...)
В однопоточном приложении вы можете выполнить задачу следующим образом:
for (int i = 1; i <= 20; i++) {
int answer = computeDigit(i);
System.out.println("Answer for " + i + " is " + answer);
}
Это будет вычислять и распечатывать ответы по одному и займет до 20 * 10 == 200 секунд.
В многопоточном приложении вы можете выполнить задачу следующим образом:
for (int i = 1; i <= 20; i++) {
new Thread(n -> { int answer = computeDigit(n);
System.out.println("Answer for " + n +
" is " + answer);
}
).start();
}
(I am using a Java lambda there ... look it up.)
Это даст тот же результат, но, вероятно, в другом порядке. Затраченное время будет зависеть от количества физических ядер, доступных приложению.
Таким образом, несколько потоков позволяют выполнять вычисления на нескольких ядрах одновременно. В некоторых случаях это может дать вам значительное ускорение (или значительное увеличение пропускной способности).
В двух словах Стивен С сказал, что потоки можно использовать для достижения параллельных вычислений. Это правда, но это не единственное, для чего хороши потоки. Фактически, потоки широко использовались задолго до того, как параллельные вычисления стали возможны за пределами передовых лабораторий компьютерной инженерии.
Потоки — это модель параллельных вычислений, которая представляет собой более широкую категорию. Все параллельные алгоритмы являются параллельными алгоритмами, но не все параллельные алгоритмы используют параллелизм. Проще говоря, «параллелизм» означает, что в программе есть два или более действия, которые выполняются, в основном независимо друг от друга, одновременно.
Классическим примером является сетевой сервер, который может одновременно обслуживать несколько разных клиентов. Все, что он делает для одного клиента, может быть совершенно не связано с тем, что он делает для любого другого клиента. Если сервер создает один поток для каждого активного клиента, то состояние работы, которую любой поток выполняет для своего клиента, неявно отражается в регистрах контекста и стеке вызовов потока. вызовы вложенных функций и локальные переменные этих функций содержат всю информацию о том, что поток делает для клиента.
Это облегчает чтение кода*, поскольку код выглядит точно так же, как выглядел бы код однопоточного сервера, который может одновременно обрабатывать только одного клиента. Он выглядит так же, как однопоточный код, который мы все учились писать, когда были новичками. Одной из альтернатив использованию потока для каждого клиента является вызов одного потока, что-то вроде Linux select, чтобы дождаться следующего сообщения от любого клиента, а затем он должен найти объект, который явно кодирует состояние этого клиента. и вызвать некоторую подпрограмму, которая продолжит работу с этого момента. Некоторые серверы построены таким образом, и на это есть веские причины, но их код может быть значительно сложнее, чем модель «поток на клиент».
Обратите внимание, что в случае нашего сервера с потоко-на-клиентом, если доступно несколько процессоров, программа будет автоматически использовать их и обрабатывать одновременные клиентские запросы действительно параллельным образом, но даже с одним процессором модель по-прежнему работает, и преимущества многопоточности по-прежнему того стоят.
* Чтение многопоточного кода может быть простым делом, но написание правильного многопоточного кода — это совсем другая история. Несмотря на это, код, написанный один раз, может быть прочитан много раз, поэтому даже простое облегчение чтения обычно является победой.
Остальные ответы хорошие. Но вот самое простое объяснение: ваш код — плохой пример, потому что он выполняется слишком быстро.
"Hello"
, не видя результатов других. Или вы можете увидеть, как Myclass1
или Myclass2
делают свое дело в любом порядке.println
, не является хорошей демонстрацией для просмотра параллельных потоков в работе. Замедлите их, возможно, добавив несколько sleep
и еще несколько вызовов println
.System.out
не в хронологическом порядкеВывод в System.out
не обязательно отображается в хронологическом порядке при вызове в разных потоках. Вы должны добавить временные метки и изучить их, чтобы проверить, что и в каком порядке произошло.
В нашей пересмотренной версии вашего кода мы:
sleep
и println
в Myclass1
или Myclass2
.sleep
к основному методу, чтобы дать потокам некоторое время поработать.Instant.now
повсюду для проверки фактического порядка событий.public class ExSimple
{
public static void main ( String[] args )
{
System.out.println( "`main` start at: " + Instant.now( ) );
Myclass1 obj1 = new Myclass1( );
Myclass2 obj2 = new Myclass2( );
obj1.start( );
obj2.start( );
try { Thread.sleep( Duration.ofSeconds( 20 ) ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
System.out.println( "`main` done at: " + Instant.now( ) );
}
}
class Myclass1 extends Thread
{
public void run ( )
{
System.out.println( "Start class 1. On thread ID: " + Thread.currentThread( ).threadId( ) + Instant.now( ) );
try { Thread.sleep( Duration.ofSeconds( 4 ) ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
System.out.println( "Middle class 1. " + Instant.now( ) );
try { Thread.sleep( Duration.ofSeconds( 7 ) ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
System.out.println( "End class 1. " + Instant.now( ) );
}
}
class Myclass2 extends Thread
{
public void run ( )
{
System.out.println( "Start class 2. On thread ID: " + Thread.currentThread( ).threadId( ) + Instant.now( ) );
try { Thread.sleep( Duration.ofSeconds( 2 ) ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
System.out.println( "Middle class 2. " + Instant.now( ) );
try { Thread.sleep( Duration.ofSeconds( 13 ) ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
System.out.println( "End class 2. " + Instant.now( ) );
}
}
Вот пример вывода. Строки появляются в том порядке, в котором мы ожидаем, учитывая нашу разную длительность sleep
.
Start class 1. On thread ID: 212024-07-30T04:59:58.860181Z
Start class 2. On thread ID: 222024-07-30T04:59:58.860307Z
Middle class 2. 2024-07-30T05:00:00.873229Z
Middle class 1. 2024-07-30T05:00:02.873274Z
End class 1. 2024-07-30T05:00:09.886505Z
End class 2. 2024-07-30T05:00:13.880383Z
`main` done at: 2024-07-30T05:00:18.867635Z
synchronized
Ваш метод synchronized
on main
не нужен, неуместен и сбивает с толку. Использование synchronized
не влияет на ваш конкретный пример.
Кстати, не расширяйте Thread
, чтобы использовать его как Runnable
. Реализация Thread
Runnable
была ошибочным проектным решением, сделанным в спешке, когда Java спешно выходила на рынок в бурные дни изобретения Всемирной паутины.
Кроме того, в Java 5 появился фреймворк Executors, который избавил нас, Java-программистов, от чреватой ошибками работы по манипулированию потоками. Нам редко приходится напрямую обращаться к классу Thread
.
Вместо этого напишите свои задачи в классе (или лямбде), который реализует либо Runnable
, либо Callable
. Затем создайте экземпляр ExecutorService
с помощью служебного класса Executors
. И отправьте экземпляры вашего класса задач в эту службу-исполнитель.
Для большинства целей лучше всего использовать виртуальные потоки с некоторыми оговорками.
В данном конкретном случае никакой выгоды нет. Но что, если каждый из двух потоков выполняет дорогостоящие вычисления или, может быть, каждый загружает большой файл? Было бы лучше делать то и другое одновременно, а не делать одно, а затем другое. Это то, что делают потоки.