Многопоточност и паралелизъм в Java - 24 октомври 2011 г. - Да научим java заедно!
Пакетът java.util.concurrent съдържа три интерфейса на Executor:
- Изпълнител
- ExecutorService
- ScheduledExecutorService
Този интерфейс е въведен, за да раздели процеса на изпращане на задачи за изпълнение от механизма за изпълнение на всяка конкретна задача. Класовете, които прилагат този интерфейс, представляват обекти, които определят как да се изпълнява всяка конкретна задача.
Този интерфейс е разширение на интерфейса на Executor и добавя следните полезни функции:
- Възможност за спиране на работещ процес
- Възможност за изпълнение не само Runnable обекти, но и java.util.concurrent.Callable. Основната им разлика от Runnable обектите е възможността да връщат стойност към нишката, от която е направено извикването.
- Възможността за връщане на java.util.concurrent.Future обект към извикващата нишка, който съдържа, наред с други неща, връщаната стойност.
ScheduledExecutorService
Този интерфейс по същество е същият ExecutorService, но с възможност за отлагане на началото на изпълнението на задача за определен период от време или планиране на изпълнение на задача след определен интервал от време.
Пример за използване
публичен клас CallableImpl прилага Callable public Integer call() //… връща ново цяло число(someValue); > > //… Callable callable = new CallableImpl(); ExecutorService executor = Executors.newFixedThreadPool(5); Future future = executor.submit(callable); try System.out.println( "Бъдеща стойност: " + future.get()); > catch (Изключение e) e.printStackTrace(); >
Чрез извикване на Executors.newFixedThreadPool беше създаден пул за 5 нишки. Темпо този начин, ако е необходимо да се създаде голям брой нишки , ще се спести време за създаване на нова нишка чрез използване на съществуващи нишки от пула. Извикването на метода get на обект от тип Future накара текущата нишка да изчака върнатата стойност.
Атомни променливи
Атомните класове предоставят възможност за извършване на операции върху основни примитивни и референтни типове атомарно. В като пример разгледайте класа AtomicInteger и неговите основни методи. Както подсказва името, този клас е обвивка около примитивния int тип, предоставящ възможност за атомно актуализиране на неговите стойности.
Помислете за основните методи на този клас:
Използвайте пример 1
private AtomicInteger someValue; //… int previousBits, newBits; do previousValue = someValue.get(); нова стойност = промяна на стойност(предишна стойност); > докато (!someValue.compareAndSet(previousValue, newValue));
Примерът по-горе се опитва да актуализира някаква стойност, докато актуализацията е успешна. Чрез използването на атомарна променлива, ние сме избегнали необходимостта от използване на синхронизация.
AtomicReferenceFieldUpdater
Доста често има ситуации, при които е необходима атомна актуализация само за една от многото употреби на даден обект. Разбира се, в такава ситуация не искате да се натоварвате с работа с атомни типове и да не можете да работите директно с обекта. AtomicReferenceFieldUpdater е перфектен за тази цел. Самото поле class, което ще се използва заедно с AtomicReferenceFieldUpdater, трябва да бъде декларирано с ключовата дума volatile. От примера по-долу ще стане ясно какначин, по който това може да се направи.
Използвайте пример 2
публичен клас Node private volatile InnerNode next; //. > //… частен статичен финал AtomicReferenceFieldUpdater nextUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, InnerNode.class, "следващ"); //… nextUpdater.compareAndSet(currentNode, очакван InnerNode, newInnerNode);
Синхронизатори
Синхронизаторите включват различни видове структури, които отговарят за координирането на потока. Нека да разгледаме някои от тези структури, които бяха добавени в пакета java.util.concurrency:
През 1968 г. E. Dijkstra предложи удобна форма на механизъм за улавяне/освобождаване на ресурси , който той нарече P и V операции върху броещи семафори. Броещият семафор е цяло число променлива, която изпълнява същите функции като заключващ байт. Въпреки това, в той може да приема други положителни цели числа освен "0" и "1". Както можете да разберете от написаното по-горе, семафорите се използват за ограничаване на броя нишки, които използват определен ресурс. Има специални методи за извършване на P и V операции в Java класа java.util.concurrent.Semaphore. acquire – опитва се да получи достъп до ресурса и блокира текущата нишка, докато ресурсът е наличен. tryAc quire - прави опити за достъп до ресурса по време на извикването, без да блокира текущата нишка. release - освобождаване на ресурса. Трябва да се отбележи, че самият Semaphore може да има различна стратегия за получаване на освободен ресурс.
Пример за употреба
наличен частен финален семафор = нов семафор(MAX_AVAILABLE, true);
public Object getItem() хвърля InterruptedException available.acquire(); връщанеgetNextAvailableItem(); >
public void putItem(Object x) if (markAsUnused(x)) available.release(); >
Бариерата е средство за синхронизация, което се използва, за да има набор от нишки, които чакат една друга да завършат на някое място, което е бариера или точка на синхронизация. След като всички нишки достигнат точката на синхронизация, те се деблокират и могат да продължат да се изпълняват. На практика бариерите се използват за събиране на резултатите от някои паралелизирани задачи. В като пример можем да разгледаме проблема с умножението на матрицата. При паралелизиране на тази задача, всяка нишка ще бъде натоварена със задачата да умножи определени редове по определени колони. В точката на синхронизация резултатите се събират от всички нишки и резултантната матрица се изгражда. В пакета java.util.concurrent класът CyclicBarrier е имплементацията на бариерата. Помислете за пример за използването му по-долу.
Пример за използване
class Worker extends Thread //… @Override public void run() < // Някои действия опитайте < barrier.await(); > catch (InterruptedException e) < e.printStackTrace(); > catch (BrokenBarrierException e) < e.printStackTrace(); > > > //… бариера = нова CyclicBarrier(N, нова Runnable() public void run() < // Действия, които се изпълняват, когато всички нишки достигнат бариерата > >); for (int i = 0; i exchanger = new Exchanger (); клас Loop1 имплементира Runnable public void run() < Моята > loop1Value = exchanger.exchange(loop1Value); //… > > class Loop2 имплементира Runnable public void run() < M y > loop2Value = exchanger.exchange(loop 2Value); //… > >
Резето е средство за синхронизиране, което се използва, за да позволи на една или повече нишки да изчакат, докато се извършат определен брой операции върху други нишки. Класът CountDownLatch на пакета java.util.concurrent е средство за синхронизиране, което има горните свойства. Този клас работи на принципа на таймера. Инициализира се с някаква начална стойност и отброяване. При извикване на метода await на този клас от която и да е нишка, тя преминава в състояние на изчакване за момента, когато броячът таймер достигне стойност 0. На практика този клас е удобен за използване за координиране на началото и края на определен брой нишки. Това означава следното:
- можете да накарате определен брой нишки да стартират едновременно;
- можете да проследите края на няколко нишки.
Случай на употреба 1
В първия пример разгледайте първия изброен по-горе случай, т.е. започнете да изпълнявате няколко нишки едновременно.