Необходимость синхронизации потоков в JAVA

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



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");
      }
}

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

yshavit 28.07.2024 05:44

Re, «При использовании синхронизации...» Для ясности: в вашем коде нет синхронизации. * Ваша процедура main синхронизируется с объектом Driver.class, но поскольку никакой другой поток в программе никогда не синхронизируется с тем же объектом, это фактически то же самое, что вообще нет синхронизации. [*В методе println, вызываемом вашим кодом, имеется синхронизация, поэтому, даже если вы видите три строки, напечатанные в разном порядке, вы никогда не увидите выходные данные двух или более потоков, смешанные в одну строку.]

Solomon Slow 28.07.2024 17:38
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
50
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Многопоточность все еще работает.

  1. Ключевое слово synchronized в коде, которым вы поделились, по сути не используется. Метод драйвера main всегда запускается основным потоком. Поскольку ваш код не обращается к основному методу из разных потоков, ключевое слово Synchronized здесь избыточно. Ключевое слово используется для блокировки метода/блока, чтобы новый метод не мог одновременно получить к нему доступ.
  2. Запустив приведенную выше программу несколько раз, вы можете заметить, что операторы печати не расположены в фиксированном порядке. Например, мне удалось получить этот результат из вашего кода: Неупорядоченные операторы печати из-за многопоточности.

Ваш пример слишком прост, чтобы продемонстрировать полезность потоков.

По сути, вы печатаете 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.)

Это даст тот же результат, но, вероятно, в другом порядке. Затраченное время будет зависеть от количества физических ядер, доступных приложению.

  • Если бы было два ядра, два потока могли бы работать одновременно, и общее время сократилось бы вдвое.
  • Если бы ядер было 4, время делилось бы на 4.
  • Если бы было 20 (или 40, или 80) ядер, время составило бы примерно 10 секунд.

Таким образом, несколько потоков позволяют выполнять вычисления на нескольких ядрах одновременно. В некоторых случаях это может дать вам значительное ускорение (или значительное увеличение пропускной способности).

Ответ принят как подходящий

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

Потоки — это модель параллельных вычислений, которая представляет собой более широкую категорию. Все параллельные алгоритмы являются параллельными алгоритмами, но не все параллельные алгоритмы используют параллелизм. Проще говоря, «параллелизм» означает, что в программе есть два или более действия, которые выполняются, в основном независимо друг от друга, одновременно.

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

Это облегчает чтение кода*, поскольку код выглядит точно так же, как выглядел бы код однопоточного сервера, который может одновременно обрабатывать только одного клиента. Он выглядит так же, как однопоточный код, который мы все учились писать, когда были новичками. Одной из альтернатив использованию потока для каждого клиента является вызов одного потока, что-то вроде 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. Реализация ThreadRunnable была ошибочным проектным решением, сделанным в спешке, когда Java спешно выходила на рынок в бурные дни изобретения Всемирной паутины.

Кроме того, в Java 5 появился фреймворк Executors, который избавил нас, Java-программистов, от чреватой ошибками работы по манипулированию потоками. Нам редко приходится напрямую обращаться к классу Thread.

Вместо этого напишите свои задачи в классе (или лямбде), который реализует либо Runnable, либо Callable. Затем создайте экземпляр ExecutorService с помощью служебного класса Executors. И отправьте экземпляры вашего класса задач в эту службу-исполнитель.

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

Другие вопросы по теме