Java 101: Java паралелност без болка, Част 1

С нарастващата сложност на едновременните приложения, много разработчици откриват, че възможностите на Java за нишки на нишки са недостатъчни за нуждите им от програмиране. В този случай може да е време да откриете Java Concurrency Utilities. Започнете с java.util.concurrentподробното въведение на Джеф Фризън в рамката Executor, типовете синхронизатори и пакета Java Concurrent Collections.

Java 101: Следващото поколение

Първата статия в тази нова серия JavaWorld представя Java API за дата и час .

Платформата Java предоставя възможности за нишки на ниско ниво, които позволяват на разработчиците да пишат едновременно приложения, при които различни нишки се изпълняват едновременно. Стандартната нишка на Java обаче има някои недостатъци:

  • Ниско ниво едновременност примитиви на Java ( synchronized, volatile, wait(), notify(), и notifyAll()) не са лесно да се използват правилно. Опасностите от нишки като блокиране, глад на нишки и условия на състезание, които са резултат от неправилно използване на примитиви, също са трудни за откриване и отстраняване на грешки.
  • Разчитането на synchronizedкоординиране на достъпа между нишките води до проблеми с производителността, които засягат мащабируемостта на приложението, изискване за много съвременни приложения.
  • Основните възможности за резбоване на Java са твърде ниски. Разработчиците често се нуждаят от конструкции от по-високо ниво като семафори и пулове от нишки, които възможностите за ниско ниво на нишките на Java не предлагат. В резултат на това разработчиците ще създадат свои собствени конструкции, което отнема много време и е склонно към грешки.

Рамката JSR 166: Concurrency Utilities е създадена, за да отговори на необходимостта от устройство за резби на високо ниво. Инициирана в началото на 2002 г., рамката е формализирана и внедрена две години по-късно в Java 5. Следват подобрения в Java 6, Java 7 и предстоящата Java 8.

Тази двучастична Java 101: Следващото поколение представя разработчици на софтуер, запознати с основните Java нишки в пакетите и рамката на Java Concurrency Utilities. В част 1 представям общ преглед на рамката за Java Concurrency Utilities и представям нейната Executor рамка, помощни програми за синхронизация и пакета Java Concurrent Collections.

Разбиране на нишките Java

Преди да се потопите в тази серия, уверете се, че сте запознати с основите на резбата. Започнете с въведението на Java 101 за възможностите за нишки на нишките на Java:

  • Част 1: Представяне на нишки и изпълними
  • Част 2: Синхронизация на нишки
  • Част 3: Планиране на нишки, изчакване / уведомяване и прекъсване на нишки
  • Част 4: Групи нишки, волатилност, локални променливи на нишки, таймери и смърт на нишки

Вътре в помощните програми на Java Concurrency

Рамката Java Concurrency Utilities е библиотека от типове, които са проектирани да се използват като градивни елементи за създаване на едновременни класове или приложения. Тези типове са обезопасени с резби, тествани са щателно и предлагат висока производителност.

Типовете в помощните програми на Java са организирани в малки рамки; а именно, Executor framework, синхронизатор, едновременни колекции, брави, атомни променливи и Fork / Join. Освен това те са организирани в основен пакет и чифт подпакети:

  • java.util.concurrent съдържа типове помощни програми на високо ниво, които често се използват при едновременно програмиране. Примерите включват семафори, бариери, пулове от нишки и едновременни хеш-карти.
    • В java.util.concurrent.atomic -пакет съдържа ниско ниво комунални класове, че подкрепата за заключване без резба-безопасно програмиране на единични променливи.
    • В java.util.concurrent.locks -пакет съдържа ниско ниво видове комунални за заключване и чакат за условия, които са различни от използване на синхронизация и монитори с ниско ниво на Java.

Рамката Java Concurrency Utilities също излага хардуерната инструкция за сравнение и размяна (CAS) на ниско ниво , варианти на която обикновено се поддържат от съвременните процесори. CAS е много по-лек от механизма за синхронизация, базиран на монитор на Java, и се използва за реализиране на някои силно мащабируеми едновременни класове. java.util.concurrent.locks.ReentrantLockКласът, базиран на CAS , например е по-ефективен от еквивалентния synchronizedпримитив, базиран на монитор . ReentrantLockпредлага повече контрол върху заключването. (В част 2 ще обясня повече за това как работи CAS java.util.concurrent.)

System.nanoTime ()

Рамката Java Concurrency Utilities включва long nanoTime(), която е член на java.lang.Systemкласа. Този метод позволява достъп до източник на време за наносекундна гранулираност за извършване на относителни измервания на времето.

В следващите раздели ще представя три полезни функции на Java Concurrency Utilities, като първо ще обясня защо те са толкова важни за съвременната паралелност и след това ще покажа как работят за увеличаване на скоростта, надеждността, ефективността и мащабируемостта на едновременните Java приложения.

Рамката на Изпълнителя

При резбата задачата е единица работа. Един проблем с нишките на нишки в Java е, че подаването на задачи е тясно свързано с политика за изпълнение на задачата, както е показано в Листинг 1.

Листинг 1. Server.java (Версия 1)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; class Server { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; new Thread(r).start(); } } static void doWork(Socket s) { } }

Горният код описва просто сървърно приложение ( doWork(Socket)оставено празно за краткост). Нишката на сървъра многократно се обажда, за socket.accept()да изчака входяща заявка, и след това стартира нишка, която да обслужва тази заявка, когато пристигне.

Тъй като това приложение създава нова нишка за всяка заявка, то не се мащабира добре, когато е изправено пред огромен брой заявки. Например, всяка създадена нишка изисква памет и твърде много нишки могат да изчерпят наличната памет, принуждавайки приложението да прекрати.

Можете да разрешите този проблем, като промените политиката за изпълнение на задачите. Вместо винаги да създавате нова нишка, можете да използвате пул от нишки, в който фиксиран брой нишки ще обслужва входящи задачи. Трябва обаче да пренапишете приложението, за да направите тази промяна.

java.util.concurrentвключва рамката Executor, малка рамка от типове, които отделят подаването на задачи от политиките за изпълнение на задачите. Използвайки рамката Executor, можете лесно да настроите политиката за изпълнение на задачите на програмата, без да се налага да пренаписвате значително кода си.

Вътре в рамката на изпълнителя

Рамката Executor се основава на Executorинтерфейса, който описва изпълнител като всеки обект, способен да изпълнява java.lang.Runnableзадачи. Този интерфейс декларира следния самотен метод за изпълнение на Runnableзадача:

void execute(Runnable command)

Изпращате Runnableзадача, като я предавате execute(Runnable). Ако изпълнителят не може да изпълни задачата по някаква причина (например, ако изпълнителят е бил изключен), този метод ще хвърли a RejectedExecutionException.

Ключовата концепция е, че подаването на задачи е отделено от политиката за изпълнение на задачата , която е описана чрез Executorизпълнение. По този начин изпълняваната задача може да се изпълни чрез нова нишка, обединена нишка, повикваща нишка и т.н.

Имайте предвид, че Executorе много ограничен. Например не можете да изключите изпълнител или да определите дали асинхронната задача е приключила. Също така не можете да отмените текуща задача. Поради тези и други причини, рамката Executor осигурява интерфейс ExecutorService, който се разширява Executor.

Пет от ExecutorServiceметодите са особено забележителни:

  • boolean awaitTermination (дълго време за изчакване, единица TimeUnit) блокира извикващата нишка, докато всички задачи завършат изпълнението след заявка за изключване, настъпи времето за изчакване или текущата нишка бъде прекъсната, което от двете се случи първо. Максималното време за изчакване е посочено от timeoutи тази стойност се изразява в unitмерните единици, посочени от TimeUnitпреброяването; например TimeUnit.SECONDS,. Този метод хвърля, java.lang.InterruptedExceptionкогато текущата нишка е прекъсната. Той връща true, когато изпълнителят е прекратен, и false, когато изтече времето за изчакване преди прекратяване.
  • boolean isShutdown () връща true, когато изпълнителят е бил изключен.
  • void shutdown () инициира организирано спиране, при което се изпълняват предварително изпратени задачи, но не се приемат нови задачи.
  • Future submit (Callable task) изпраща задача за връщане на стойност за изпълнение и връща Futureпредставяне на чакащите резултати от задачата.
  • Future submit (Runnable task) изпраща Runnableзадача за изпълнение и връща Futureпредставляваща тази задача.

В Futureинтерфейса представлява резултат от асинхронен изчисление. Резултатът е известен като бъдеще, тъй като обикновено няма да бъде достъпен до някакъв момент в бъдещето. Можете да извикате методи, за да отмените задача, да върнете резултата от нея (изчакване за неопределено време или за изтичане на времето за изчакване, когато задачата не е приключила) и да определите дали задача е била отменена или завършена.

В CallableИнтерфейсът е подобен на Runnableинтерфейса с това, че осигурява единен метод описва задача да се изпълни. За разлика Runnableе void run()метод, Callableе V call() throws Exceptionметод може да върне стойност и се хвърли изключение.

Изпълнителни фабрични методи

По някое време ще искате да получите изпълнител. Рамката Executor предоставя Executorsкласа на помощната програма за тази цел. Executorsпредлага няколко фабрични метода за получаване на различни видове изпълнители, които предлагат специфични политики за изпълнение на нишки. Ето три примера:

  • ExecutorService newCachedThreadPool () създава пул от нишки, който създава нови нишки при необходимост, но който използва повторно конструирани нишки, когато са налични. Конци, които не са били използвани в продължение на 60 секунди, се прекратяват и премахват от кеша. Този пул от нишки обикновено подобрява производителността на програми, които изпълняват много краткотрайни асинхронни задачи.
  • ExecutorService newSingleThreadExecutor () създава изпълнител, който използва единична работна нишка, оперираща неограничена опашка - задачите се добавят към опашката и се изпълняват последователно (не повече от една задача е активна наведнъж). Ако тази нишка завърши поради неуспех по време на изпълнение преди изключване на изпълнителя, ще бъде създадена нова нишка, която ще заеме мястото си, когато трябва да бъдат изпълнени следващи задачи.
  • ExecutorService newFixedThreadPool (int nThreads) създава пул от нишки, който използва повторно фиксиран брой нишки, работещи от споделена неограничена опашка. Най-много nThreadsнишките са активно обработващи задачи. Ако се изпратят допълнителни задачи, когато всички нишки са активни, те изчакват в опашката, докато дадена нишка е налична. Ако някоя нишка завърши поради неуспех по време на изпълнение преди изключване, ще бъде създадена нова нишка, която да заеме мястото си, когато трябва да бъдат изпълнени следващи задачи. Нишките на пула съществуват, докато изпълнителят бъде изключен.

Рамковите Изпълнител предлага допълнителни видове (като ScheduledExecutorServiceинтерфейс), но видовете които е вероятно да се работи с най-често са ExecutorService, Future, Callable, и Executors.

Вижте java.util.concurrentJavadoc, за да разгледате допълнителни типове.

Работа с рамката Executor

Ще откриете, че с рамката Executor се работи доста лесно. В Листинг 2 използвах Executorи за Executorsда заместя примера на сървъра от Листинг 1 с по-мащабируема алтернатива, базирана на пул от нишки.

Листинг 2. Server.java (Версия 2)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.Executor; import java.util.concurrent.Executors; class Server { static Executor pool = Executors.newFixedThreadPool(5); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; pool.execute(r); } } static void doWork(Socket s) { } }

Листинг 2 използва newFixedThreadPool(int)за получаване на изпълнител, базиран на пул от нишки, който използва повторно пет нишки. Той също така замества new Thread(r).start();с pool.execute(r);за изпълнение на изпълними задачи чрез някоя от тези нишки.

Листинг 3 представя друг пример, в който приложение чете съдържанието на произволна уеб страница. Той извежда получените редове или съобщение за грешка, ако съдържанието не е налично в рамките на максимум пет секунди.