На этот раз давайте полностью разберемся с многопоточностью Java (2/10)

Java
На этот раз давайте полностью разберемся с многопоточностью Java (2/10)

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

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

Хотя чтение этой статьи не требует предварительного знания концепций, связанных с параллелизмом, если вы усвоили некоторые общие концепции, это значительно упростит понимание. Заинтересованные читатели могут обратиться к первой статье этой серии, чтобы понять основные понятия, связанные с параллелизмом.Когда мы говорим «параллелизм, многопоточность», что мы имеем в виду?.

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

Первые пять статей содержат следующее содержание и будут опубликованы в ближайшее время:

  1. Основные понятия параллелизма -Когда мы говорим «параллелизм, многопоточность», что мы имеем в виду?
  2. Начало работы с многопоточностью — эта статья
  3. Анатомия пула потоков
  4. Анализ механизма синхронизации нитей
  5. Часто задаваемые вопросы о параллелизме

Зачем иметь несколько потоков?

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

сцена

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

Но если хорошенько подумать, то кажется, что в обычных ресторанах это не так, потому что в большинстве ресторанов подают его в смешанном порядке. То же самое и в программе: разные программы могут запускаться попеременно, поэтому мы не можем получать сообщения WeChat, когда инструмент разработки открыт на нашем компьютере.

Это прикладной сценарий многопоточности: за счет поочередного выполнения задач на компьютере одновременно может выполняться несколько программ.

другой сценарий

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

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

последняя сцена

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

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

Что такое многопоточность?

Многопоточность означает много потоков, это очень просто?

Поток — это исполнительная единица в операционной системе, и такая же исполнительная единица имеет еще и процесс, и весь код должен выполняться в процессе/потоке. Потоки подчинены процессам, а процесс может содержать несколько потоков. Еще одно различие между процессом и потоком заключается в том, что каждый процесс имеет свое собственное независимое пространство памяти и не может напрямую обращаться друг к другу; однако несколько потоков в одном и том же процессе совместно используют пространство памяти процесса, поэтому они могут напрямую обращаться к одному и тому же фрагменту данных. memory, наиболее типичным из которых является куча в Java.

Введение в многопоточное программирование

Зная так много теоретических концепций, наконец пришло время написать код вручную.

создать тему

Использование потоков в JavaThreadКласс указывает, что конструктор класса Thread может передаваться в реализациюRunnableОбъект интерфейса в этом объекте Runnablevoid run()Методы представляют задачи, которые будут выполняться в потоке. Например, если вы хотите создать самоувеличивающуюся целочисленную переменнуюRunnableТогда задачу можно записать так:

// 静态变量,用于自增
private static int count = 0;

// 创建Runnable对象(匿名内部类对象)
Runnable task = new Runnable() {
    public void run() {
        for (int i = 0; i < 1e6; ++i) {
            count += 1;
    }
}

имеютRunnableПосле того, как задача, которую нужно выполнить, представлена ​​объектом, мы можем создать два потока для ее запуска.

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);

Но в это время создается только объект потока.На самом деле поток не был выполнен.Если вы хотите выполнить поток, вам нужно вызвать объект потока.start()метод.

t1.start();
t2.start();

В этот момент поток может начать выполняться, и полный код выглядит следующим образом:

public class SimpleThread {

    private static int count = 0;

    public static void main(String[] args) throws Exception {
        Runnable task = new Runnable() {
            public void run() {
                for (int i = 0; i < 1000000; ++i) {
                    count = count + 1;
                }
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();
        
        // 等待t1和t2执行完成
//        t1.join();
//        t2.join();

        System.out.println("count = " + count);
    }
}

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

Это связано с тем, что мы не ждем завершения потока после того, как создадим и запустим поток в основном методе, используйтеt1.join()Вы можете заставить текущий поток ждать завершения выполнения потока t1, прежде чем продолжить выполнение. Давайте попробуем, удалив двойную косую черту перед двумя вызовами метода соединения.

синхронизация потоков

Результат выполнения на моем компьютере 1753490, результат вашего выполнения будет другим, но опять же не достигнет ожидаемых нами 2 миллионов. Конкретную причину можно найти на диаграмме последовательности выполнения ниже.

t1 t2
Получить значение счетчика 0
Получить значение счетчика 0
Результат вычисления 0+1 равен 2
сохранить 2 для подсчета
Результат вычисления 0+1 равен 2
сохранить 2 для подсчета

Видно, что одновременная работа между двумя потоками t1 и t2 приведет к перезаписи собственных результатов друг друга.Конечный результат будет между миллионом и двумя миллионами, но будет относительно большое расстояние от двух миллионов. Такой блок кода, в котором несколько потоков совместно считывают и изменяют одни и те же общие данные, называетсякритическая секция, только один поток может войти в критическую секцию одновременно, если несколько потоков войдут одновременно, это вызоветгонка данныхпроблема. Если кому-то из читателей будет интереснокритическая секцияигонка данныхЕсли концепция не ясна, вы можете обратиться к первой статье этой серии, в которой представлены основные концепции параллелизма —Когда мы говорим «параллелизм, многопоточность», что мы имеем в виду?.

До Java 5 наиболее распространенным методом синхронизации потоков, который мы использовали, было ключевое словоsynchronized, это ключевое слово может быть либо отмечено в методе, либо использоваться как независимая структура блока. Синхронизированное ключевое слово в форме объявления метода может использоваться в определении метода следующим образом:public synchronized static void methodName(). Потому что наша операция накопления наследуется от интерфейса Runnablerun()метод, поэтому нет возможности изменить объявление метода, тогда вы можете использовать следующую блочную структуруsynchronizedКлючевые слова:

Runnable task = new Runnable() {
    public void run() {
        for (int i = 0; i < 1000000; ++i) {
            synchronized (SimpleThread.class) {
                count += 1;
            }
        }
    }
};

Синхронизированный — это своего рода блокировка объекта. Используемая блокировка связана с конкретным объектом. Если это тот же объект, это та же блокировка, если это другой объект, это другая блокировка. Только один поток может одновременно удерживать блокировку, а это означает, что другие потоки, которые хотят получить ту же блокировку, будут заблокированы до тех пор, пока поток, удерживающий блокировку, не освободит блокировку. Здесь в качестве имени блокировки можно рассматривать объект, соответствующий объектной блокировке, синхронизацию осуществляет не сам объект, а объектная блокировка, соответствующая объекту.

В скобках после синхронизированного ключевого слова блочной структуры указан объект, соответствующий блокировке объекта.В приведенном выше коде мы используем блокировку, соответствующую объекту класса класса SimpleThread, в качестве средства синхронизации. Если в объявлении метода используется ключевое слово synchronized, то, если это метод экземпляра (нестатический метод), соответствующий объект является объектом, на который указывает этот указатель, а если это статический метод, то соответствующий объект объект класса того класса, в котором он находится.

На этот раз мы видим, что каждый раз на выходе стабильно 2 миллиона, мы успешно завершили нашу первую полную многопоточную программу 🎉🎉🎉

постскриптум

Однако при написании многопоточного кода мы обычно не создаем объекты Thread напрямую, а используем пулы потоков для управления выполнением задач. Я полагаю, что читатели также во многих местах встречали слово «пул потоков».Если вы хотите разобраться в использовании и конкретной реализации пулов потоков, то можете обратить внимание на следующую статью, которая будет опубликована в ближайшее время.

До сих пор мы рассмотрели только концепции параллелизма и многопоточности, а также простую реализацию многопоточной программы. Далее мы рассмотрим более глубокую и сложную реализацию многопоточности, включая, помимо прочего, изменчивые ключевые слова, CAS, AQS, видимость памяти, общие пулы потоков, блокирующие очереди, взаимоблокировки, проблемы параллелизма без взаимоблокировок, события. применение и соединение точек знаний, таких как модель вождения, и, наконец, каждый может постепенно реализовать ряд параллельных структур данных и программ, обычно используемых в различных инструментах, таких как AtomicInteger, блокирующая очередь и веб-сервер, управляемый событиями. Я верю, что после этой серии приключений по многопоточному программированию вы сможете обращаться с темой многопоточности легко и упорядоченно.