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

By 16 April 2015 Kurs Javy

Wąt­ki to kole­jne bard­zo potężne narzędzie jezy­ka pro­gramowa­nia, które równie łat­wo może stać się ułatwie­niem jak i przeszkodą. Na szczęś­cie Java upraszcza nam pracę z wątka­mi i pozwala uniknąć wielu problemów.

Pro­gramowanie wielowątkowe pozwala nam na real­iza­cję kilku rzeczy w jed­nym cza­sie. Tak naprawdę już ter­az z niego korzys­tałaś, choć nieświadomie — ser­w­er aplikacji (jak np. Tom­cat) korzys­ta z wątków, aby umożli­wić wielu klien­tom korzys­tanie z Two­jej aplikacji jed­nocześnie. W tej lekcji nauczysz się sama wyko­rzysty­wać wąt­ki do zadań, które wyma­ga­ją więcej cza­su — np. wysłanie maila. Użytkown­ik strony zami­ast czekać, aż sys­tem wyśle maila i dopiero wtedy zwró­ci odpowiedź, dosta­je odpowiedź od razu, a mail jest wysyłany w tle. Doty­czy to nie tylko wysył­ki maili, ale wszys­t­kich oper­acji, które wyma­ga­ją cza­su — gen­erowa­nia plików PDF, ren­derowa­nia grafi­ki (np. dia­gramów), wykony­wa­nia licznych zmi­an na bazie danych, szyfrowa­nia itp. Zas­tosowa­nia tego pode­jś­cia są naprawdę niezliczone.

Lekcja

Wstęp

Na początku wyjaśn­imy sobie kil­ka pojęć i czym w rzeczy­wis­toś­ci są wąt­ki. Ale wszys­tko po kolei.

Wątki

Z wątka­mi mamy do czynienia właś­ci­wie każdego dnia — sys­te­my, które nie korzys­ta­ją z wątków są spo­tykane właś­ci­wie tylko przy kon­troli sys­temów pro­duk­cyjnych, gdzie czas i kole­jność dzi­ała­nia są kry­ty­czne, oraz w prostych układach elek­tron­icznych, gdzie układy są zbyt proste do obsłu­gi wątków.

Wąt­ki to sposób pro­ce­so­ra na robi­e­nie wielu rzeczy rzeczy jed­nocześnie. W danym momen­cie cza­su pro­ce­sor może wykon­ać tylko tyle instrukcji, ile ma rdzeni (w uproszcze­niu, w rzeczy­wis­toś­ci jest to bardziej skom­p­likowane, ale takie uproszcze­nie poz­woli nam łatwiej wytłu­maczyć). To by oznacza­ło, że jed­nocześnie mógłbyś mieć uru­chomione tylko tyle pro­gramów ile masz rdzeni pro­ce­so­ra — prze­ważnie dwa lub cztery we współczes­nych kom­put­er­ach. Aby obe­jść to ogranicze­nie, pro­ce­sor pracu­je na wątkach — jeden pro­gram to jeden (lub więcej) wątków. Wątek zaw­iera infor­ma­c­je o tym, jaki pro­gram jest uru­chomiony, co aku­rat się dzieje oraz jaka część pamię­ci RAM do niego należy. Pro­ce­sor przełącza się pomiędzy wątka­mi wiele razy w ciągu każdej sekundy, przez co użytkown­ik kom­put­era ma wraże­nie, że wiele pro­gramów dzi­ała jed­nocześnie. W rzeczy­wis­toś­ci więk­szość z nich jest ‘zam­rożonych’ i czeka na swo­ją kole­jkę, aby wznow­ić działanie.

Wielowątkowość

Wielowątkowość to nic innego jak zdol­ność sys­te­mu do wyko­rzys­ta­nia (lub obsłu­gi, w zależnoś­ci od kon­tek­stu) wielu wątków. O aplikacji mówimy, że jest wielowątkowa, jeśli korzys­ta z wątków, aby lep­iej real­i­zować swo­je zada­nia (np. real­izu­je czasochłonne zada­nia ‘w tle’, nie opóź­ni­a­jąc inter­akcji z użytkown­ikiem). Wielowątkowość wiąże się też z prob­le­ma­mi — główne z nich to syn­chro­niza­c­ja wątków (nie mamy gwarancji, który wątek dostanie więcej ‘cza­su’ od pro­ce­so­ra) oraz dostęp do zasobów.

Synchronizacja

Prob­lem syn­chro­niza­cji może­my sobie wyobraz­ić np w przy­pad­ku algo­ryt­mu, który doda­je do jakiejś licz­by (wspól­nej dla wielu wątków) jedynkę, tylko jeśli jest ona zerem. Dwa wąt­ki, które w tym samym cza­sie (tzn jeden po drugim, ale zan­im wykona się kole­j­na instrukc­ja) wykon­a­ją warunek uzna­ją, że jest on spełniony, po czym oba wąt­ki zwięk­szą liczbę o jeden. Taka sytu­ac­ja nazy­wa się sytu­acją wyś­cigu i w uproszcze­niu chodzi o to, że pomiędzy sprawdze­niem warunk­ów, a wyko­naniem czyn­noś­ci, warun­ki mogą się zmienić.

Java zapew­nia wiele mech­a­nizmów, które pozwala­ją tej sytu­acji uniknąć, jed­nak nie będziemy ich omaw­iać w tej lekcji. Rozwiązy­wanie tego typu prob­lemów powin­no się odby­wać na etapie pro­jek­towa­nia i wyma­ga dużej wiedzy i doświad­czenia, aby zro­bić to praw­idłowo. Póki co skupimy się na pod­stawach uży­cia wątków, bez bardziej zaawan­sowanych zagadnień :)

Uruchamianie i tworzenie wątków w Javie

Java ofer­u­je trzy sposo­by na uruchami­an­ie wątków, z których będzie nas intere­sował tylko ten ostatni:

  • dziedz­icze­nie po klasie Thread — to naj­gorszy ze sposobów, samodziel­nie musimy zarządzać wątkiem i nie mamy praw­ie żad­nych dodatkowych narzędzi; korzys­tanie z tego sposobu może także negaty­wnie wpłynąć na dzi­ałanie aplikacji webowej, narażamy się też na prob­lem związany z niepraw­idłowym zakończe­niem pra­cy wątku i niez­wol­niony­mi zasobami
  • imple­men­tac­ja inter­fe­j­su Runnable i uży­cie klasy Thread do jego uru­chomienia — to trochę lep­sza meto­da, ale zasad­nic­zo cier­pi na takie same prob­le­my jak punkt pier­wszy; z drugiej strony dość łat­wo może­my ją prze­r­o­bić na sposób trzeci
  • imple­men­tac­ja inter­fe­j­su Runnable i uży­cie imple­men­tacji Execu­torSer­vice — to najlep­sza meto­da na uruchami­an­ie włas­nych wątków — pozwala nam tworzyć pule wątków, dele­gować im zada­nia, tworzyć zada­nia cyk­liczne, kończyć pracę wątków w kon­trolowany sposób i wiele więcej; tą metodą zajmiemy się w dzisiejszej lekcji

Interfejsy Runnable oraz ExecutorService

Te dwa inter­fe­jsy są kluc­zowe do korzys­ta­nia z wątków w Javie. Omówimy tylko ich pod­sta­wowe zas­tosowanie, ale zachę­cam do ekspery­men­tów i testowa­nia innych możliwości.

Na początku inter­fe­js Runnable, który deklaru­je jed­ną metodę — run, która nic nie zwraca i nic nie przyj­mu­je. To cen­tral­ny punkt naszego wątku — wątek uru­cho­mi tą metodę i będzie dzi­ałał, dopó­ki nie skończy się ona wykony­wać. Fakt, że nic nie zwraca i nie przyj­mu­je jest celowy — jeśli chodzi o przyj­mowanie danych, moż­na do tego wyko­rzys­tać kon­struk­tor lub np. Springa (tak, może­my zadeklarować beana za pomocą @Component, który imple­men­tu­je również Runnable i uży­wać w nim np. @Autowired). Jeśli chce­my więc zaim­ple­men­tować prosty wątek, który np. wyśle maile z bazy danych czeka­ją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.
        }
    }
}

Execu­torSer­vice to z kolei inter­fe­js, którego nie musimy imple­men­tować samodziel­nie, środowisko dostar­cza nam gotowe imple­men­tac­je. Dzię­ki temu może­my w prosty sposó utworzyć wątek lub pulę wątków, która będzie dzi­ałała wg naszych oczeki­wań. Najważniejsze metody, które udostęp­nia ten inter­fe­js, to:

  • submit(Runnable task) — pozwala przesłać ‘zadanie’ (imple­men­tację inter­fe­j­su Runnable) do wyko­na­nia (uwa­ga: nie mamy gwarancji, czy zostanie ono od razu uru­chomione! To zalezy od aktu­al­nego sta­tusu Execu­torSer­vice, kole­j­ki zadań, dostęp­nych wątków etc)
  • shut­down() — pozwala praw­idłowo zakończyć pracę wątków, uprzed­nio kończąc wszys­tkie zada­nia i zwal­ni­a­jąc wszys­tkie zaso­by. Wywołanie tej metody jest wyma­gane przed zakończe­niem dzi­ała­nia aplikacji!

Zobaczmy przykład, jak stworzyć ser­wis, który korzys­ta z Execu­torSer­vice, który w praw­idłowy sposób kończy pracę. Wyko­rzys­tamy tutaj inter­fe­js Life­cy­cle - dzię­ki niemu, Spring powiado­mi naszą klasę o wyłącza­niu aplikacji, będziemy więc mogli odpowied­nio zamknąć ExecutorService.

Użyliśmy też adno­tacji @PostConstruct — wprawdzie mogliśmy to samo zro­bić w metodzie start() z inter­fe­j­su Life­cy­cle, ale chci­ałem przy okazji pokazać, do czego moż­na użyć tej adno­tacji. Meto­da (pub­licz­na, bez argu­men­tów), którą oznaczymy tą adno­tacją, zostanie wywołana w momen­cie, w którym wszys­tkie beany Springa zostaną już utwor­zone i pod­pięte (tzn. wszys­tkie pola z adno­tacją @Autowired powin­ny mieć już właś­ci­we obiek­ty), czyli real­nie w momen­cie, w którym nasza aplikac­ja 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 Execu­tors. Przyjrzyjmy się jeszcze jakie inne opc­je tworzenia puli mamy dostęp­ne i do czego może­my ich używać:

  • newScheduledThreadPool(int core­Pool­Size) — tworzy pulę wątków ( o rozmi­arze, który przekazu­je­my jako para­metr), która pozwala nam wykony­wać zada­nia np. cyk­licznie (zobacz deklarację inter­fe­j­su Sched­uledEx­e­cut­ingSer­vice)
  • newFixedThreadPool(int nThreads) — tworzy pulę o określonej iloś­ci wątkó, które są cały czas akty­wne (aż do zamknię­cia Executora)
  • new­CachedThread­Pool() — tworzy pulę, w której wąt­ki są twor­zone tylko, jeśli są potrzeb­ne i zamykane po upły­wie określonego cza­su bezczyn­noś­ci, pole­cam do wykony­wa­nia nieczęstych zadań (oszczędza zasoby)
  • newS­in­gleThread­Ex­ecu­tor() — poje­dynczy wątek do naprost­szych zastosowań

Częs­to poje­dynczy wątek to wszys­tko, czego potrze­ba. W zależnoś­ci od konkret­nego przy­pad­ku, różne pule mogą się sprawdzać w różnych sytuacjach.

Podsumowanie

W tej lekcji dowiedzi­ałaś się, czym są wąt­ki w Javie, do czego moż­na je wyko­rzysty­wać i jak uży­wać ich praw­idłowo. Nie było aka­demick­iego (bezsen­sownego) przykładu ze śli­maka­mi (brr, oso­by które miały nieprzy­jem­nosć uczęszczać na zaję­cia z pro­gramowa­nia na uczel­ni na pewno wiedzą, o czym mówię) za to trochę prak­ty­ki. Pamię­taj tylko, że więcej nie zawsze znaczy lep­iej — zbyt duża ilość wątków może ograniczyć wyda­jność aplikacji zami­ast ją poprawić.

Zadanie

Zmody­fikuj pro­gram, który już napisałeś tak, aby uży­wał Execu­torSer­vice (uruchami­a­jącego to samo zadanie co min­utę) i wyp­isy­wał w logu ilość kotów, które są w bazie danych.

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przy­dat­ną, mamy małą prośbę: pol­ub nasz fan­page. Dzię­ki temu będziesz zawsze na bieżą­co z nowy­mi treś­ci­a­mi na blogu ( i oczy­wiś­cie, z nowy­mi częś­ci­a­mi kur­su Javy). Dzięki!