#23 – Korzystamy z wątków, zadań w tle

By 16 kwietnia 2015Kurs Javy

Wątki to kolejne bardzo potężne narzędzie jezyka programowania, które równie łatwo może stać się ułatwieniem jak i przeszkodą. Na szczęście Java upraszcza nam pracę z wątkami i pozwala uniknąć wielu problemów.

Programowanie wielowątkowe pozwala nam na realizację kilku rzeczy w jednym czasie. Tak naprawdę już teraz z niego korzystałaś, choć nieświadomie – serwer aplikacji (jak np. Tomcat) korzysta z wątków, aby umożliwić wielu klientom korzystanie z Twojej aplikacji jednocześnie. W tej lekcji nauczysz się sama wykorzystywać wątki do zadań, które wymagają więcej czasu – np. wysłanie maila. Użytkownik strony zamiast czekać, aż system wyśle maila i dopiero wtedy zwróci odpowiedź, dostaje odpowiedź od razu, a mail jest wysyłany w tle. Dotyczy to nie tylko wysyłki maili, ale wszystkich operacji, które wymagają czasu – generowania plików PDF, renderowania grafiki (np. diagramów), wykonywania licznych zmian na bazie danych, szyfrowania itp. Zastosowania tego podejścia są naprawdę niezliczone.

Lekcja

Wstęp

Na początku wyjaśnimy sobie kilka pojęć i czym w rzeczywistości są wątki. Ale wszystko po kolei.

Wątki

Z wątkami mamy do czynienia właściwie każdego dnia – systemy, które nie korzystają z wątków są spotykane właściwie tylko przy kontroli systemów produkcyjnych, gdzie czas i kolejność działania są krytyczne, oraz w prostych układach elektronicznych, gdzie układy są zbyt proste do obsługi wątków.

Wątki to sposób procesora na robienie wielu rzeczy rzeczy jednocześnie. W danym momencie czasu procesor może wykonać tylko tyle instrukcji, ile ma rdzeni (w uproszczeniu, w rzeczywistości jest to bardziej skomplikowane, ale takie uproszczenie pozwoli nam łatwiej wytłumaczyć). To by oznaczało, że jednocześnie mógłbyś mieć uruchomione tylko tyle programów ile masz rdzeni procesora – przeważnie dwa lub cztery we współczesnych komputerach. Aby obejść to ograniczenie, procesor pracuje na wątkach – jeden program to jeden (lub więcej) wątków. Wątek zawiera informacje o tym, jaki program jest uruchomiony, co akurat się dzieje oraz jaka część pamięci RAM do niego należy. Procesor przełącza się pomiędzy wątkami wiele razy w ciągu każdej sekundy, przez co użytkownik komputera ma wrażenie, że wiele programów działa jednocześnie. W rzeczywistości większość z nich jest ‚zamrożonych’ i czeka na swoją kolejkę, aby wznowić działanie.

Wielowątkowość

Wielowątkowość to nic innego jak zdolność systemu do wykorzystania (lub obsługi, w zależności od kontekstu) wielu wątków. O aplikacji mówimy, że jest wielowątkowa, jeśli korzysta z wątków, aby lepiej realizować swoje zadania (np. realizuje czasochłonne zadania ‚w tle’, nie opóźniając interakcji z użytkownikiem). Wielowątkowość wiąże się też z problemami – główne z nich to synchronizacja wątków (nie mamy gwarancji, który wątek dostanie więcej ‚czasu’ od procesora) oraz dostęp do zasobów.

Synchronizacja

Problem synchronizacji możemy sobie wyobrazić np w przypadku algorytmu, który dodaje do jakiejś liczby (wspólnej dla wielu wątków) jedynkę, tylko jeśli jest ona zerem. Dwa wątki, które w tym samym czasie (tzn jeden po drugim, ale zanim wykona się kolejna instrukcja) wykonają warunek uznają, że jest on spełniony, po czym oba wątki zwiększą liczbę o jeden. Taka sytuacja nazywa się sytuacją wyścigu i w uproszczeniu chodzi o to, że pomiędzy sprawdzeniem warunków, a wykonaniem czynności, warunki mogą się zmienić.

Java zapewnia wiele mechanizmów, które pozwalają tej sytuacji uniknąć, jednak nie będziemy ich omawiać w tej lekcji. Rozwiązywanie tego typu problemów powinno się odbywać na etapie projektowania i wymaga dużej wiedzy i doświadczenia, aby zrobić to prawidłowo. Póki co skupimy się na podstawach użycia wątków, bez bardziej zaawansowanych zagadnień :)

Uruchamianie i tworzenie wątków w Javie

Java oferuje trzy sposoby na uruchamianie wątków, z których będzie nas interesował tylko ten ostatni:

  • dziedziczenie po klasie Thread – to najgorszy ze sposobów, samodzielnie musimy zarządzać wątkiem i nie mamy prawie żadnych dodatkowych narzędzi; korzystanie z tego sposobu może także negatywnie wpłynąć na działanie aplikacji webowej, narażamy się też na problem związany z nieprawidłowym zakończeniem pracy wątku i niezwolnionymi zasobami
  • implementacja interfejsu Runnable i użycie klasy Thread do jego uruchomienia – to trochę lepsza metoda, ale zasadniczo cierpi na takie same problemy jak punkt pierwszy; z drugiej strony dość łatwo możemy ją przerobić na sposób trzeci
  • implementacja interfejsu Runnable i użycie implementacji ExecutorService – to najlepsza metoda na uruchamianie własnych wątków – pozwala nam tworzyć pule wątków, delegować im zadania, tworzyć zadania cykliczne, kończyć pracę wątków w kontrolowany sposób i wiele więcej; tą metodą zajmiemy się w dzisiejszej lekcji

Interfejsy Runnable oraz ExecutorService

Te dwa interfejsy są kluczowe do korzystania z wątków w Javie. Omówimy tylko ich podstawowe zastosowanie, ale zachęcam do eksperymentów i testowania innych możliwości.

Na początku interfejs Runnable, który deklaruje jedną metodę – run, która nic nie zwraca i nic nie przyjmuje. To centralny punkt naszego wątku – wątek uruchomi tą metodę i będzie działał, dopóki nie skończy się ona wykonywać. Fakt, że nic nie zwraca i nie przyjmuje jest celowy – jeśli chodzi o przyjmowanie danych, można do tego wykorzystać konstruktor lub np. Springa (tak, możemy zadeklarować beana za pomocą @Component, który implementuje również Runnable i używać w nim np. @Autowired). Jeśli chcemy więc zaimplementować prosty wątek, który np. wyśle maile z bazy danych czekające na wysłanie, będzie on wyglądał mniej więcej tak:

@Component
public class EmailSenderRunnable implements Runnable {

    @Autowired
    public EmailDao emailDao;

    public void run() {
        List<Email> emailsToSend = emailDao.fineByStatus(EmailStatus.UNSENT);
        for (Email email : emailsToSend) {
            //do the actual sending, connect to mail server, handle exceptions etc.
        }
    }
}

ExecutorService to z kolei interfejs, którego nie musimy implementować samodzielnie, środowisko dostarcza nam gotowe implementacje. Dzięki temu możemy w prosty sposó utworzyć wątek lub pulę wątków, która będzie działała wg naszych oczekiwań. Najważniejsze metody, które udostępnia ten interfejs, to:

  • submit(Runnable task) – pozwala przesłać ‚zadanie’ (implementację interfejsu Runnable) do wykonania (uwaga: nie mamy gwarancji, czy zostanie ono od razu uruchomione! To zalezy od aktualnego statusu ExecutorService, kolejki zadań, dostępnych wątków etc)
  • shutdown() – pozwala prawidłowo zakończyć pracę wątków, uprzednio kończąc wszystkie zadania i zwalniając wszystkie zasoby. Wywołanie tej metody jest wymagane przed zakończeniem działania aplikacji!

Zobaczmy przykład, jak stworzyć serwis, który korzysta z ExecutorService, który w prawidłowy sposób kończy pracę. Wykorzystamy tutaj interfejs Lifecycle – dzięki niemu, Spring powiadomi naszą klasę o wyłączaniu aplikacji, będziemy więc mogli odpowiednio zamknąć ExecutorService.

Użyliśmy też adnotacji @PostConstruct – wprawdzie mogliśmy to samo zrobić w metodzie start() z interfejsu Lifecycle, ale chciałem przy okazji pokazać, do czego można użyć tej adnotacji. Metoda (publiczna, bez argumentów), którą oznaczymy tą adnotacją, zostanie wywołana w momencie, w którym wszystkie beany Springa zostaną już utworzone i podpięte (tzn. wszystkie pola z adnotacją @Autowired powinny mieć już właściwe obiekty), czyli realnie w momencie, w którym nasza aplikacja jest w pełni uruchomiona.

@Service
public class EmailService implements Lifecycle {

    private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);

    @Autowired
    EmailSenderRunnable emailSenderRunnable;

    @PostConstruct //ta adnotacja powoduje, że metoda wykona się po uruchomieniu aplikacji
    public void setup() {
        threadPool.scheduleAtFixedRate(emailSenderRunnable, 60, 60, TimeUnit.SECONDS)
    }

    public void sendMessage(Email message) {
        //dodaj wiadomość do kolejki wiadomości do wysłania
    }

    // inne metody mail service

    public boolean isRunning() {
        return threadPool.isTerminated();
    }
    public void start() {
        //nic nie rób
    }
    public void stop() {
        threadPool.shutdown();
        while (isRunning()) {
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                //ignoruj wyjątek
            }
        }
    }

}

Tworzenie nowych ExecutorService

W powyższym przykładzie utworzyliśmy pulę wątków za pomocą metody klasy Executors. Przyjrzyjmy się jeszcze jakie inne opcje tworzenia puli mamy dostępne i do czego możemy ich używać:

  • newScheduledThreadPool(int corePoolSize) – tworzy pulę wątków ( o rozmiarze, który przekazujemy jako parametr), która pozwala nam wykonywać zadania np. cyklicznie (zobacz deklarację interfejsu ScheduledExecutingService)
  • newFixedThreadPool(int nThreads) – tworzy pulę o określonej ilości wątkó, które są cały czas aktywne (aż do zamknięcia Executora)
  • newCachedThreadPool() – tworzy pulę, w której wątki są tworzone tylko, jeśli są potrzebne i zamykane po upływie określonego czasu bezczynności, polecam do wykonywania nieczęstych zadań (oszczędza zasoby)
  • newSingleThreadExecutor() – pojedynczy wątek do naprostszych zastosowań

Często pojedynczy wątek to wszystko, czego potrzeba. W zależności od konkretnego przypadku, różne pule mogą się sprawdzać w różnych sytuacjach.

Podsumowanie

W tej lekcji dowiedziałaś się, czym są wątki w Javie, do czego można je wykorzystywać i jak używać ich prawidłowo. Nie było akademickiego (bezsensownego) przykładu ze ślimakami (brr, osoby które miały nieprzyjemnosć uczęszczać na zajęcia z programowania na uczelni na pewno wiedzą, o czym mówię) za to trochę praktyki. Pamiętaj tylko, że więcej nie zawsze znaczy lepiej – zbyt duża ilość wątków może ograniczyć wydajność aplikacji zamiast ją poprawić.

Zadanie

Zmodyfikuj program, który już napisałeś tak, aby używał ExecutorService (uruchamiającego to samo zadanie co minutę) i wypisywał w logu ilość kotów, które są w bazie danych.

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przydatną, mamy małą prośbę: polub nasz fanpage. Dzięki temu będziesz zawsze na bieżąco z nowymi treściami na blogu ( i oczywiście, z nowymi częściami kursu Javy). Dzięki!

  •  
  •  
  •  
  •  
  •  
  • Ania Janowska

    Witam mam pytanie:) Napisałam program, który odtwarza kilka plików muzycznych jeden po drugim.
    Program ma 2 przyciski ( Play i Stop)

    Bardzo mi zależy bym mogła zatrzymać odtwarzanie w każdej chwili, ale naciśnięty przycisk play uniemożliwia naciśnięcie przycisku stop. Dlatego myślałam żeby stworzyć metodę (play) w jednym wątku.by nie blokował przycisku (stop).

    Następnie przycisk stop zatrzymywał by cały wątek. Czy to dobry pomysł? to pomoże? Pozdrawiam i z góry dziękuję za odpowiedz.

  • Damian

    Hej, Mam pytanie dotyczące wątków.

    Mam aplikację w spring MVC, w kontrolerze użytkownik ma możliwość uruchomienia procesu, który długo trwa dlatego też chcę uruchomić go w innym wątku. Dodatkowo, jeżeli jakiś użytkownik uruchomił ten proces (i nadal on trwa) to inny użytkownik już nie uruchomi po raz drugi tego procesu, jego wątek zostanie uśmiercony dla tego długotrwającego procesu ( nie bedzie czekać na zakończenie działania innego wątku).

    Czy mógłbym prosić o naprowadzenie w jaki sposób mogę to zrealizować? :)

    • Opisany problem brzmi jak coś, co można rozwiązać za pomocą locków – bardziej szczegółowe informacje możesz znaleźć na http://winterbe.com/posts/2015/04/30/java8-concurrency-tutorial-synchronized-locks-examples/.

      Jeśli jednak odstępy czasowe pomiędzy dwoma zdarzeniami (użytkownikami uruchamiającymi wątek) są wystarczająco duże (powiedzmy liczone w minimum sekundach), to teoretycznie zadziała zwykłe pole statyczne które będzie sprawdzane za każdym razem. Miej jednak na uwadze, że takie rozwiązanie jest bardzo ryzykowne w aplikacji która miałaby działać w ‚realnym świecie’ i sprawdzi się jedynie w przypadku małej aplikacji lub projektu na uczelnie / dla znajomych.