Чи ExecutorServiceгарантує безпеку різьблення?
Я буду надсилати завдання з різних потоків до одного і того ж ThreadPoolExecutor, чи потрібно синхронізувати доступ до виконавця перед взаємодією / надсиланням завдань?
Відповіді:
Це правда, схожі класи JDK, здається, не дають явної гарантії безпечного подання завдань. Однак на практиці всі реалізації ExecutorService в бібліотеці справді безпечні для потоку таким чином. Я думаю, що розумно залежати від цього. Оскільки весь код, що реалізує ці функції, був розміщений у відкритому доступі, немає абсолютно жодної мотивації для того, щоб повністю переписати його по-іншому.
interface ExecutorService, якого ThreadPoolExecutorтакож слід дотримуватися. (Більш детально в моїй нещодавно оновленій відповіді.)
( В відміну від інших відповідей) договір потокобезпечна буде документований: погляд у interfaceJavadocs (на відміну від 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();
}
}
Застереження: Я не фахівець, і я знайшов це запитання, оскільки сам шукав відповідь.