Одновременное программирование является одним из самых важных навыков для программистов Java и одного из самых сложных навыков для освоения. Это требует, чтобы программисты имели глубокое понимание самых низких принципов эксплуатации компьютера, и в то же время программисты имели четкое логику и тщательное мышление, чтобы они могли писать эффективные, безопасные и надежные многопоточные программы параллельных параллелей. Эта серия начнется с природы межполосной координации (подождите, уведомление, уведомление), синхронизированное и нестабильное, и подробно объясните каждый инструмент параллелизма и базовый механизм реализации, предоставленный JDK. Исходя из этого, мы дополнительно проанализируем классы инструментов пакета java.util.concurrent, включая его использование, реализацию исходного кода и принципы, стоящие за ним. Эта статья является первой статьей в этой серии и является самой основной теоретической частью этой серии. Последующие статьи будут проанализированы и объяснены на основе этого.
1. Обмен
Обмен данными является одной из основных причин безопасности потока. Если все данные действительны только в потоке, не существует проблемы безопасности потока, которая является одной из основных причин, почему нам часто не нужно рассматривать безопасность потока при программировании. Однако в многопоточном программировании обмен данными неизбежно. Наиболее типичным сценарием являются данные в базе данных. Чтобы обеспечить согласованность данных, нам обычно нужно поделиться данными в одной и той же базе данных. Даже в случае мастера и раба доступна те же данные. Мастер и раб просто копируют те же данные для эффективности доступа и безопасности данных. Теперь мы демонстрируем проблемы, вызванные обменом данными в нескольких потоках через простой пример:
Код фрагмент 1:
пакет com.paddx.test.concurrent; открытый класс sharedata {public static int count = 0; public static void main (string [] args) {final sharedata data = new sharedata (); for (int i = 0; i <10; i ++) {new Thread (new Runnable () {@Override public void run () {try {// пауза для 1 миллисекунд при увеличении вероятности задач параллерода. data.addcount (); } try {// Основная программа приостановлена на 3 секунды, чтобы гарантировать, что выполнение вышеуказанной программы завершено Thread.sleep (3000); } catch (прерванное искусство e) {e.printstacktrace (); } System.out.println ("count =" + count); } public void addCount () {count ++; }}Целью вышеуказанного кода является добавление одной операции, чтобы подсчитать и выполнить 1000 раз, но здесь реализовано через 10 потоков, каждый поток выполняется 100 раз, а при нормальных обстоятельствах 1000 должен быть выведен. Однако, если вы запустите вышеуказанную программу, вы обнаружите, что результат не имеет значения. Вот результат выполнения определенного времени (результаты каждого прогона могут быть не одинаковыми, и иногда можно получить правильный результат):
Видно, что для общих операций переменных различные неожиданные результаты легко наблюдаются в многопоточной среде.
2. взаимное исключение
Взаимное исключение ресурсов означает, что только одному посетителю разрешается получить к нему доступ к нему одновременно, что является уникальным и эксклюзивным. Мы обычно позволяем нескольким потокам считывать данные одновременно, но только один поток может писать данные одновременно. Таким образом, мы обычно делим блокировки на общие замки и эксклюзивные блокировки, также называемые блокировками чтения и блокировки записи. Если ресурсы не являются взаимоисключающими, нам не нужно беспокоиться о безопасности потоков, даже если они являются общими ресурсами. Например, для неизменного обмена данными все потоки могут только читать его, поэтому проблемы безопасности потоков не нужны. Тем не менее, операции по написанию общих данных обычно требуют взаимного исключения. В приведенном выше примере проблемы модификации данных возникают из -за отсутствия взаимного исключения. Java предоставляет несколько механизмов для обеспечения взаимного исключения, самым простым способом является использование синхронизации. Теперь мы добавляем синхронизированные в вышеуказанную программу и выполняем:
Код фрагмент два:
пакет com.paddx.test.concurrent; открытый класс sharedata {public static int count = 0; public static void main (string [] args) {final sharedata data = new sharedata (); for (int i = 0; i <10; i ++) {new Thread (new Runnable () {@Override public void run () {try {// пауза для 1 миллисекунд при увеличении вероятности задач параллерода. data.addcount (); } try {// Основная программа приостановлена на 3 секунды, чтобы гарантировать, что выполнение вышеуказанной программы завершено Thread.sleep (3000); } catch (прерванное искусство e) {e.printstacktrace (); } System.out.println ("count =" + count); } / *** Добавить синхронизированное ключевое слово* / public synchronized void addCount () {count ++; }}Теперь, когда приведенный выше код выполнен, вы обнаружите, что независимо от того, сколько раз вы выполняете, конечный результат будет 1000.
Iii. Атомность
Атомность относится к работе данных как независимого и неделимого целого. Другими словами, это операция, которая является непрерывной и непрерывной. Половина выполнения данных не изменяется другими потоками. Самый простой способ обеспечить атомичность - это инструкции по операционной системе, то есть, если одна операция соответствует одной инструкции по операционной системе за раз, она определенно обеспечит атомичность. Тем не менее, многие операции не могут быть завершены с одной инструкцией. Например, для операций длинного типа многие системы должны быть разделены на несколько инструкций для работы на высоких и низких положениях соответственно. Например, операция целого числа i ++, которую мы часто используем, на самом деле должна быть разделена на три шага: (1) прочитать значение целого числа I; (2) добавить одну операцию в I; (3) Напишите результат обратно в память. Этот процесс может произойти в многопользовательском виде:
Это также причина, по которой результат выполнения сегмента кода неверен. Для этой комбинированной операции можно реализовать наиболее распространенный способ обеспечения атомичности, такой как синхронизированный или блокировка в Java, а сегмент 2 кода реализован с помощью синхронизации. В дополнение к замкам, есть еще один способ CAS (сравнить и обмениваться), то есть, прежде чем изменять данные, сравните, являются ли значения, прочитанные до предыдущих, согласованными. Если они последовательны, изменяйте их, и если они противоречивы, они будут выполнены снова. Это также является принципом оптимизации реализации блокировки. Тем не менее, CAS может не быть эффективным в некоторых сценариях. Например, другой поток сначала изменяет определенное значение, а затем изменяет его на исходное значение. В этом случае CAS не может судить.
4. видимость
Чтобы понять видимость, вам нужно иметь определенное понимание модели памяти JVM. Модель памяти JVM аналогична операционной системе, как показано на рисунке:
На этом рисунке мы видим, что каждый поток имеет свою собственную рабочую память (эквивалентно расширенному буферу ЦП. Цель этого состоит в том, чтобы еще больше сузить разницу скорости между системой хранения и ЦП и улучшить производительность). Для общих переменных каждый раз, когда поток считывает копию общей переменной в рабочей памяти. При написании он напрямую изменяет значение копии в рабочей памяти, а затем синхронизирует рабочую память со значением в основной памяти в определенный момент времени. Проблема, которую это вызывает, заключается в том, что, если поток 1 изменяет определенную переменную, поток 2 может не видеть модификации, сделанные потоком 1, на общую переменную. С помощью следующей программы мы можем продемонстрировать невидимую проблему:
пакет com.paddx.test.concurrent; public class visibueTest {Private Static Boolean Ready; частный статический int номер; Private Static Class ReaderThread Extends Thread {public void run () {try {thread.sleep (10); } catch (прерванное искусство e) {e.printstacktrace (); } if (! Готово) {System.out.println (ready); } System.out.println (номер); }} private static class writerthread extends {public void run () {try {thread.sleep (10); } catch (прерванное искусство e) {e.printstacktrace (); } номер = 100; Готово = правда; }} public static void main (string [] args) {new workerthread (). start (); new ReaderThread (). start (); }}Интуитивно, эта программа должна выводить только 100, а готовое значение не будет напечатано. На самом деле, если вы выполняете приведенный выше код несколько раз, может быть много разных результатов. Вот результаты около двух прогонов:
Конечно, этот результат можно сказать, что только возможен из -за видимости. Когда поток записи (writerthread) подготовлен ready = true, ReaderThread не может увидеть измененный результат, поэтому false будет напечатан. Для второго результата, то есть результат поток записи не был прочитан при выполнении if (! Готово), но результат выполнения записи выполняется при выполнении System.out.println (готово). Однако этот результат также может быть вызван альтернативным выполнением потоков. Видимость может быть обеспечена с помощью синхронизированной или летучих в Java, и конкретные детали будут проанализированы в последующих статьях.
5. Последовательность
Чтобы улучшить производительность, компилятор и процессор могут переупорядочить инструкции. Есть три типа переупорядочения:
(1) Оптимизированный компилятор переупорядочение. Компилятор может перенести порядок выполнения операторов, не изменяя семантику программы с одним нагрузкой.
(2) Переупорядочение параллелизма уровня инструкции. Современные процессоры используют параллельные технологии на уровне инструкций (ICP) для перекрытия выполнения нескольких инструкций. Если нет зависимости данных, процессор может изменить порядок выполнения оператора, соответствующий инструкциям машины.
(3) Переупорядочение системы памяти. Поскольку процессор использует буферы кэша и чтения/записи, это делает операции загрузки и хранения, которые, по -видимому, выполняются вне порядка.
Мы можем непосредственно обратиться к описанию переупорядочения проблем в JSR 133:
(1) (2)
Давайте сначала посмотрим на часть исходного кода (1) на рисунке выше. Из исходного кода либо инструкция 1 выполняется первой, либо в первую очередь выполняется инструкция 3. Если инструкция 1 выполнена в первую очередь, R2 не должен видеть значение, записанное в инструкции 4. Если инструкция 3 выполнена сначала, R1 не должно видеть значение, написанное инструкцией 2. Однако результат выполнения может иметь R2 == 2 и R1 == 1, что является результатом «переупорядочения». Вышеупомянутый рисунок (2) является возможным результатом юридической компиляции. После компиляции порядок инструкции 1 и инструкции 2 могут быть взаимозаменяемы. Поэтому появится результат R2 == 2 и R1 == 1. Синхронизированный или летучий также может использоваться на Java для обеспечения порядка.
Шесть резюме
В этой статье объясняется теоретическая основа параллельного программирования Java, и некоторые вещи будут обсуждаться более подробно в последующем анализе, таких как видимость, порядок и т. Д. Последующие статьи будут обсуждаться на основе содержания этой главы. Если вы сможете хорошо понять вышеупомянутый контент, я считаю, что вам будет очень полезно, будь то понимание других одновременных программных статей или в вашей ежедневной одновременной программной работе.