Programowanie wielowątkowe/współbieżne pozwala na realizację zadań programu w tym samym czasie/równolegle.
Wielowątkowość pozwala na zwiększenie wydajności, program nie będzie już realizowany linijka po linijce, wydzielone bloki programu będą działały niezależnie od siebie i w tym samym czasie.
thread – wątek
Wątek ma kilka etapów „życia”:
NEW – został utworzony, jeszcze nie działa, przed wywołaniem metody start
RUNNABLE – w gotowości do działania lub działający
BLOCKED – oczekujący na odblokowanie zasobów
WAITING – oczekujący aż inny wątek się zakończy, zwykle powiązane jest to z metodą join()
TIMED_WAITING – uśpiony, związany z metodą sleep()
TERMINATED – zakończył pracę
etap wątku możemy sprawdzić metodą getState()
Przykład: stwórzmy najprostszy wątek odliczający
//tworzymy nową klasę
class NewThread extends Thread{
//każdy z wątków będzie usypiany na inny czas, oczywiście czasem zarządza Java, my jedynie możemy jej przekazać że bardzo nam zależy żeby było jak napisaliśmy
public int sleepTime;
//tworzymy konstruktor, by móc nazywać wątki
public NewThread(String name, int sleepTime) {
super(name);
this.sleepTime = sleepTime;
}
public void run(){
for (int i=1; i<=10; i++){
System.out.println(this.getName() + " i: " + i);
//chcemy uśpić wątek na sekundę
try {
this.sleep(this.sleepTime);
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public class ThreadsExample {
public static void main(String[] args) {
NewThread t1 = new NewThread("Wątek1", 1000); //wątek NEW
System.out.println(t1.getState());//możemy sprawdzić stan wątku
t1.start();//aby uruchomić wątek należy go wystarować
System.out.println(t1.getState());
NewThread t2 = new NewThread("Wątek2", 800);
t2.start();
}
}
Race condition
Podczas pracy z wątkami należy uważać na sytuację, kiedy kilka wątków pracuje na tym samym zasobie.
Wyobraźmy sobie, że te wątki pobierają aktualną wartość i ją modyfikują, który wątek będzie pierwszy? czy wartość, którą pobrał wątek została już zmodyfikowana przez inny wątek? czy może jeszcze nie?
Taka sytuacja, gdzie kilka wątków jednocześnie odczytuje i modyfikuje identyczne dane nazywa się wyścigiem – race condition.
Taki problem możemy rozwiązać dzięki słówku synchronized, który umieszczamy przy tworzeniu metody. Zapewnia ono, że kilka współistniejących wątków nie wykona w tym samym czasie tego samego kodu. Uniemożliwia wielu wątkom współdzielenie zasobu.
Aby zrozumieć problem wyścigu przeanalizujmy poniższy kod, gdzie mamy do czynienia z 2 wątkami, które jednocześnie wywołują metodę increment, która zwiększa o 1 wartość zmiennej counter.
class ThreadCounter implements Runnable{
ThreadsRaceCondition app;
public ThreadCounter(ThreadsRaceCondition app) {
this.app = app;
}
@Override
public void run() {
for (int a = 0; a < 100000; a++){
app.increment();
}
}
}
public class ThreadsRaceCondition {
public int counter = 0;
public /*synchronized*/ void increment(){ //dodanie słówka synchronized eliminuje problem wyścigu
this.counter++;
}
public int getCounter(){
return counter;
}
public static void main(String[] args) throws InterruptedException{
ThreadsRaceCondition t = new ThreadsRaceCondition();
Thread th1 = new Thread(new ThreadCounter(t));
Thread th2 = new Thread(new ThreadCounter(t));
th1.start();
th2.start();
th1.join(); //main poczeka na oba wątki, join() pozwala nakazać wątkowi, aby poczekał aż poprzedni wątek zakończy działanie
th2.join();
System.out.println(t.getCounter());
//różne wyniki przed użyciem słówka synchronized: 188983 137796 120909
//po dodaniu synchronized zawsze otrzymamy poprawny wynik 200000
/* jeżeli chcemy synchronizować określony blok kodu używamy zapisu:
public void increment(){
synchronized(this){//i to co jest w {} zostanie zsynchronizowane
this.counter++;
}
}
Używając powyższego rozwiązania korzystamy z tzw monitora, czyli obiektu, który zostaje odblokowany lub zablokowany. I w powyższym przykładzei określamy go jako this, chyba że korzystamy z innego obiektu np. listy to przekazujemy w nawiasie nie this, a właśnie ArrayList
Nie należy nadużywać synchronized, ponieważ powoduje to spowolnienie działania programu
*/
}
}
Wątki działają równolegle i każdy ma iterację w pętli for po 100000 razy, czyli po zakończeniu pracy wynik powinien wynosić 200000, ale tak się nie dzieje. Co więcej, za każdym razem otrzymujemy inny wynik.
W każdej iteracji pętli wątki pobierają wartość counter i zwiększają ją o 1, jeżeli np. w aktualnym momencie wartość zmiennej wynosi 4, pobierają ją oba wątki, zwiększają o 1 i zapisują, to wartość counter zamiast wynosić 6, będzie wynosiła 5 – co jest błędem.
Podobny problem może nastąpić jeżeli rdzeń procesora przydzieli danemu wątkowi więcej czasu na wykonanie operacji lub wątek zostanie wywłaszczony(uśpiony).
Np.
w1 i w2 startują, więc pobierają wartość counter 0;
w1 zostaje uśpiony na jakiś czas;
w2 w tym czasie zwiększa wartość counter w 4 iteracjach i zapisuje 4
w1 się wybudza, ale w pamięci ma 0, które zwiększa o 1 i do zmiennej counter zapisuje 1
i tak właśnie tracone są dane.
Wątki należy synchronizować.
Oczekiwanie na dane
Często zachodzi potrzeba, aby zaczekać na dane, które są przetwarzane w innym wątki.
W takim przypadku używamy wait i notify.
Napiszmy apkę dla oczekiwania na dostarczenie pizzy.
import java.util.LinkedList;
class House{
public LinkedList<String> delivery = new LinkedList<>();
public void waitForDelivery(){
//tworzymy blok synchronized ponieważ na tym obiekcie mogą jednocześnie pracować 2 wątki
synchronized (delivery) {
//pętla sprawdzi czy delivery nie jest pusta. Jeżeli kolekcja jest pusta to będziemy czekali na powiadomienie o zmianie tego stanu
System.out.println("Oczekiwanie na dostawę");
while(delivery.isEmpty()){//pętla zabezpiecza wątek przed nieplanowanym wybudzeniem
try{
delivery.wait();
} catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("Pizza odebrana: " + delivery.poll());
}
}
public void pizzaGuy(){
synchronized (delivery){
System.out.println("Pizza dostarczona");
delivery.add("Pepperoni");
//teraz musimy powiadomić wątek, który czekał
delivery.notify();//to wybudzi wątek, który czekał
}
}
}
public class ThreadsWaitNotify {
public static void main(String[] args) throws InterruptedException{
//tworzymy obiekt i wątki
House house = new House();
Thread customer = new Thread(new Runnable() {//wątek klienta
@Override
public void run() {
house.waitForDelivery();
}
});
customer.start();//ten wątek będzie czekał, aż w delivery coś się zmieni
Thread.sleep(3000);//symulujemy, że coś się z pizzą dzieje
//kolejny wątek
Thread producer = new Thread(new Runnable() {
@Override
public void run() {
house.pizzaGuy(); //w tym wątku mamy dodanie elementu do delivery, a następnie wywołanie metody notify(), co spowoduje wybudzenie 1 wątku i wyświetlenie informacji, że pizza została dostarczona i element zostanie zdjęty z listy przez pool
}
});
producer.start();
customer.join();
}
}
Interfejs Runnable
Zamiast korzystać z klasy Thread możemy skorzystać z interfejsu Runnable.
Pozwala to na rozszerzanie wielu klas.
Przykład:
class RunnableC implements Runnable{
private int sleepTime;
private String threadName;
public RunnableC(int sleepTime, String threadName) {
this.sleepTime = sleepTime;
this.threadName = threadName;
}
@Override
public void run() {
for (int i=1; i<=10; i++){
System.out.println(threadName + ", wartość: " + i);
//chcemy uśpić wątek na sekundę
try {
Thread.sleep(sleepTime); //Thread.sleep usypia tylko aktualnie wykonywany wątek
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
//jeżeli chcemy się pozbyć Thread możemy stworzyć nową klasę
class RunnableC2 extends RunnableC implements Runnable{
private Thread thread;
public RunnableC2(int sleepTime, String threadName) {
super(sleepTime, threadName);
}
public void start(){
if (thread == null){
thread = new Thread(this);
thread.start();
}
}
}
public class RunnableExample {
public static void main(String[] args) {
RunnableC t1 = new RunnableC(500, "Wątek 1 ");
Thread th1 = new Thread(t1, "Wątek 1 ");
th1.start();
RunnableC t2 = new RunnableC(1000, "Wątek 2 ");
Thread th2 = new Thread(t2, "Wątek 2 ");
th2.start();
RunnableC2 t3 = new RunnableC2(300, "Wątek 3 ");
t3.start();
}
}
Wątek na bazie klasy anonimowej
public class AnonimusRunnableExample {
public static void main(String[] args) throws InterruptedException {/*nie będziemy robili przechwytywania ewentualnego błędu, zapiszemy że może taki wystąpić */
Thread thread = new Thread(new Runnable() {//jak zaczniemy pisać new Ru... intellij podpowie {}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("i = " + i);
}
}
});
thread.start();
//sprawdzimy czy wątek jest zakończony
Thread.sleep(200);
System.out.println(thread.getState());
}
}
Wątki i Lambda
public class LambdaThread {
public static void main(String[] args) {
Thread thread = new Thread( () -> {
for (int i=0; i<5; i++){
System.out.println("i = " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
}
});
thread.start();
}
}
Wątek demoniczny
Wątki demoniczne to najczęściej wątki, które monitorują stan aplikacji (jednym z takich wątków jest GarbageCollector),
nie wpływają bezpośrednio na działanie apki i mogą być „odgórnie” zakończone przez JVM, gdy kończy ona pracę z wątkami klienckimi np main.
Należy pamiętać , aby umieszczać w tych wątkach takie zadania, które mogą zostać gwałtownie przerwane.
Przykład:
public class ThreadsDemon {
public static void main(String[] args) throws InterruptedException{
//tworzymy nowy wątek
Thread th = new Thread(new Runnable() {
@Override
public void run() {
//tworzymy nieskończoną pętlę, bo i tak zostanie ona zamknięta gdy main się zakończy
int i = 0; //tworzymy zmienną by podejrzeć działanie wątku w tle
while(true) {
try{
Thread.sleep(1000);
System.out.println("Wątek demoniczny: " + i);
i++;
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
});
//najpierw określamy czy wątek jest demoniczny, później starujemy
th.setDaemon(true);
th.start();
//zapauzujmy nasz główny wątek kliencki
Thread.sleep(5000);//w tym czasie będzie działał nasz wątek demoniczny, a po 5 sekundach zakończy się działanie wątków
System.out.println("Zakończenie działania programu.");
}
}
Zadanie: Napisz program – Zegar
import javax.swing.*;
import java.awt.*;
import java.time.LocalDateTime;
import java.time.chrono.ChronoLocalDateTime;
//skorzystamy z klasy JFrame i Runnable
public class Clock extends JFrame implements Runnable{
//dodajemy prywatną zmienną, która będzie przechowywała aktualny wątek
private Thread thread;
//deklarujemy zmienne określające h, m i s
String hour, min, sec;
//teraz label, w którym przechowamy informację
JLabel label;
//tworzymy pusty konstruktor z wątkiem
public Clock() {
label = new JLabel("", JLabel.CENTER); //dodajemy nowy label
label.setBounds(5,5,100,40); //ustawiamy położenie o wielkość labela
add(label); //dodajemy nasz label do JFrame
setSize(100,80); //wymiery okna
setLayout(null);//mamy bounds więc ustawiamy null, nie określamy layoutu
setVisible(true); //pokazujemy okienko
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //co się stanie po kliknięciu X
thread = new Thread(this);
thread.start();
}
@Override
public void run() {
try{
//tworzymy nieskończoną pętlę, bo czas będzie liczony bez końca
while (true){
//pobieramy aktualny czas
LocalDateTime today = LocalDateTime.now();
hour = "" + today.getHour();
//sformatujemy wyświetlanie minut do 2 cyfr
min = String.format("%02d",today.getMinute()); //przekazujemy 2 znaki i liczby całkowite stąd %2d, a jeżeli będzie pobrana pojedyncza liczba to dodajemy 0 stąd %02d
sec = String.format("%02d",today.getSecond()); //przekazujemy 2 znaki i liczby całkowite stąd %2d, a jeżeli będzie pobrana pojedyncza liczba to dodajemy 0 stąd %02d
//dodajemy informację do Labela
label.setText(hour + ":" + min + ":" + sec);
//przed zakończeniem pętli while uśpimy wątek na 1s
thread.sleep(1000);
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
//tworzymy nową instancję zegara
Clock clock = new Clock();
}
}