Чи ExecutorService
гарантує безпеку різьблення?
Я буду надсилати завдання з різних потоків до одного і того ж ThreadPoolExecutor, чи потрібно синхронізувати доступ до виконавця перед взаємодією / надсиланням завдань?
Відповіді:
Це правда, схожі класи JDK, здається, не дають явної гарантії безпечного подання завдань. Однак на практиці всі реалізації ExecutorService в бібліотеці справді безпечні для потоку таким чином. Я думаю, що розумно залежати від цього. Оскільки весь код, що реалізує ці функції, був розміщений у відкритому доступі, немає абсолютно жодної мотивації для того, щоб повністю переписати його по-іншому.
interface ExecutorService
, якого ThreadPoolExecutor
також слід дотримуватися. (Більш детально в моїй нещодавно оновленій відповіді.)
( В відміну від інших відповідей) договір потокобезпечна буде документований: погляд у interface
Javadocs (на відміну від Javadoc методів). Наприклад, внизу javadoc ExecutorService ви знайдете:
Ефекти узгодженості пам’яті: дії в потоці до подання виконуваного чи викличного завдання до ExecutorService відбуваються - до будь - яких дій, виконаних цим завданням, які, в свою чергу, відбуваються - до отримання результату через Future.get ().
Цього достатньо, щоб відповісти на це:
"чи потрібно синхронізувати доступ до виконавця перед взаємодією / поданням завдань?"
Ні, ні. Чудово конструювати та подавати завдання будь-якому (правильно реалізованому) ExecutorService
без зовнішньої синхронізації. Це одна з головних цілей проектування.
ExecutorService
є одночасною утилітою, тобто, вона призначена для роботи в найбільшій мірі, не вимагаючи синхронізації, для підвищення продуктивності. (Синхронізація спричиняє суперечку між потоками, що може погіршити ефективність багатопоточності - особливо при масштабуванні до великої кількості потоків.)
Немає гарантії щодо того, в який час у майбутньому завдання будуть виконуватися або виконуватися (деякі можуть навіть виконуватися негайно в тому самому потоці, який їх надіслав), проте робочий потік гарантовано бачив усі ефекти, які виконав подаючий потік до пункт подання . Тому (потік, який виконується) ваше завдання також може безпечно читати будь-які дані, створені для його використання, без синхронізації, безпечних для потоків класів або будь-яких інших форм "безпечної публікації". Сам акт подання завдання є достатнім для "безпечної публікації" вхідних даних до завдання. Потрібно лише переконатись, що вхідні дані жодним чином не будуть змінені під час виконання завдання.
Подібним чином, коли ви повертаєте результат завдання назад Future.get()
, отриманий потік гарантовано бачитиме всі ефекти, зроблені робочим потоком виконавця (в обох повернутих результатах, а також будь-які зміни побічних ефектів, зроблені робочим потоком) .
Цей контракт також передбачає, що самі завдання можуть подавати більше завдань.
"Чи гарантує ExecutorService безпеку ниток?"
Зараз ця частина питання набагато більш загальна. Наприклад, не вдалося знайти жодного твердження договору про безпеку потоків щодо методу shutdownAndAwaitTermination
- хоча я зауважую, що зразок коду в Javadoc не використовує синхронізацію. (Хоча, можливо, існує приховане припущення, що зупинку ініціює той самий потік, який створив Виконавець, а не, наприклад, робочий потік?)
До речі, я рекомендую книгу "Java Concurrency In Practice" для кращого ознайомлення зі світом паралельного програмування.
Ваше запитання досить відкрите: усе, що ExecutorService
робить інтерфейс, це гарантує, що якийсь потік десь обробить надісланий Runnable
або Callable
екземпляр.
Якщо представлені Runnable
/ Callable
посилання сумісництвом структури даних , яка доступна з інших Runnable
/ Callable
сек випадків (потенційно оброблюваних simulataneously різних потоків), то це ваша відповідальність , щоб забезпечити безпеку потоку через цю структуру даних.
Щоб відповісти на другу частину вашого запитання, так, перед тим, як подавати будь-які завдання, ви матимете доступ до ThreadPoolExeecuter; напр
BlockingQueue<Runnable> workQ = new LinkedBlockingQueue<Runnable>();
ExecutorService execService = new ThreadPoolExecutor(4, 4, 0L, TimeUnit.SECONDS, workQ);
...
execService.submit(new Callable(...));
РЕДАГУВАТИ
На основі коментаря Брайана та на випадок, якщо я неправильно зрозумів ваше запитання: Подання завдань із декількох виробницьких потоків до ExecutorService
, як правило, є безпечним для потоків (незважаючи на те, що явно не згадується в інтерфейсі API, наскільки я можу зрозуміти). Будь-яка реалізація, яка не забезпечує безпеку потоків, була б марною в багатопотоковому середовищі (оскільки багато виробників / декілька споживачів є досить поширеною парадигмою), і саме для цього ExecutorService
(і для решти java.util.concurrent
) було розроблено.
Бо ThreadPoolExecutor
відповідь просто так . ExecutorService
ніяк НЕ санкціонувати або іншим чином гарантувати , що всі реалізації потокобезпечна, і вона не може , як це інтерфейс. Ці типи контрактів виходять за рамки інтерфейсу Java. Однак і те, ThreadPoolExecutor
і інше чітко задокументовано як безпечне для потоків. Більше того, ThreadPoolExecutor
керує своєю чергою завдань, використовуючи java.util.concurrent.BlockingQueue
інтерфейс, який вимагає, щоб усі реалізації були потокобезпечними. Будь-яку java.util.concurrent.*
реалізацію BlockingQueue
можна сміливо вважати безпечною для потоків. Будь-яка нестандартна реалізація може цього не робити, хоча це було б абсолютно безглуздо, якби хтось запропонував BlockingQueue
чергу впровадження, яка не була безпечною для потоків.
Отже, відповідь на ваше титульне запитання однозначно так . Відповідь на наступний зміст вашого запитання, ймовірно , є , оскільки між ними є деякі розбіжності.
List.hashCode()
). У Java-документах сказано, що "реалізації BlockingQueue безпечні для потоку" (отже, не безпечна для потоків BlockingQueue є не просто безглуздою, а помилковою), але такої документації для ThreadPoolExecutor або будь-якого інтерфейсу, який він реалізує, немає.
ThreadPoolExecutor
вона захищена від потоків?
Для ThreadPoolExecutor його подання є потокобезпечним. Ви можете побачити вихідний код у jdk8. Додаючи нове завдання, він використовує mainLock, щоб забезпечити безпеку потоку.
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
На відміну від того, що стверджує відповідь Люка Ашервуда , з документації не випливає, що ExecutorService
реалізації гарантовано є безпечними для потоків. Щодо конкретного питання ThreadPoolExecutor
, дивіться інші відповіді.
Так, вказано взаємозв'язок " до-до" , але це не означає нічого про безпеку потоків самих методів, як прокоментував Майлз . У відповіді Люка Ашервуда зазначено, що першого достатньо, щоб довести друге, але фактичних аргументів не наводиться.
"Безпека потоків" може означати різні речі, але ось простий контра-приклад Executor
(не, ExecutorService
але це не має значення), який тривіально відповідає необхідним відносинам " до-до", але не є безпечним для потоків через несинхронізований доступ до count
поля .
class CountingDirectExecutor implements Executor {
private int count = 0;
public int getExecutedTaskCount() {
return count;
}
public void execute(Runnable command) {
command.run();
}
}
Застереження: Я не фахівець, і я знайшов це запитання, оскільки сам шукав відповідь.