Wprowadzenie do programowania współbieżnego. Java util.concurent review... 1
JavaCzyHerbata
Przetwarzanie współbieżne (ang. concurrent computing) – przetwarzanie oparte na współistnieniu wielu wątków lub procesów, operujących na współdzielonych danych. Wątki uruchomione na tym samym procesorze są przełączane w krótkich przedziałach czasu, co sprawia wrażenie, że wykonują się równolegle. - tyle z Wikipedii ...
***
W tym artykule nie będę się skupiać na teorii, pokażę kilka podstawowych klas i interfejsów jakie programiści mają do swojej dyspozycji aby móc tworzyć programy z użyciem wątków. Oczywiście w początkowych lekcjach skupimy się na podstawowych klockach z których możemy tworzyć nasze programy, nie mniej jednak jak później zobaczymy niektóre wykorzystywane przez nas klasy, mechanizmy będą tylko tłem dla wiedzy jaką chcę przekazać. Java oferuje nam dużo bardziej wyszukane mechanizmy pozwalające na obsługę wątków, ich wykonywania, odbierania od nich zadań.
Dodatkowo Java 8 wprowadza nowy interfejs CompletableFuture umożliwiający nam na programowanie reaktywne, tworząc kod bardziej czytelnym i zrozumiałym. Nasze api nie zawsze musi być napisane w tradycyjny sposób, mam na myśli klient, czeka na zakończenie wywołanej przez niego metody (blokujące api), możemy mieć api, które będzie nam zwracać wynik, o którym zostaniemy powiadomieni, a wtedy możemy wykonać pewną akcję, akcja ta zostanie wywołana niezwłocznie po zakończeniu zadania, lecz bez blokowania wątku, z którego została wywołana nasza metoda. Z programowaniem współbieżnym wiąże się, także inne pojęcie synchronizacji, które jest kluczowym elementem jeśli nasze wątki operują na dzielonym zasobie, taki zasób utożsamiany będzie z pojęciem sekcji krytycznej... ale to wszystko później. Na początku skupmy się na podstawach.
Przykłady, które będą umieszczone na stronie są pisane z użyciem wersji kompilatora Java 8. Sam wstęp ma pokazać pewien zarys jeśli chodzi o pewien koncept programowania współbieżnego oraz od strony technicznej jak wygląda to po stronie języka.
Spójrzmy na definicję interfejsu Runnable z pakietu java.util
* @author Arthur van Hoff
* @see java.lang.Thread
* @see java.util.concurrent.Callable
* @since JDK1.0
*/
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
Co prawda interfejs ten nie należy do pakietu java.util.concurent, ale nie da się go pominąć chcąc przybliżyć możliwości jakie oferuje nam Java w kontekście programowania współbieżnego, lub po prostu nazwijmy to programowania z użyciem wątków.
Jak widać od wersji Javy 8 jest on opatrzony w adnotację @FunctionalInterface co oznacza, że jest to interfejs funkcyjny posiadający jedną abstrakcyjną metodę.
Interfejs ten powinien być implementowany przez klasę, która ma reprezentować wykonanie pewnej operacji w wątku. Operację jaką chcemy wykonać, albo inaczej zadanie jest implementacją metody run.
Jak wiadomo w Javie nie musimy jawnie deklarować klasy, która ma swoją nazwę, zaś możemy utworzyć implementację interfejsu za pomocą klasy anonimowej. Przejdźmy zatem do przykładu. Stworzymy w nim dwie implementacje metody run z interfejsu Runnable.
public class RunnableExample1 {
public static void main(String[] args) {
//old style
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Start method r1");
}
};
Runnable r2 = () -> System.out.println("Start method r2");
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
W linijce opatrzonej komentarzem //old style utworzyliśmy implementację interfejsu z użyciem klasy anonimowej, a poniżej niej mamy bardziej eleganckie rozwiązanie, które oferuje nam Java 8. Użyliśmy tutaj wyrażenia lambda, inaczej mówiąc jest to funkcja anonimowa, która może przyjąć jakiś argument i go zwrócić lub nie.
Interfejs Runnable definiuje nam funkcję taką w postaci metody run, która nie przyjmuje żadnego argumentu oraz nie zwraca żadnego rezultatu. Możemy to rozumieć na naszym przykładzie, że po operatorze -> przekazujemy ciało metody, w skrócie funkcję, która ma się wykonać w wątku.
Samo zadeklarowanie klasy implementującej interfejs Runnable nie wystarcza aby nasz kod uruchomił się w oddzielnym wątku, potrzebujemy klasy, która będzie reprezentować ten wątek.
Klasa Thread z pakietu java.lang w jednym z konstruktorów przyjmuje właśnie obiekt Runnable, którego metoda run zostanie wywołana przy starcie nowego wątku.
/**
* Initializes a Thread with the current AccessControlContext.
* @see #init(ThreadGroup,Runnable,String,long,AccessControlContext)
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null);
}
Wątek w ramach JVM zawsze musi należeć do pewnej grupy wątków. Grupa wątków reprezentowana przez klasę java.lang.ThreadGroup jest tworzona przy starcie maszyny wirtualnej o domyślnej nazwie "system". Grupy są zorganizowane w drzewo służą logicznemu podziałowi wątków.
*
* @author unascribed
* @since JDK1.0
*/
/* The locking strategy for this code is to try to lock only one level of the
* tree wherever possible, but otherwise to lock from the bottom up.
* That is, from child thread groups to parents.
* This has the advantage of limiting the number of locks that need to be held
* and in particular avoids having to grab the lock for the root thread group,
* (or a global lock) which would be a source of contention on a
* multi-processor system with many thread groups.
* This policy often leads to taking a snapshot of the state of a thread group
* and working off of that snapshot, rather than holding the thread group locked
* while we work on the children.
*/
public
class ThreadGroup implements Thread.UncaughtExceptionHandler {
private final ThreadGroup parent;
String name;
int maxPriority;
boolean destroyed;
boolean daemon;
boolean vmAllowSuspension;
int nUnstartedThreads = 0;
int nthreads;
Thread threads[];
int ngroups;
ThreadGroup groups[];
/**
* Creates an empty Thread group that is not in any Thread group.
* This method is used to create the system Thread group.
*/
private ThreadGroup() { // called from C code
this.name = "system";
this.maxPriority = Thread.MAX_PRIORITY;
this.parent = null;
}
Poniżej widzimy, że w przypadku kiedy nie określimy w konstruktorze Thread, do jakiej grupy ma być przydzielony wątek zostanie wywołana metoda init z parametrem null.
Najpierw pobierany jest aktualny wątek wykonujący się w ramach metody init, a następnie zostaje określona grupa do, której zostanie przydzielony wątek.
/**
* Initializes a Thread.
*
* @param g the Thread group
* @param target the object whose run() method gets called
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name.toCharArray();
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
Takie grupowanie daje pewną elastyczność oraz umożliwia wprowadzenie pewnych nazwijmy polityk bezpieczeństwa. Każdy wątek może należeć do jednej grupy, oraz możemy sprawdzić czy dany wątek próbuje utworzyć wątek z innej grupy.
Poniżej mamy przykład programu wypisujący z bieżącego wątku wszystkie grupy w hierarchii drzewa.
public class ThreadGroupExample1 {
public static void printThreadGroupSystemTreeFromCurrentThread() {
ThreadGroup parentThreadGroup = getThreadGroupOfCurrentThread();
while (parentThreadGroup != null) {
System.out.println(parentThreadGroup.getName());
parentThreadGroup = parentThreadGroup.getParent();
}
}
private static ThreadGroup getThreadGroupOfCurrentThread() {
return Thread.currentThread().getThreadGroup();
}
public static void main(String[] args) {
printThreadGroupSystemTreeFromCurrentThread();
}
}
W prosty sposób możemy napisać program wypisujący nazwy wątków działających w maszynie wirtualnej Java.
Metoda printAllThreadsCurrentlyRunningInJVM na początku znajduje korzeń drzewa, a potem wywołuje metodę enumerate, która wypełnia podaną przez nas tablicę.
public class ThreadGroupExample2 {
public static void printAllThreadsCurrentlyRunningInJVM() {
ThreadGroup parentThreadGroup = findRootGroup();
// estimate because the number of threads may change dynamically
int activeThreadsInGroupAndSubGroups = parentThreadGroup.activeCount();
Thread[] threads = new Thread[activeThreadsInGroupAndSubGroups];
parentThreadGroup.enumerate(threads);
printThreadsNames(threads);
}
private static void printThreadsNames(Thread[] threads) {
Stream.of(threads).forEach(thread ->
System.out.println("Thread: " + thread.getName())
);
}
private static ThreadGroup findRootGroup() {
ThreadGroup rootGroup = getThreadGroupOfCurrentThread();
ThreadGroup parentGroup;
while ((parentGroup = rootGroup.getParent()) != null) {
rootGroup = parentGroup;
}
return rootGroup;
}
private static ThreadGroup getThreadGroupOfCurrentThread() {
return Thread.currentThread().getThreadGroup();
}
public static void main(String[] args) {
printAllThreadsCurrentlyRunningInJVM();
}
}
W następnej lekcji skupimy się na klasie Thread, na jej metodach na stanach w jakich może znajdować się wątek.
Api pakietu java.util.concurent w wersji 7 (nie znalazłem nigdzie 8, a nie chce mi się bawić w malarza uml)
Źródło: http://kawaczyjava.blogspot.com/2016/12/wprowadzenie-do-programowania.html