при использовании cachedthreadpool можно

Русские Блоги

Разница между FixedThreadPool и CachedThreadPool

текст

CachedThreadPool

Обратите внимание, что экземпляр очереди: новый SynchronousQueue

FixedThreadPool

Обратите внимание, что экземпляр очереди: new LinkedBlockingQueue

SingleThreadPool

Программа демо

Когда интервал ожидания составляет 5 миллисекунд, в общей сложности 3 потока создаются и выполняются поочередно.

при использовании cachedthreadpool можно

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

при использовании cachedthreadpool можно

Независимо от интервала ожидания, в общей сложности 3 потока создаются и выполняются поочередно.

при использовании cachedthreadpool можно

Независимо от интервала ожидания, всего создается 1 поток.

при использовании cachedthreadpool можно

Сравнение функций FixedThreadPool и CachedThreadPool

собственностиFixedThreadPoolCachedThreadPool
Повторное использованиеFixedThreadPool похож на CacheThreadPool и может быть использован повторно, но новые потоки не могут быть созданы в любое время.Кэш-пул, сначала проверьте, есть ли ранее созданные потоки в пуле, если они есть, используйте повторно, если нет, создайте новый поток, чтобы присоединиться к пулу
Размер бассейнаУказываемые nThreads, фиксированный номерРастущий, максимум Integer.MAX_VALUE
Размер очерединеограниченныйнеограниченный
Тайм-аутНет бездействия60 секунд по умолчанию
Используйте сценуПоэтому FixedThreadPool в основном предназначен для некоторых очень стабильных и фиксированных регулярных параллельных потоков, которые в основном используются сервером.Большое количество кратковременных асинхронных задач
конецНе будет уничтожен автоматическиОбратите внимание, что потоку, помещенному в CachedThreadPool, не нужно беспокоиться о его завершении, он будет автоматически завершен, если он неактивен по истечении TIMEOUT.

Лучшая практика

FixedThreadPool и CachedThreadPool не особенно удобны для приложений с высокой нагрузкой.

CachedThreadPool намного опаснее, чем FixedThreadPool.

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

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

Если вам нужно выполнить некоторые операции до и после выполнения задачи, вы можете перезагрузить

Динамически контролировать размер пула потоков во время выполнения (Dynamic Thread Pool)

Источник

FixedThreadPool против CachedThreadPool: меньшее из двух зол

Итак, у меня есть программа, которая порождает потоки (

5-150), которые выполняют кучу задач. Первоначально я использовал FixedThreadPool поскольку этот аналогичный вопрос предполагал, что они лучше подходят для более длительных задач и с моими очень ограниченными знаниями многопоточности, я рассмотрел средний срок службы потоков (несколько минут)»долго жил«.

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

пробовать их обоих предварительно, нет кажется быть разница, так что я склонен идти с CachedThreadPool просто, чтобы избежать отходов. Однако, означает ли продолжительность жизни потоков, что я должен вместо этого выбрать FixedThreadPool и просто разобраться с неиспользуемыми потоками? Этот вопрос заставляет вас думать, что эти дополнительные потоки не являются впустую, но я был бы признателен за разъяснение.

3 ответов:

CachedThreadPool-это именно то, что вы должны использовать для своей ситуации, поскольку нет никаких негативных последствий для использования одного для длинных потоков. Комментарий в документе java о том, что CachedThreadPools подходит для коротких задач, просто предполагает, что они особенно подходят для таких случаев, а не то, что они не могут или не должны использоваться для задач, связанных с длительными задачами.

в дальнейшем, исполнителей.newCachedThreadPool и исполнителей.newFixedThreadPool оба поддерживаются одной и той же реализацией пула потоков (по крайней мере, в открытом JDK) только с разными параметрами. Различия просто являются их минимумом потока, максимумом, временем уничтожения потока и типом очереди.

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

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

и FixedThreadPool и CachedThreadPool являются злом в высоконагруженных приложениях.

CachedThreadPool опаснее, чем FixedThreadPool

если ваше приложение сильно загружено и требует низкой задержки, лучше избавиться от обоих вариантов из-за ниже недостатков

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

Итак, у меня есть программа, которая порождает потоки (

5-150), которые выполняют кучу задач.

вы уверены, что понимаете, как потоки на самом деле обрабатываются вашей ОС и оборудованием по выбору? Как Java сопоставляет потоки с потоками ОС, как это сопоставляет потоки с потоками процессора и т. д.? Я спрашиваю, потому что создание 150 потоков внутри в одном JRE имеет смысл только в том случае, если у вас есть массивные ядра процессора/потоки внизу, что, скорее всего, не так. В зависимости от ОС и оперативной памяти в использование, создание более n потоков может даже привести к тому, что ваша JRE будет прекращена из-за ошибок OOM. Поэтому вы должны действительно различать потоки и работать с этими потоками, сколько работы вы даже можете обрабатывать и т. д.

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

и эта проблема не возникает с FixedThreadPool, созданным, например, с 2 + n потоками, где N является разумно низким, конечно, потому что с этим используются аппаратные и операционные ресурсы с гораздо меньшими накладными расходами для управления потоками, которые не могут работать в любом случае.

Источник

Русские Блоги

Углубленное объяснение структуры пула потоков Java-Executor

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

1. Введение в структуру

1.1 Двухуровневая модель планирования фреймворка Executor

Потоки Java (java.lang.Thread) взаимно однозначно отображаются на собственные потоки операционной системы. Собственный поток операционной системы создается при запуске потока Java; когда поток Java завершается, поток операционной системы также будет повторно использован. Операционная система будет планировать все потоки и распределять их по доступным ЦП. На верхнем уровне многопоточные программы Java обычно разбивают приложение на несколько задач, а затем используют планировщик уровня пользователя (структура Executor) для сопоставления этих задач с фиксированным числом потоков; внизу ядро ​​операционной системы сопоставляет эти потоки с аппаратной обработкой. 。 На устройстве. Прикладная программа управляет планированием верхнего уровня через среду Executor; планирование нижнего уровня контролируется ядром операционной системы, а планирование нижнего уровня не контролируется прикладной программой.

при использовании cachedthreadpool можно

1.2 Структура и состав фреймворка Executor

1. Структура фреймворка «Исполнитель»

(1) Задача. Включая интерфейс, который должен быть реализован выполняемой задачей: Runnable interface или Callable interface.

(2) Выполнение заданий. Включая основной интерфейс Executor механизма выполнения задачи и интерфейс ExecutorService, унаследованный от Executor. Платформа Executor имеет два ключевых класса, реализующих интерфейс ExecutorService (ThreadPoolExecutor и ScheduledThreadPoolExecutor).

(3) Результат асинхронного расчета. Включая интерфейс Future и класс FutureTask, реализующий интерфейс Future.

2. Основные классы и интерфейсы, включенные в среду Executor.

(4) Интерфейс Future и класс FutureTask, реализующий интерфейс Future, представляют результат асинхронных вычислений.

(5) Классы реализации интерфейса Runnable и Callable.

при использовании cachedthreadpool можно

3. Использование фреймворка Executor:

(1) Сначала создайте объект задачи, который реализует интерфейс Runnable или Callable. Инструментальный класс Executors может инкапсулировать объект Runnable как вызываемый объект (Executors.callable (Runnable task) или Executors.callable (Runnable task, Object resule)).

(2) Затем объект Runnable может быть напрямую доставлен в ExecutorService для выполнения (ExecutorService.execute (команда Runnable)); или объект Runnable или объект Callable можно передать в ExecutorService для выполнения (ExecutorService.submit (Runnable task) или ExecutorService.submit (Callabletervice) )).

(3) Если ExecutorService.submit (. ) выполняется, ExecutorService вернет объект, реализующий интерфейс Future (пока что в JDK возвращением является объект FutureTask). Поскольку FutureTask реализует Runnable, программисты также могут создавать FutureTask, а затем напрямую передавать его ExecutorService для выполнения.

(4) Наконец, основной поток может выполнить метод FutureTask.get (), чтобы дождаться завершения выполнения задачи. Основной поток также может выполнить FutureTask.cancel (логическое mayInterruptIfRunning), чтобы отменить выполнение этой задачи.при использовании cachedthreadpool можно

4. Члены структуры «Исполнитель»

Основные члены структуры Executor: ThreadPoolExecutor, ScheduledThreadPoolExecutor, Future interface, Runnable interface, Callable interface и Executors.

(1) ThreadPoolExecutor: ThreadPoolExecutor можно создать с помощью фабричного класса Executors. Исполнители могут создавать 3 типа ThreadPoolExecutor: SingleThreadExecutor, FixedThreadPool и CachedThreadPool. Или вы можете создать его сами. Подробно объясните SingleThreadExecutor, FixedThreadPool и CachedThreadPool.

(2) ScheduledThreadPoolExecutor: обычно создается классом фабрики Executors. Исполнители могут создавать 2 типа ScheduledThreadPoolExecutor, включая

(3) Интерфейс будущего: интерфейс Future и класс FutureTask, реализующие интерфейс Future, используются для представления результата асинхронного вычисления. Когда мы отправляем реализацию интерфейса Runnable или интерфейса Callable в ThreadPoolExecutor или ScheduledThreadPoolExecutor, ThreadPoolExecutor или ScheduledThreadPoolExecutor вернет нам объект FutureTask. Однако в API возвращается объект FutureTask, а Java гарантирует только то, что возвращается объект, реализующий интерфейс Future. В будущих реализациях JDK возвращаемое значение не обязательно может быть FutureTask.

(4) Интерфейс Runnable и Интерфейс Callable: классы реализации интерфейса Runnable и Callable могут выполняться ThreadPoolExecutor или ScheduledThreadPoolExecutor. Разница между ними в том, что Runnable не возвращает результатов, а Callable может возвращать результаты. Помимо создания объектов, реализующих интерфейс Callable, вы также можете использовать метод Executors фабричного класса, чтобы обернуть Runnable в Callable.

2. Подробное описание трех классов расширений ThreadPoolExecutor.

2.1 Подробное описание FixedThreadPool

FixedThreadPool называется пулом потоков с фиксированным количеством повторно используемых потоков. Ниже приводится реализация исходного кода FixedThreadPool.

Процесс выполнения метода execute:

1) Если количество запущенных в настоящее время потоков меньше, чем corePoolSize, создайте новый поток для выполнения задачи.

2) Когда количество запущенных в данный момент потоков равно или больше corePoolSize, задача добавляется в LinkedBlockingQueue.

При использовании неограниченной очереди будут достигнуты следующие эффекты:

1) Когда количество потоков в пуле потоков достигает corePoolSize, новые задачи будут ожидать в неограниченной очереди, поэтому количество потоков в пуле потоков не будет превышать corePoolSize.

2) Из-за 1, maximumPoolSize будет недопустимым параметром при использовании неограниченной очереди. Чтобы

3) Из-за 1 и 2 keepAliveTime будет недопустимым параметром при использовании неограниченных очередей. Чтобы

4) Из-за использования неограниченных очередей запущенный FixedThreadPool (метод shutdown () или shutdownNow () не выполняется) не будет отклонять задачу (метод RejectedExecutionHandler.rejectedExecution не будет вызываться).

2.2 Подробное объяснение SingleThreadExecutor

2.3 Подробное описание CachedThreadPool

CorePoolSize для CachedThreadPool установлен на 0, то есть corePool пуст; для параметра maximumPoolSize установлено значение Integer.MAX_VALUE, то есть максимальный пул не ограничен. Здесь keepAliveTime имеет значение 60L, что означает, что неактивный поток в CachedThreadPool ожидает новой задачи не более 60 секунд, а неактивный поток будет завершен через 60 секунд.

CachedThreadPool использует SynchronousQueue без емкости в качестве рабочей очереди пула потоков, но
Максимальный пул CachedThreadPool не ограничен. Это означает, что если основной поток отправляет задачи быстрее, чем
Когда скорость задачи обработки потока установлена ​​в maximumPool, CachedThreadPool будет постоянно создавать новые потоки. В крайних случаях
CachedThreadPool будет исчерпывать ресурсы ЦП и памяти из-за создания слишком большого количества потоков.

при использовании cachedthreadpool можно

3. Подробное объяснение ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor наследуется от ThreadPoolExecutor. В основном он используется для запуска задач после заданной задержки или для периодического выполнения задач. Функция ScheduledThreadPoolExecutor похожа на Timer, но функция ScheduledThreadPoolExecutor более мощная и гибкая. Таймер соответствует одному фоновому потоку, а ScheduledThreadPoolExecutor может указать несколько соответствующих фоновых потоков в конструкторе.

3.1 Рабочий механизм

Выполнение ScheduledThreadPoolExecutor в основном разделено на две части. Во-первых, когда вызывается метод scheduleAtFixedRate () или scheduleWithFixedDelay () ScheduledThreadPoolExecutor, ScheduledFutureTask, который реализует интерфейс RunnableScheduledFutur, добавляется в DelayQueue ScheduledThreadPoolExecutor. Затем потоки в пуле потоков получают ScheduledFutureTask из DelayQueue, а затем выполняют задачу.

при использовании cachedthreadpool можно

Для реализации задач периодического выполнения ScheduledThreadPoolExecutor внес некоторые изменения в ThreadPoolExecutor.

3.2 Реализация ScheduledThreadPoolExecutor

1. ScheduledThreadPoolExecutor поместит задачу для планирования (ScheduledFutureTask) в DelayQueue. ScheduledFutureTask в основном содержит три переменные-члены, длинную переменную-член time, которая указывает конкретное время, когда эта задача будет выполнена. Переменная-член длинного типа sequenceNumber представляет порядковый номер этой задачи, добавляемой в ScheduledThreadPoolExecutor. Длинный период переменной-члена указывает период интервала выполнения задачи.

2. DelayQueue инкапсулирует PriorityQueue, который сортирует ScheduledFutureTask в очереди. При сортировке меньшее время занимает первое место (более ранняя задача будет выполнена первой). Если время двух ScheduledFutureTasks одинаково, сравните sequenceNumber, и меньший sequenceNumber получает ранжирование первым (то есть, если время выполнения двух задач одинаково, задача, отправленная первой, будет выполнена первой).

3. Процесс, в котором потоки в ScheduledThreadPoolExecutor выполняют периодические задачи:

при использовании cachedthreadpool можно

(1) Поток 1 получает истекшую ScheduledFutureTask из DelayQueue (DelayQueue.take ()). Выполненная задача означает, что время ScheduledFutureTask больше или равно текущему времени.

(2) Поток 1 выполняет эту ScheduledFutureTask.

(3) Поток 1 изменяет временную переменную ScheduledFutureTask на время, когда она будет выполнена в следующий раз.

(4) Поток 1 помещает ScheduledFutureTask после изменения времени обратно в DelayQueue (DelayQueue.add ()).

4. Шаги выполнения DelayQueue.take ():

(2) Получите периодические задания. Этот шаг включает

(3) Отпустите фиксатор.

ScheduledThreadPoolExecutor выполняет шаг 2 в цикле и не выйдет из бесконечного цикла, пока поток не получит элемент из PriorityQueue (конечный шаг 2).

5. Шаги выполнения DelayQueue.add ():

Добавление задач делится на 3 основных этапа:

3) Отпустите фиксатор.

4. Подробная FutureTask

4.1 Введение в FutureTask

Интерфейс Future и класс FutureTask, реализующий интерфейс Future, представляют результат асинхронного вычисления. В дополнение к интерфейсу Future, FutureTask также реализует интерфейс Runnable. Следовательно, FutureTask может быть передан Executor для выполнения или может быть выполнен непосредственно вызывающим потоком (FutureTask.run ()). В зависимости от времени, когда выполняется метод FutureTask.run (), FutureTask находится в следующих трех состояниях.

Когда FutureTask не запущен или не запущен, выполнение метода FutureTask.get () приведет к блокировке вызывающего потока;

Когда FutureTask находится в завершенном состоянии, выполнение метода FutureTask.get () заставит вызывающий поток немедленно вернуть результат или выбросить исключение.

Если FutureTask не запущен, выполнение метода FutureTask.cancel () приведет к тому, что задача никогда не будет выполнена;

Когда FutureTask находится в запущенном состоянии, выполнение метода FutureTask.cancel (true) попытается остановить задачу, прервав поток, выполняющий задачу;

Когда FutureTask находится в запущенном состоянии, выполнение метода FutureTask.cancel (false) не повлияет на поток, выполняющий задачу (пусть выполняющаяся задача завершится);

Когда FutureTask находится в завершенном состоянии, выполнение метода FutureTask.cancel (. ) вернет false.

4.2 Использование FutureTask

FutureTask может быть передан Executor для выполнения; FutureTask может быть возвращен с помощью метода ExecutorService.submit (. ), а затем может быть выполнен метод FutureTask.get () или FutureTask.cancel (. ). Кроме того, FutureTask также можно использовать отдельно. Обратите внимание на характеристики его метода запуска и метода получения. Только метод выполнения объекта FutureTask может возвращаться после выполнения метода get. В противном случае он будет заблокирован. Это можно использовать для достижения и механизма ожидания / уведомления Тот же эффект.

4.3 Базовая реализация FutureTask

Каждый синхронизатор на основе AQS будет содержать два типа операций:

Заметка: «Искусство параллельного программирования на Java» применимо не ко всем версиям jdk. Я обнаружил, что реализация FutureTask основана не на абстрактном классе AbstractQueuedSynchronizer, а на методах park () и unpark () в LockSupport. Реализовано и через встроенный связанный список класса WaitNode для хранения и управления всеми потоками, заблокированными методом get (). Поэтому здесь я буду анализировать только реализацию jdk1.8. На основе абстрактного класса AbstractQueuedSynchronizer вы можете самостоятельно ссылаться на исходный код в классе Semaphore, поэтому я не буду повторять его здесь.

2. Затем мы смотрим на исходный код метода get, чтобы увидеть, как он реализует блокировку.

Шаги выполнения, реализуемые методом get:

(1) Сначала определите, запущена ли текущая FutureTask или нет, затем вызовите метод awaitDone для блокировки и перейдите к следующему шагу. В противном случае возвращается результат расчета задачи.

(2) В методе awaitDone сначала проверяется, прерван ли текущий поток, и если это так, он генерирует исключение и возвращается, а метод awaitDone прекращает блокировку. В противном случае переходите к следующему шагу.

(4) Находится ли задача FutureTask в рабочем состоянии, если да, продолжить блокировку. В противном случае переходите к следующему шагу.

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

(6) Следует ли добавлять заблокированный в данный момент поток в связанный список, если нет, добавить его в связанный список и вернуться в очередь как истинный; если он уже был добавлен, перейдите к следующему шагу.

(7) Вызовите метод park (Object), чтобы заблокировать текущий поток, пока метод unpark (Thread t) не разблокируется. Или используйте parkNanos (блокировщик объектов, длинные нано) для блокировки с течением времени и возврата в состояние по истечении указанного времени или вызовите метод unpark (Thread t) для разблокировки.

(8) Повторяйте шаги 2-7, пока метод не вернется к разблокировке.

3. Метод run и метод cancle намного проще: после того, как метод run выполнен или метод cancle прерывает текущий поток, будет вызван метод finishCompletion (). Метод finishCompletion () пройдет по списку заблокированных потоков, удалит все узлы и разблокирует потоки во всех узлах, а затем вернется.

Источник

Русские Блоги

резюме

Представьте несколько основных ExecutorService в параллельном пакете Java.

текст

CachedThreadPool

CachedThreadPool является экземпляром ThreadPoolExecutor, созданным java.util.concurrent.Executors. Этот экземпляр будет повторно использовать ранее созданный поток в пуле по мере необходимости, когда поток доступен. Этот пул потоков выполняетсяМногие недолговечные асинхронные задачи (многие недолговечные асинхронные задачи), Может значительно улучшить производительность программы. вызовexecute В настоящее время вы можете повторно использовать ранее созданные доступные потоки.Если нет доступных потоков, новый поток создается заново и добавляется в пул потоков. Если поток не использовался более 60 секунд, он будет прерван и удален из кэша. Следовательно, пул потоков не будет использовать ресурсы после простоя в течение длительного времени.

Обратите внимание, что экземпляр очереди: new SynchronousQueue ()

FixedThreadPool

Обратите внимание, что экземпляр очереди: new LinkedBlockingQueue ()

SingleThreadPool

Программа демо

Когда интервал ожидания составляет 5 миллисекунд, в общей сложности 3 потока создаются и выполняются поочередно.

при использовании cachedthreadpool можно

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

при использовании cachedthreadpool можно

Независимо от интервала ожидания, в общей сложности 3 потока создаются и выполняются поочередно.

при использовании cachedthreadpool можно

Независимо от интервала ожидания, всего создается 1 поток.

при использовании cachedthreadpool можно

Сравнение функций FixedThreadPool и CachedThreadPool

собственностиFixedThreadPoolCachedThreadPool
Повторное использованиеFixedThreadPool похож на CacheThreadPool и может быть использован повторно, но новые потоки не могут быть созданы в любое времяКэшируемый пул, сначала проверьте, есть ли ранее созданные потоки в пуле, если есть, повторно используйте их, если нет, создайте новый поток, чтобы присоединиться к пулу
Размер бассейнаNThreads может быть указан, фиксированный номерРастущее, максимальное значение Integer.MAX_VALUE
Размер очерединеограниченныйнеограниченный
Тайм-аутНет бездействия60 секунд по умолчанию
Используйте сценуТак что FixedThreadPool в основном предназначен для некоторых обычных параллельных потоков, которые очень стабильны и исправлены, в основном используются для серверовБольшое количество кратковременных асинхронных задач
конецНе будет уничтожен автоматическиОбратите внимание, что потоку, помещенному в CachedThreadPool, не нужно беспокоиться о его завершении, он будет автоматически завершен, если он неактивен в течение более чем TIMEOUT.

Лучшая практика

FixedThreadPool и CachedThreadPool не особенно удобны для приложений с высокой нагрузкой.

CachedThreadPool намного опаснее, чем FixedThreadPool.

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

Если вам нужно выполнить определенные операции до и после выполнения задачи, вы можете перезагрузить

Динамически контролировать размер пула потоков во время выполнения (Dynamic Thread Pool)

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *