Baza Wiedzy

Wątki - wprowadzenie i przykład

Wprowadzenie

Tworząc swoje aplikacje, szczególnie te wykorzystujące interfejs użytkownika z pewnością spotkałeś się z sytuacją, w której pewna czynność, jak na przykład obliczenie wyniku skomplikowanej funkcji, czy pobranie pewnych danych z bazy danych, zabierało dużo czasu, a przez to aplikacja sprawiała wrażenie jakby się zawiesiła. Nie jest to pożądana funkcjonalność i każdy programista chciałby jej uniknąć.

Kurs Programowania Java

Istnieje jednak rozwiązanie, które w stosunkowo prosty sposób pozwala poradzić sobie z tym problemem – są to wątki.

Wątki pozwalają na symultaniczne wykonywanie pewnych operacji dzięki czemu czas wykonania pewnych operacji można znacząco skrócić. W przypadku przykładu z „zawieszeniem” się interfejsu użytkownika można pewne skomplikowane obliczenia wykonać asynchronicznie w tle, dzięki czemu użytkownik aplikacji będzie miał lepsze odczucia w związku z jej użytkowaniem. Na obrazku wygląda to tak:

java_watki

To co istotne to fakt, że zastosowanie wątków sprawdza się nawet na procesorze, który posiada tylko jeden rdzeń. Jest to spowodowane tym, że każdy z wątków może otrzymywać swój czas procesora na wykonanie pewnych operacji. W przypadku jednego wątku nie ma wyjścia – jeśli wchodzimy do funkcji obliczającej skomplikowane równanie, cały interfejs użytkownika jest zamrażany aż do momentu skończenia obliczeń, natomiast jeśli obliczenia uruchomimy w wątku niezależnym od interfejsu będą one naprzemiennie otrzymywały krótki czas procesora i będą sprawiały wrażenie wykonywania się równoległego (a w przypadku procesora wielordzeniowego faktycznie tak będą działały) – oczywiście pomijamy tu fakt tego, że obok nich wykonuje się wiele innych procesów, które również walczą o uzyskanie czasu procesora. Bardzo ważne jest również to, że jeśli uruchomimy jeden po drugim np. 10 wątków, to wcale nie będą one wykonywały się sekwencyjnie jeden po drugim, jeśli sami o to nie zadbamy. Jeśli jakieś zadanie nie zdąży się wykonać w czasie, który został dla niego przydzielony, to może ono zostać przerwane na pewien czas.

Kiedy wykorzystywać wątki? W wielu sytuacjach:

    • Wszelkie obliczenia, które mogą zablokować interfejs użytkownika powinny być wykonywane asynchronicznie
    • Animacje, które powinny być przetwarzane niezależnie od interfejsu użytkownika
    • Pobieranie danych z internetu (zamiast przetwarzać strony internetowe jedna po drugiej można połączyć się np. z 10 jednocześnie)
    • W ogólności wszystkie operacje wejścia/wyjścia, zapis i odczyt plików, czy baz danych
    • Złożone obliczenia, które mogą być podzielone na mniejsze podzadania
    • I wiele innych

    Mając za sobą ten krótki wstęp teoretyczny przejdźmy więc do rzeczy bardziej praktycznych.

    Wątki w Javie - podstawy i przykład

    Wątki w Javie można tworzyć na kilka sposobów, poprzez:

      • jawne rozszerzenie klasy Thread
      • stworzenie klasy implementującej interfejs Runnable, który może być wykonany w osobnym wątku (Thread)
      • stworzenie klasy implementującej interfejs Callable, który może być wykonany w osobnym wątku (Thread)

      Preferowane jest stosowanie rozszerzeń interfejsów (czyli 2 i 3 punkt), ponieważ dają one dużo lepszą elastyczność, szczególnie jeśli dojdziemy do momentu szeregowania wątków, utrzymywania stałej puli wątków wykonujących się w tle. Interfejsy Runnable i Callable są do siebie bardzo podobne, jednak najważniejszą różnicą jest to, że Callable może zwrócić w wyniku pewną wartość, natomiast w przypadku Runnable nie ma takiej możliwości.

      Napiszmy prostą aplikację, która utworzy 10 wątków, z których każdy będzie miał za zadanie jedynie wyświetlenie swojego przypisanego ID, a następnie wstrzymanie swojego działania na krótki okres czasu – i tak w kółko.

      MyRun.java - klasa pozwalająca obiekt, który będzie wykonywany w osobnym wątku

      public class MyRun implements Runnable {
      
      	private int id;
      
      	public MyRun(int id) {
      		this.id = id;
      	}
      
      	@Override
      	public void run() {
      		while(true) {
      			System.out.println("Watek "+id);
      			try {
      				//usypiamy wątek na 100 milisekund
      				Thread.sleep(100);
      			} catch (InterruptedException e) {
      				e.printStackTrace();
      			}
      		}
      	}
      }

      Jak widać interfejs Runnable posiada jedną metodę, którą musimy zaimplementować- run(). Wszystko co się w niej znajduje zostanie wykonane po uruchomieniu wątku, do którego przekażemy obiekt klasy MyRun. My utworzyliśmy wewnątrz niej nieskończoną pętlę, której zadaniem jest wyświetlanie ID wątku, a następnie wstrzymanie działania na 100 milisekund za pomocą statycznej metody sleep().

      Runner.java - klasa testowa

      public class Runner {
      	public static void main(String[] args) {
      		Runnable[] runners = new Runnable[10];
      		Thread[] threads = new Thread[10];
      
      		for(int i=0; i<10; i++) {
      			runners[i] = new MyRun(i);
      		}
      
      		for(int i=0; i<10; i++) {
      			threads[i] = new Thread(runners[i]);
      		}
      
      		for(int i=0; i<10; i++) {
      			threads[i].start();
      		}
      	}
      }

      Wynik:

      Watek 0
      Watek 3
      Watek 1
      Watek 7
      Watek 5
      Watek 2
      ...

      W klasie Runner utworzyliśmy tablicę typu Runnable (do której przypiszemy nasze obiekty MyRun) oraz drugą tablicę obiektów Thread - czyli tabilcę, która będzie przechowywała obiekty wątków, które będziemy mogli uruchomić.

      W pierwszej pętli for tworzymy obiektu typu MyRun. Obiekty te to jednak nie są jeszcze obiekty wątków - te tworzymy w drugiej pętli, jak widać w celu utworzenia obiektu Thread przekazujemy w konstruktorze nasze wcześniej utworzone obiekty MyRun (rozszerzające Runnable).

      W trzeciej pętli nie pozostaje nam nic innego jak uruchomienie wątki poprzez wywołanie metody start() - powoduje ona rozpoczęcie wykonania kodu, który stworzyliśmy w metodzie run().

      Co ciekawe, jeśli spojrzymy na wydruk programu, to może on być po chwili nieco zaskakujący, ponieważ numery wątków nie będą wyświetlały się w kolejności tworzenia obiektów, ale w kolejności losowej - jest to związane z tym o czym pisałem we wprowadzeniu - możliwości losowego przypisywania czasu procesora różnym wątkom.

      Kurs Programowania Java

      Dla porównania zobaczmy co się stanie, jeśli zupełnie pominiemy kwestię wątków w przypadku naszego kodu.

      public class MyRun {
      
      	private int id;
      
      	public MyRun(int id) {
      		this.id = id;
      	}
      
      	public void run() {
      		while(true) {
      			System.out.println("NieWatek "+id);
      			try {
      				//usypiamy wątek na 100 milisekund
      				Thread.sleep(100);
      			} catch (InterruptedException e) {
      				e.printStackTrace();
      			}
      		}
      	}
      }

      Tym razem nie implementujemy interfejsu Runnable - tworzymy jedynie nieskończoną pętlę wewnątrz metody run().

      public class Runner {
      	public static void main(String[] args) {
      		MyRun[] notRunners = new MyRun[10];
      
      		for(int i=0; i<10; i++) {
      			notRunners[i] = new MyRun(i);
      		}
      
      		for(int i=0; i<10; i++) {
      			notRunners[i].run();
      		}
      	}
      }

      Wynik:

      NieWatek 0
      NieWatek 0
      NieWatek 0
      NieWatek 0
      ...

      Tworzymy tablicę obiektów MyRun, przypisujemy do niej obiekty, a następnie wywołujemy metodę run(). Jak się jednak okazuje w konsoli eclipse wyświetli nam się jedynie "Watek 0", co jest spowodowane tym, że kod wykonywany jest w tym przypadku sekwencyjnie - wchodząc do pętli pierwszego obiektu nasza aplikacja się w niej zawiesza i wykonuje bez końca, tak więc metoda run() obiektu o id=1 nie wykona się nigdy.

      Na koniec zachęcam do zapoznania się z dokumentacją klas: Thread, Runnable. W kolejnych lekcjach mam nadzieję poruszyć tematy nieco bardziej zaawansowane takie jak synchronizacja, zarządzanie wątkami, utrzymywanie stałej puli wątków, czy ich wykorzystanie w połączeniu z klasą Robot do pisania aplikacji automatyzujących pewne czynności, czy tworzenia botów.

      Temat wątków w Javie warto śledzić, ponieważ są to jedne z głównych usprawnień wprowadzonych zarówno w wersji 7 jak i planowanej wersji 8.

      Zapisz się do newslettera

      Otrzymuj nasz Newsletter z przykładowymi pytaniami rekrutacyjnymi, wyzwaniami programistycznymi i nowościami ze świata Javy, a także informacje o nowych kursach i promocjach.

      Traktujemy Twoją prywatność poważnie. Nikomu nie udostępniamy Twojego maila no i zawsze możesz się wypisać.

      Komentarze do artykułu

      Wyłączyliśmy możliwość dodawania komentarzy. Poniżej znajdziesz archiwalne wpisy z czasów gdy strona była jeszcze hobbystycznym blogiem. Zapraszamy natomiast do zadawnia pytań i dyskusji na naszej grupe na facebooku.

      Grzesiek

      Dzieki za przejrzyste wprowadzenie do watkow. Czekam na temat synchronizacji... Pytanie: zaimplementowalem 1-szy przyklad klasy Runner stosujac 1 wspolna petle, zamiast trzech. Czy powinna pojawic sie roznica w outpucie?

      Marcin Kunert

      Wynik nie powinien się za bardzo różnić jeśli chodzi o jego istotę. Wszystko zależy od tego dla którego wątku wirtualna maszyna javy zdecyduje się przydzielić czas procesora. Ponieważ tworzenie wątku nie jest szczególnie czasochłonne, to czy utworzysz wszystkie w jednej pętli i uruchomisz w innej, czy zrobisz to jednocześnie nie ma większego znaczenia.

      MrPhi

      Co to znaczy asynchronicznie ? Mógłbyś rozwinąć ?

      lolo

      Chodzi o to, że będzie można dokonywać obliczeń niezależnie od wątku głównego. Nie będzie trzeba czekać aż się wykonają tylko wątku głównym może się dziać coś innego. A gdy się wykonają obliczenia w tle to wywołają odpowiednią funkcję.

      JavaStart na Youtube