#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.