Эта статья призвана рассказать самые скучные основы самым распространенным языком.
Полная схема текста:
1. Что такое нить? (начальство)
2. Разница и связь между потоками и процессами (Часть 1)
3. Способ создания многопоточности (Часть 1)
4. Жизненный цикл потока (включен)
5. Контроль потока (вкл.)
6. Синхронизация потоков (ниже)
7. Пул потоков (ниже)
8. Потоковое общение (ниже)
9. Безопасность потоков (часть 2)
10. Базовое использование ThreadLocal (ниже)
В последнем эпизоде уже были описаны некоторые основные концепции потоков Java, а следующая часть этой статьи посвящена некоторым продвинутым приложениям Java.
6. Синхронизация потоков
Понятие «синхронизация потоков» может быть немного сложным для понимания поначалу, давайте возьмем каштан:
Папа открыл банковскую карту и положил 10 000 юаней, которые были зарезервированы для моего брата, который учился в Шаньдунском университете, и моей сестры, которая училась в старшей школе в его родном городе Хэнань. Брат позавчера взял 2000,а стало 8000.Сестра вчера взяла 1000,а остальные 7000.Сегодня пошли в банк снимать деньги одновременно.Когда брат открыл банкомат он обнаружил что там был баланс 7000. Когда сестра открыла его, он также обнаружил, что баланс был 7000. Они решили снять 1000 юаней. Когда они проверили баланс после снятия денег, они обнаружили, что было только 5000 юаней. думая, что я взял только 1000 юаней.
Это проблема «синхронизации» в жизни.
Обратим наше мышление в фоновую программу этого банкомата.К счастью, фоновая программа имеет синхронный прослушиватель действий для операции снятия денег.Он может блокировать действие снятия денег при одновременной работе нескольких потоков.Если программа не справиться с этим Проблема синхронизации, арифметика банкоматов с обеих сторон такова: 7000-1000, а результат - остальные 6000. Таким образом сверка банка будет неправильной.
Таким образом, можно увидеть, что неразумное использование параллельного программирования также принесет некоторые недостатки, и для проблемы многопоточного параллелизма в Java для решения проблемы вводится монитор синхронизации: прежде чем поток захочет выполнить синхронизированный блок кода/ метод, он должен сначала получить блокировку монитора синхронизации устройства.
Места, где используются блокировки в Java:
- кодовый блок
- Методы (кроме конструкторов и переменных-членов)
1. Синхронизация кодовых блоков
грамматика:
1synchronized (obj) {
2//同步内容(比如取钱的操作)
3}
Среди них obj является монитором синхронизации, то есть перед тем, как какой-либо поток войдет и выполнит блок кода, он сначала получает блокировку obj.После его получения другие потоки не могут его получить и изменить, пока текущий поток не освободит блок кода. должность.
Пример: счет банковской карты папы
1public BankCardAccount bankAccount;
2synchronized (bankAccount) {
3//对bankAccount的扣钱动作
4}
Когда брат и сестра снимают деньги одновременно, это похоже на выполнение двух потоков.Когда один из потоков получает блокировку на bankAccount, другой поток должен ждать, пока текущий поток освободит блокировку bankAccount до того, как это произойдет. можно получить и изменить.
2. Метод синхронизации
грамматика:
1修饰符 synchronized 返回值 方法名(形参列表){
2}
Синхронизация метода не требует отображения указанного монитора синхронизации, поскольку его монитор синхронизации является объектом текущего класса, которым является this.
3. Блокировка разблокировки
Если есть блокировка, ее необходимо снять.События снятия блокировки монитора синхронизации следующие:
- Выполнение синхронизированного блока/метода потока завершается
- Возникает исключение или возникает ОШИБКА во время выполнения синхронизированного блока/метода потока.
- Выполнить код завершения, такой как return и break в синхронизированном блоке/методе потока
- Метод wait() объекта монитора синхронизации выполняется в блоке/методе синхронизации потока.
Неопубликованные события также следующие:
- При выполнении в синхронизированном блоке/методе потока в программе выполняется операция паузы с помощью sleep()\yield().
- При выполнении в синхронизированном блоке/методе потока вызывается функция suspend() для приостановки потока.
4. Блокировка синхронизации
Для основных задач синхронизации может быть достаточно синхронизированного, но если вам нужны более мощные операции для синхронизации потоков, вам нужно использовать блокировку синхронизации Lock.
Блокировка — это инструмент для управления многопоточным доступом к общим ресурсам. Обычно он обеспечивает эксклюзивный доступ к общим ресурсам. Только один поток может заблокировать объект блокировки в каждый момент времени. Прежде чем поток начнет обращаться к общему ресурсу, он должен сначала получить объект блокировки.
Lock предоставляет множество классов/интерфейсов для различных сценариев использования, в основном следующим образом:
- Lock
- ReentrantLock
- ReadWriteLock
- ReentrantReadWriteLock
1. Блокировка интерфейса
Интерфейс блокировки предоставляет несколько методов управления блокировками:
1package java.util.concurrent.locks;
2import java.util.concurrent.TimeUnit;
3//Lock接口
4public interface Lock {
5 //获取锁。如果锁已被其他线程获取,则进行等待
6 void lock();
7 //获取锁,在等待过程中可以相应中断等待状态
8 void lockInterruptibly() throws InterruptedException;
9 //尝试获取锁,返回true为获得成功,返回false为获取失败
10 //它和lock()不一样的是,它不会一直等待,而是尝试获取,立即返回
11 boolean tryLock();
12 //尝试获得锁,如果获取不到就等待time时间
13 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
14 //释放锁
15 void unlock();
16}
2. ReentrantLock
Ревходная блокировка. Это означает, что один и тот же поток может получить одну и ту же блокировку несколько раз.Хотя синхронизация также является реентерабельной блокировкой, синхронизация не прерывается в процессе получения блокировки, а ReentrantLock может.
ReentrantLock — единственный класс, реализующий интерфейс Lock, поэтому мы можем создать объект Lock следующим образом:
1Lock l=new ReentrantLock();
Состояние ReentrantLock по умолчанию и состояние, полученное с помощью synchronized, относятся к несправедливым блокировкам (Получаются упреждающие блокировки Поток, который ожидает первым (вызов блокировки()), не обязательно получает блокировку первым, в то время как справедливая блокировка является точечной блокировкой потока, который получает блокировку первым.). Но ReentrantLock можно установить как справедливую блокировку, например:
1//公平锁
2Lock l1=new ReentrantLock(true);
3//非公平锁
4Lock l2=new ReentrantLock(false);
3. ReadWriteLock
Как следует из названия, это называется блокировкой чтения-записи, которая представляет собой интерфейс, используемый для управления блокировками чтения и блокировками записи. Блокировки чтения также называются общими блокировками, что означает, что блокировки чтения могут совместно использоваться несколькими потоками. также называемые эксклюзивными блокировками, что означает, что когда поток получает блокировку записи, другие потоки могут только ждать и не могут совместно использовать.
Ранее мы говорили: многопоточный параллелизм приносит проблемы с синхронизацией, а проблемы с синхронизацией решаются прослушивателями синхронизации.
Но мы нашли вот такой замкнутый круг:
Чтобы повысить эффективность выполнения программы с помощью многопоточности, прослушиватель синхронизации используется для многопоточного выполнения, и только один из потоков может выполнять блок кода или метод, измененный с помощью synchronized.Эти две вещи имеют отношения торговли -выключенный.
Так? Как сделать так, чтобы многопоточность гуляла счастливо, а проблем с синхронизацией возникало как можно меньше?
На самом деле, блокировки чтения-записи могут в определенной степени решить эту проблему. Его особенности:
- обмен чтением
- мьютекс чтения-записи
- мьютекс записи-записи
То есть, например, когда программа открывает несколько потоков для чтения и записи файла, если используется синхронизация, операции чтения и записи должны ждать друг друга, а с помощью ReadWriteLock
Мы можем разделить операции чтения и записи с блокировками, операции чтения с файлами с блокировками чтения и операции записи с файлами с блокировками записи.
Это заставит его работать быстрее.
Давайте посмотрим на его исходный код:
1public interface ReadWriteLock {
2 //获取读锁
3 Lock readLock();
4 //获取写锁
5 Lock writeLock();
6}
Существует только один метод интерфейса для получения блокировки чтения и один для получения блокировки записи Существование интерфейса имеет смысл только в том случае, если есть класс, который его реализует Давайте посмотрим на следующий класс:
4. ReentrantReadWriteLock
ReentrantReadWriteLock — это класс реализации интерфейса ReadWriteLock.Когда мы хотим создать блокировку ReadWriteLock, обычно:
1ReadWriteLock rl=new ReentrantReadWriteLock();
Как упоминалось ранее, ReentrantLock — это класс реализации Lock, ReentrantLock — это исключительная блокировка, то есть только одному потоку разрешен доступ в определенное время (но этот поток может получить доступ несколько раз одновременно), в то время как ReentrantLock является блокировкой чтения-записи, то есть одновременно нескольким потокам разрешено получать блокировки чтения для работы в одно и то же время (но одновременные операции чтения и записи, записи и записи не допускаются). бизнес-сценариев (таких как операции чтения намного выше, чем операции записи), ReentrantReadWriteLock будет больше ReentrantLock имеет лучшую производительность и параллелизм.
ReentrantReadWriteLock в основном имеет следующие специальные эффекты:
- Могут быть установлены честные блокировки и несправедливые блокировки.
1//公平锁
2ReadWriteLock rl=new ReentrantReadWriteLock(true);
3//非公平锁
4ReadWriteLock rl=new ReentrantReadWriteLock();
- Ревходная блокировка.
2.1 Один и тот же поток чтения может получить блокировку чтения несколько раз
2.2 Один и тот же поток записи может получить блокировку записи или блокировку чтения несколько раз- Возможность прерывания: то есть операция может быть прервана во время получения блокировки.
- Понижение блокировки: то есть блокировка записи может быть понижена до блокировки чтения.
7. Потоковое общение
Когда потоки выполняются в программе, планирование потоков имеет некоторую неопределенность, то есть время попеременного выполнения между потоками не может точно контролироваться в обычных условиях, поэтому Java предоставляет некоторые механизмы, облегчающие разработчикам управление скоординированной работой потоков. .
- Используйте wait(), notify(), notifyAll() в синхронизированных модифицированных методах/блоках кода для координации
- Использовать контроль состояния
- Использование управления очередью блокировки
1. Используйте wait(), notify(), notifyAll() для координации в синхронизированных модифицированных методах/блоках кода.
По сути, wait, notify, notifyAll — это методы экземпляра, определенные в классе Object, и их можно использовать только в синхронизированных блоках/методах кода для управления потоками.
- wait: Поток, удерживающий блокировку, готов снять разрешение на блокировку объекта, освободить ресурс ЦП и войти в режим ожидания.
- notify: Поток 1, удерживающий блокировку объекта, собирается снять блокировку и уведомить JVM о необходимости разбудить поток 2, конкурирующий за блокировку. После того, как поток завершается в области синхронизированного кода, поток 2 получает блокировку напрямую, а другие конкурирующие потоки продолжают ждать (даже если поток X синхронизируется и снимает блокировку объекта, другие конкурирующие потоки все еще ждут, пока не будет вызвано новое уведомление, notifyAll) .
- notifyAll: поток 1, удерживающий блокировку, готов снять блокировку и информирует JVM о необходимости разбудить все потоки, конкурирующие за блокировку.После того, как поток 1 завершает область синхронизированного кода, JVM назначает разрешение на блокировку объекта потоку 2 с помощью алгоритма , и все пробужденные темы Ждать больше нечего. После того, как поток 1 завершит область синхронизированного кода, все ранее пробужденные потоки могут получить разрешение на блокировку объекта, которое определяется алгоритмом JVM.
2. Используйте контроль условий
В случае использования Lock для выполнения работы по синхронизации Java предоставляет класс условий, помогающий управлять взаимодействием потоков. Экземпляры условия создаются объектами блокировки,
1//创建一个lock对象
2Lock l=new ReentrantLock();
3//创建一个condition实例
4Condition con=l.newCondition();
Класс Condition имеет следующие методы:
- await(): аналогично ожиданию(), заставляет текущий поток ждать, пока другие потоки не используют сигнал() или signalAll() условия, чтобы разбудить поток.
- signal(): пробуждает один поток, ожидающий этого объекта блокировки. Если все потоки ожидают объекта блокировки, он решит разбудить один из потоков. Выбор произвольный, только после того, как текущий поток откажется от блокировки на объекте Lock. Может выполнять пробужденный поток
- signalAll(): пробуждает все потоки, ожидающие этого объекта блокировки.Только после того, как текущий поток снимает блокировку с объекта блокировки, пробуждённый поток может быть выполнен.
3. Используйте блокировку управления очередью
Интерфейс предоставляется в Java5:BlockingQueue, он создается как инструмент для синхронизации потоков. Когда поток-производитель пытается поместить элементы в BlockingQueue, если очередь заполнена, поток блокируется. Когда поток-потребитель пытается извлечь элементы из BlockingQueue, если очередь Если он пуст, поток блокируется.
Исходный код интерфейса BlockingQueue:
1public interface BlockingQueue<E> extends Queue<E> {
2 boolean add(E e);
3 boolean offer(E e);
4 void put(E e) throws InterruptedException;
5 boolean offer(E e, long timeout, TimeUnit unit)
6 throws InterruptedException;
7 E take() throws InterruptedException;
8 E poll(long timeout, TimeUnit unit)
9 throws InterruptedException;
10 int remainingCapacity();
11 boolean remove(Object o);
12 public boolean contains(Object o);
13 int drainTo(Collection<? super E> c);
14 int drainTo(Collection<? super E> c, int maxElements);
15}
Есть два, которые поддерживают блокировку:
- take(): попытаться получить элемент из заголовка BlockingQueue
- put(E e): попытаться поместить e в BlockingQueue
Классы реализации интерфейса BlockingQueue:
- ArrayBlockingQueue: очередь блокировки массива
- LinkedBlockingQueue: очередь блокировки связанного списка
- PriorityBlockingQueue: нестандартная очередь блокировки с упорядочением
- SynchronousQueue: Синхронная очередь, чтение и запись не могут выполняться одновременно, только попеременно
- DelayQueue: специальная очередь блокировки, которая требует, чтобы элементы коллекции реализовывали интерфейс Dely.
Блокирующие очереди используются редко, поэтому я описываю лишь некоторые основные принципы и способы использования, и примеры повторяться не буду.
8. Пул потоков
Генерация пула потоков аналогична пулу соединений базы данных. Стоимость запуска потока системой относительно высока. Если при запуске программы инициализируется определенное количество потоков, они помещаются в пул потоков, и они удаляются из пула, когда это необходимо. Помещайте их обратно в пул после использования, что может значительно повысить производительность программы. Кроме того, некоторые конфигурации инициализации пула потоков также могут эффективно контролировать количество параллельных систем.
Java предоставляет фабричный класс Executors для создания пула потоков.Для создания нового пула потоков в основном используются следующие статические методы:
- newFixedThreadPool: многоразовый пул с фиксированным количеством потоков.
- newCachedThreadPool: пул с кешем
- newSingleThreadExecutor: пул только с одним потоком
- newScheduledThreadPool: можно указать пул для отложенного выполнения.
Что касается конкретного использования и параметров каждого метода, я не буду их повторять еще раз, интересующие пакеты можно увидеть непосредственно, войдя в класс Executors.
9. Безопасность потоков
Что такое потокобезопасность?
В многопоточной среде, когда несколько потоков одновременно обращаются к общим данным, доступ потока модифицируется другими потоками, что приводит к использованию неверных данных и генерации ошибок, что приводит к небезопасности потока.
И когда несколько потоков обращаются к классу, класс ведет себя правильно, независимо от того, как запланирована среда выполнения или как будут чередоваться процессы, и без какой-либо дополнительной синхронизации или координации в поведении вызывающего кода, тогда класс считается потокобезопасным. .
Помните ли вы, что, будь то внеклассные упражнения учителя или вопросы на собеседовании, часто возникают такие вопросы, как «Являются ли StringBuilder и StringBuffer потокобезопасными»?
Давайте посмотрим на их соответствующий исходный код, чтобы увидеть, что происходит.
Метод добавления StringBuffer:
1@Override
2 public synchronized StringBuffer append(String str) {
3 toStringCache = null;
4 super.append(str);
5 return this;
6 }
Метод добавления StringBuilder:
1 @Override
2 public StringBuilder append(String str) {
3 super.append(str);
4 return this;
5 }
Взгляните на их исходный код super.append:
1public AbstractStringBuilder append(String str) {
2 if (str == null)
3 return appendNull();
4 int len = str.length();
5 ensureCapacityInternal(count + len);
6 str.getChars(0, len, value, count);
7 count += len;
8 return this;
9 }
Можно увидеть, что разница между двумя методами добавления заключается в том, что первый из них модифицируется с помощью синхронизированного, что означает, что когда несколько потоков могут получить доступ к этому методу одновременно, первый блокируется, а второй может выполняться одновременно. время и доступ подсчитываются одновременно, поэтому может возникнуть путаница в подсчете. Видно, что:
StringBuffer является потокобезопасным, но эффективность становится низкой из-за добавления блокировок.
StringBuilder небезопасен для потоков, а в однопоточной среде он очень эффективен.
Теперь, когда мы знаем, что такое безопасность потоков, как Java решает проблему безопасности потоков?
Начиная с Java5, были добавлены некоторые потокобезопасные классы для решения проблем с потокобезопасностью, например:
- ThreadLocal
- ConcurrentHashMap
- ConcurrentSkipListMap
- ConcurrentSkipListSet
- ConcurrentLinkedQueue
- ConcurrentLinkedDeque
- CopyOnWriteArrayList
- CopyOnWriteArrayList
- CopyOnWriteArraySet
- CopyOnWriteHashMap
10. ThreadLocal
ThreadLocal представляет собой локальную переменную потока.Помещая данные в ThreadLocal, каждый поток может создать копию этой переменной, никогда не избегая проблемы безопасности потоков, связанной с одновременным доступом.
Один из способов сохранить ограничение потока — использовать ThreadLocal. Он предоставляет методы доступа, такие как set и get, которые сохраняют отдельную копию для каждого потока, использующего переменную, поэтому метод get всегда возвращает самое последнее значение, установленное текущим исполняемым потоком при вызове набора.
Он предоставляет три метода:
- T get(): возвращает значение в текущей копии потока этой локальной переменной потока.
- remove(): удаляет значение текущего потока в этой локальной переменной потока.
- set(T t): устанавливает значение в копии текущего потока в этой локальной переменной потока.
Например: создайте класс с ThreadLocal:
1public class TestThreadLocal {
2 // 副本
3 private ThreadLocal<Integer> countLoacl = new ThreadLocal<Integer>();
4 public TestThreadLocal(Integer num) {
5 countLoacl.set(num);
6 }
7 public Integer getCount() {
8 return countLoacl.get();
9 }
10 public void setCount(Integer num) {
11 countLoacl.set(num);
12 }
13}
Класс, созданный таким образом, имеет countLoacl, равный ThreadLocal. Когда несколько потоков используют этот объект одновременно, ThreadLocal создаст копию countLoacl для каждого потока, чтобы избежать конкуренции за ресурсы между несколькими потоками и вызвать проблемы с безопасностью.