Kihagyás

7. gyakorlat

Párhuzamosítás

Az eddig bemutatott projektek esetén a programok egy szálon futottak szekvenciálisan. Ez azt jelenti, hogy a végrehajtás "sorról sorra" történik (a fordítás optimalizációit és a JVM trükkjeit figyelmen kívül hagyva).

A Java nyelv egy többszálú programozási nyelv, ami azt jelenti, hogy olyan programok készíthetők a segítségével, melyek egyszerre több szálon futnak. Mivel a legtöbb mai számítógép (még a beágyazott rendszerek is) támogatja ezt a futtatási módszert, így kihasználható a nyelv adta lehetőség.

A párhuzamos programozás több kérdést vet fel, melyek egy része a közös erőforrás használatáról és a szálak közötti szinkronizációról szólnak. Ezek egy részével a kurzus előadásán és Operációs rendszerek kurzuson találkoztak a hallgatók. Jelen gyakorlat főleg a JavaFX-ben alkalmazható megoldásokkal foglalkozik.

Maximumkeresés párhuzamosítása

A következő példa szemlélteti a naiv maximumkeresést egy konzolos alkalmazásban.

Az alkalmazás létrehozása (Programozás-I ismétlés):

  1. IntelliJ IDEA: File - New Project
  2. Java (nem kell kiválasztani semmilyen frameworkot) - Next
  3. Create project from template - Command Line App
  4. A projekt helye és a csomag tetszőleges (pl. Asztal, hu.alkfejl.max)

Amennyiben a fenti lépések alapján nem sikerült létrehozni a projektet, az IntelliJ honlapján képekkel illusztrálva ugyanez a leírás megtalálható.

A következő kódrészlet készít egy ARRAY_SIZE méretű tömböt és feltölti véletlenszerűen generált adatokkal. A tömbön végig kell iterálni és meghatározni a maximális értékű elemét.

1
2
3
4
5
6
  int ARRAY_SIZE = 500000000;

  double[] arr = new double[ARRAY_SIZE];
  for (int i = 0; i < arr.length; i++) {
    arr[i] = Math.random();
  }

A cél az, hogy az algoritmus futási idejét mérjük és összehasonlítsuk a párhuzamos futtatás eredményével. Ezt a System.nanoTime() hívások segítségével tehetjük meg az alábbi formában.

1
2
3
4
5
6
7
  long singleThreadStart = System.nanoTime();
  double max = Double.MIN_VALUE;
  for (double value : arr) {
    if (value > max)
      max = value;
  }
  long singleThreadTime = (System.nanoTime() - singleThreadStart) / 1000000;

Az így előállt programot futtatva és kiíratva a max és a singleThreadTime értékeit az alábbi kimenetet kapjuk:

1
2
Maximum value: 0.9999999940819203
Single thread execution time: 398 ms

A futásidő a használt számítógép erőforrásaitól függően változhat (és minden futtatás során egy kicsit változik is), de ez az érték tekintendő alapvonalnak, amihez képest a párhuzamos megvalósítás "jóságát" vizsgáljuk.

Az az ötletünk támadhat, hogy a nagy elemszámú tömböt több kis részre osztjuk (teoretikusan, másolás nélkül) és a kis részeken egyszerre kellene végrehajtani a maximumkeresést. Az egyes lokális maximumokat megvárva azok közül kiválasztható a tényleges, globális maximum értéke.

Ahhoz, hogy a maximumkeresés több szálon legyen végrehajtva, egy új osztályt kell létrehozni, mely a Thread osztályból öröklődik és a run() metódust felüldefiniálja. Az osztály a következőképpen épül fel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class MaxSearchThread extends Thread {
  double localMaximum = Double.MIN_VALUE;
  int fromIndex;
  int toIndex;
  final double[] array;

  public MaxSearchThread(double[] array, int fromIndex, int toIndex) {
    this.fromIndex = fromIndex;
    this.toIndex = toIndex;
    this.array = array;
  }

  @Override
  public void run() {
    for (int i = fromIndex; i < toIndex; i++) {
      if (array[i] > localMaximum)
        localMaximum = array[i];
    }
  }
}

A konstruktorban át kell adni a vizsgálandó tömböt és azt, hogy a tömb melyik részén végezze el a keresést. Az, hogy hány részre van osztva a keresési tér a szálak számától függ.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int NUMBER_OF_THREADS = 4;
ArrayList<MaxSearchThread> threads = new ArrayList<>();

for (int i = 0; i < NUMBER_OF_THREADS; i++) {
  int from = i * (arr.length / NUMBER_OF_THREADS);
  int to = (i + 1) * (arr.length / NUMBER_OF_THREADS);

  MaxSearchThread thread = new MaxSearchThread(arr, from, to);
  threads.add(thread);
  thread.start();
}

A szálak számától függően feldaraboljuk a tömböt, 4 szál esetén 4 részre és mindegyik szál az egyiken dolgozik. A threads lista szükséges ahhoz, hogy a szálak által produkált eredményeket elérjük. A thread.start() hívás után a program futása úgymond "kettéválik", a for ciklus is folytatódik, illetve a thread objektum futása egy külön szálon megkezdődik.

A példában 4 db szál külön életet él, viszont a main függvényben szeretnénk a lokális maximumértékeiket megszerezni. Ahhoz hogy megvárjuk egy szál futásának végét, a .join() hívással blokkolható a fő szál amíg a kívánt szál run metódusa véget nem ér.

1
2
3
4
5
6
7
8
double multipleThreadsMax = Double.MIN_VALUE;
for (MaxSearchThread findMaxThread : threads) {
  try {
    findMaxThread.join();
    if (findMaxThread.localMaximum > multipleThreadsMax)
        multipleThreadsMax = findMaxThread.localMaximum;
  } catch (InterruptedException e) { e.printStackTrace(); }
}

A join() metódus InterruptedException kivételt dobhat, így try-catch blokkba kell tenni és lekezelni az esetlegesen dobott kivételt.

A szálak létrehozása elé és a globális maximumérték meghatározása mögé beszúrva System.nanoTime() hívásokat, lemérhető a párhuzamos megvalósítás futásideje.

1
2
3
4
5
Single thread max:   0.9999999982963121
Multiple thread max: 0.9999999982963121

Single thread time:   409 ms
Multiple thread time: 246 ms

Látható, hogy ugyanazt a maximumértéket találták meg (elvárt viselkedés), illetve a párhuzamos futás 163 ms-sel gyorsabban végzett. A szálak menedzselése és a kontextusváltások plusz terhet jelentenek a JVM számára, így túl sok szálat indítani nem érdemes, illetve kis adaton (kis tömbméret esetén) nem fog túl sok javulást hozni.

Ha a MaxSearchThread::call és a main metódusokban elhelyezzük a System.out.println(Thread.currentThread().getName()); sort, akkor a konzolon követhető, hogy milyen nevű threadek lettek létrehozva, illetve melyik fut.

1
2
3
4
5
main
Thread-0
Thread-1
Thread-2
Thread-3

Feladat

  1. Módosítsuk az ARRAY_SIZE értékét: próbáljuk ki milyen futásidők születnek, amennyiben sokkal kisebb, vagy sokkal nagyobb tömbön kell keresni. Esetleg szükséges lehet a heap méretét megnövelni, melyhez itt található leírás.

  2. Módosítsuk a NUMBER_OF_THREADS értékét: kevesebb vagy több szál futtatása esetén milyen eredmények születnek?

A megvalósított projekt megtalálható pub/Alkalmazásfejlesztés-I/07 mappában, 01_ParallelCommandLine néven.

JavaFX

Felhasználói felületek esetén is fontos szerepe van a párhuzamosításnak. Az eddigi gyakorlatok során minden esetben egy helyi SQLite adatbázishoz kapcsolódott az alkalmazás, így nem tapasztaltunk késleltetést. Amennyiben az adatbázis egy szerveren van, a szerver terheltsége és hálózati késleltetés függvényében akár másodperceket is várhatunk a válaszra. A várakozás során a felhasználói felület teljesen le van fagyva.

A modern kor felhasználója nem tud várni, bármi történik reszponzív kell maradjon az alkalmazás: görgetni, nyomkodni lehessen. Ez elérhető olyan formában, hogy a potenciálisan hosszan tartó folyamatokat külön szál végzi el, mely "vissza szól" a UI-nak, ha kész van.

JavaFX esetében van egy kitüntetett szál, amin az összes UI esemény feldolgozása zajlik. Ezt a szálat hívják JavaFX Application Thread-nek. A SceneGraph Node-jai nem Thread-safe-ek, azaz több szálon történő hozzáférésük nem engedélyezett. Ennek előnye, hogy gyorsabb, mert nem kell szinkron blokkokkal operálni, ugyanakkor nem tudjuk csak egy szálról használni őket. A UI elemekhez csak az Application Thread-ről férhetünk hozzá. Ennek eredményeképpen ajánlatos a hosszan futó feladatokat elkerülni a felületen, mivel az Application Thread addig blokkolva lesz és így egy nem reszponzív felületet kapunk.

Nézzünk egy egyszerű példát:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class App extends Application {

    private Label label = new Label("Initial state");
    private Button goBtn = new Button("Go");
    private Button exitBtn = new Button("Exit");

    @Override
    public void start(Stage stage) {
        goBtn.setOnAction(e -> runTask());
        exitBtn.setOnAction(e -> Platform.exit());

        HBox buttons = new HBox(10, goBtn, exitBtn);

        VBox root = new VBox(10, label, buttons);

        var scene = new Scene(root, 640, 480);
        stage.setScene(scene);
        stage.show();
    }

    private void runTask() {
        int maxIter = 5;
        for(int i = 0; i < maxIter; i++) {
            try {
                String status = "Processing " + (i+1)  + " of " + maxIter;
                System.out.println(status);
                label.setText(status);
                Thread.sleep(2000);
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        launch();
    }

}

Amikor a Go gombot megnyomja a felhasználó akkor a konzolon látszik, hogy hanyadik iterációban tart a program (Processing 5 of 5), ugyanakkor a UI-on egészen addig az Initial state szöveg látszódik a Label-en, ameddig mind az 5 iteráció le nem ment, azaz addig blokkolva van a UI. Miközben fut a Go, próbáljuk megnyomni az Exit gombot. Azt tapasztaljuk, hogy ez csak akkor fog lefutni, azaz akkor lép ki az alkalmazásból, amikor a runTask lefutott.

A fenti probléma megoldása, hogy ne futtassunk hosszú feladatokat az eseménykezelőkben, pontosabban azokat egy külön szálon indítsuk el a háttérben, ne pedig az Application Thread-et blokkoljuk ezzel.

Elsőként lássunk egy rossz példát, melynek során egy külön thread-ben indítjuk a runTask-ot. A start-on belül csak az eseménykezelőt írjuk át a következőképpen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Override
public void start(Stage stage) {
    goBtn.setOnAction(e -> startTaskOnThread());
    ...
}

private void startTaskOnThread() {
    Runnable task = () -> runTask();
    Thread longRunningThread = new Thread(task);

    longRunningThread.setDaemon(true); // ha megálítjuk az alkalmazásunkat, akkor vele hal ez a szál is
    longRunningThread.start();
}
A megoldás a következő hibát fogja eredményezni:

1
java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-3`

, mely a label.setText(status); utasításon keletkezik. A JavaFX Runtime ellenőrzi, hogy UI-t befolyásoló utasításokat csak az Application Thread-ről adhatunk ki.

A helyes megoldás csak kicsiben különbözik a fentitől. A sima label.setText(status) hívás helyett a következőt kell használni:

1
Platform.runLater(() -> label.setText(status));

A Platform.runLater a paraméterben megadott Runnable-t futtatja valamikor a jövőben az Application Thread-en, így nem okoz olyan problémát, mint amit az előző esetben tapasztaltunk.

A fenitek csak idealizált környezetben alkalmazhatók, illetve van egy szabály, amit már meg is sértünk, hiszen külön szálból visszaszólunk az Application Thread-nek, hogy frissítse a label szövegét, ami korántsem elegáns. Ezen felül ebben a környezetben a task nem tud visszaadni eredményt, nincs megbízható kivételkezelés, nem tudunk megbízhatóan leállítani, újraindítani és ütemezni task futtatásokat. A következőkben ezeket a problémákat oldjuk meg a JavaFX Concurrency API segítségével.

JavaFX Concurrency API

A JavaFX Concurrency API a hagyományos concurrency API-ra épít, azaz a java.util.concurrent csomag alatt található elemekre. Maga a JavaFX Concurrency API egyáltalán nem terjedelmes, viszont annál hasznosabb kiegészítéseket tartalmaz a GUI alkalmazások többszálúsításához.

Az egyik központi elem a Worker<V> interface, mely egy feladatot (task-ot) reprezentál, amit egy vagy több háttérszálon kell végrehajtani. Ami nagyon fontos, hogy a Worker státuszát az Application Thread követni tudja. A Worker három implementációját is megkapjuk, melyek a következő absztrakt osztályok:

  • Task<V>: "Egyszer használatos" feladat futtatásához.
  • Service<V>: Többször is futtatható feladat futtatásához
  • ScheduledService<V>: A Service kiterjesztése úgy, hogy meghatározott időintervallumonként futtatja a rendszer.

A fenti osztályok generikus paramétere a Worker visszatérési típusát adja meg. Amennyiben egy Worker nem ad vissza eredményt, úgy használjuk a Void megadást!

Worker állapotai

Egy Worker a következő állapotok egyikében lehet, melyet a Worker.State enum ír le:

  • READY
  • SCHEDULED
  • RUNNING
  • SUCCEEDED
  • CANCELLED
  • FAILED

Az állapotok közötti lehetséges átmeteket a következő ábra mutatja be.

State Transitions

Amikor egy Worker-t létrehozunk, akkor a READY állapotba kerül. A végrehajtás előtt ütemezett, azaz SCHEDULED állapotba kerül, melyből a futó (RUNNING) állapotba kerülhet. Ezután 3 féle forgatókönyv létezik:

  • Futás közben kezeletlen kivétel keletkezik -> FAILED
  • cancel() metódus hívás eredményeképpen CANCELLED állapotba kerül. Mivel bármikor dönthetünk úgy, hogy mégsem szeretnénk futtatni a Worker-t, így nem csak a RUNNING, hanem a READY és SCHEDULED állapotokból is átkerülhetünk CANCELLED állapotba
  • Emellett nyilván sikeresen is lefuthat egy Worker, melynek eredményeképpen a SUCCEEDED állapotba kerül

A Service-ek ezen 3 állapotból visszakerülnek a READY állapotba, hogy a futtatás megismételhető legyen (az ábrán ezt a szaggatott vonalak jelzik).

Az állapotok közötti váltások WorkerStateEvent eseményeket is generálnak, így könnyen kezelhetjük, azt hogy például mi történjen hiba esetén (pl.: néhányszor próbálkozunk még az újrafuttatással utána viszont jelezzük a problémát).

Worker property-jei

Minden Worker a következő property-kkel rendelkezik, melyeket a létrehozáskor lehetőségünk van megadni:

  • title: a task neve
  • message: visszajelzések küldésére használatos (pl.: Processed 1 out of 10)
  • running: megmondja, hogy a feladat éppen fut-e (SCHEDULED is már igazat ad vissza)
  • state: az előző fejezetben bemutatott állapotok közül adja vissza az éppen aktuálisat
  • workDone, totalWork, progress: a feladat készültségét lehet jelezni vele
  • value: A Worker eredményét adja vissza
  • exception: Amennyiben a Worker FAILED állapotba került, akkor a kivétel ide kerül, mely Throwable típusú, azaz tovább is dobhatjuk az Application Thread-en, ha úgy tartja kedvünk

Ha a futó Worker állapotáról szeretnénk információkat megjeleníteni, akkor nyugodtan kössünk vezérlőket ezekhez a property-khez, illetve használhatunk Invalidation- és ChangeListener-eket is, melyekben UI elemeket hivatkozunk.

Nézzük, hogyan lehet Task használata mellett elvégezni az előző feladatokat:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class App extends Application {

    private Label label = new Label("Initial state");
    private Label progressLabel = new Label("Progress: 0%");
    private Label valueLabel = new Label("Current Value: ");
    private Label stateLabel = new Label("Current State: ");
    private Button goBtn = new Button("Go");
    private Button cancelBtn = new Button("Cancel Task");
    private Button exitBtn = new Button("Exit");
    Task<Integer> task = new Task<>() {
        {
            this.setOnSucceeded(event -> {
                updateProgress(1,1); // set progress to done (100%)
            });
        }

        @Override
        protected Integer call() throws Exception {
            int i, maxIter = 5;
            for(i = 0; i < maxIter; i++) {
                try {
                    String status = "Processing " + (i+1)  + " of " + maxIter;
                    this.updateMessage(status);
                    this.updateValue(i);
                    this.updateProgress(i, maxIter);
                    Thread.sleep(2000);
                }
                catch (InterruptedException e) {
                    if(this.isCancelled()) {
                        break;
                    }
                }
            }
            return i;
        }
    };

    @Override
    public void start(Stage stage) {
        goBtn.setOnAction(event -> startTaskOnThread());
        cancelBtn.setOnAction(event -> task.cancel());
        exitBtn.setOnAction(event -> Platform.exit());

        //bindings
        label.textProperty().bind(task.messageProperty());
        progressLabel.textProperty().bind(Bindings.concat("Progress: ", task.progressProperty().multiply(100).asString().concat("%")));
        valueLabel.textProperty().bind(Bindings.concat("Current Value: ", task.valueProperty().asString()));
        stateLabel.textProperty().bind(Bindings.concat("Current State: ", task.stateProperty().asString()));

        goBtn.disableProperty().bind(task.stateProperty().isNotEqualTo(Worker.State.READY));
        cancelBtn.disableProperty().bind(task.stateProperty().isNotEqualTo(Worker.State.RUNNING));


        HBox buttons = new HBox(10, goBtn, cancelBtn, exitBtn);

        VBox root = new VBox(10, label, progressLabel, valueLabel, stateLabel, buttons);

        var scene = new Scene(root, 640, 480);
        stage.setScene(scene);
        stage.show();
    }

    private void startTaskOnThread() {
        Thread longRunningThread = new Thread(task);

        longRunningThread.setDaemon(true);
        longRunningThread.start();
    }

    public static void main(String[] args) {
        launch();
    }
}

Az első és legfontosabb, hogy a feladatot egy Task formájában adjuk meg, melyet egy field-ben el is tárolunk. Az init blokkban megadunk egy eseménykezelőt is, mely akkor fut le, ha a task SUCCEEDED állapotba kerül. Hasonló eseménykezelőket találhatunk a különböző állapotokhoz, illetve van egy általános eseménykezelő is, melynek meg lehet adni az esemény típusát:

1
2
3
task.addEventHandler(WorkerStateEvent.WORKER_STATE_SUCCEEDED, event -> {
  System.out.println("Success");
});

Az absztrakt call metódus kifejtésében annyi változik, hogy a property-k értékét frissítjük az updateXXX metódusok használatával, így a folyamatról a UI-on is megjeleníthetünk információkat, ahogy azt meg is tesszük majd jelen esetben. Amennyiben InterruptedException-t kapunk, akkor leállítjuk a for ciklus futását és visszatérünk az aktuális i értékkel (ilyenkor vizsgáljuk, hogy azért kaptunk-e ilyen kivételt, mert valaki a cancel()-t meghívta-e).

Ezután a felületre kivezetünk néhány property-t (binding-ok segítségével), hogy követni tudjuk a háttérben futó feladat állapotát.

Megjegyzés

A Service osztály belül egy Task-ot tartalmaz, illetve használata nagyban megegyezik a Task használatával. Itt nem fogjuk bemutatni részletesen sem a Service sem pedig a ScheduledService használatát.

Kontakt alkalmazás továbbfejlesztése

A kiinduló projekt megtalálható a pub/Alkalmazásfejlesztés-I/06/01_contacts_final mappában.

Figyelem

Ne felejtsük el átírni az application.properties fájlban az adatbázis elérését!

A táblázat jobb oldalán, minden sorban található egy "Delete" és egy "Edit" gomb. Tegyük fel, hogy ezek hatására egy lassú hálózati kommunikáción keresztüli akció fut le, ami blokkolja a UI felületet és nem lehet görgetni még be nem fejeződik. Ennek imitálásához egy 5 másodperces várakozást tegyünk be a DAO megvalósításába.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Override
public void delete(Contact contact) {

    try(Connection c = DriverManager.getConnection(connectionURL);
        PreparedStatement stmt = c.prepareStatement(DELETE_CONTACT);
    ) {
        stmt.setInt(1, contact.getId());
        TimeUnit.SECONDS.sleep(5);
        stmt.executeUpdate();

    } catch (SQLException | InterruptedException throwables) {
        throwables.printStackTrace();
    }

}

Ami a kontextusban változott: a stmt.executeUpdate(); elé került egy sleep. A várakozást jelentő függvényhívás is dobhat InterruptedException kivételt, amit le kell kezelni! A catch ág képes arra, hogy több kivételosztályt is elkapjon egyszerre, és az osztályokat | (vagy) jellel kell elválasztani. Ugyanez a módosítás kerüljön bele az összes Contact DAO metódusba, kivéve a findAll-t.

A módosult metódusok: save, delete.

A futtatás utáni elvárt eredmény, hogy az alkalmazás indulása után minden művelet sokáig tart és addig a táblázat nem görgethető, hozzáadás ablak kifagy.

Ahhoz hogy reszponzív maradjon a UI, amíg a back-end fut, a következő változtatások szükségesek.

MainWindowsController

A hu.alkfejl.controller csomagban található MainWindowController osztály deleteContact metódusát kell felokosítani ahhoz, hogy a PersonController.getInstance().delete(p); metódushívás ne legyen blokkoló. Ehhez az előbb ismertetett Thread-eket kell alkalmazni.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void deleteContact(Contact c) {
    Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to delete contact: " + c.getName(), ButtonType.YES, ButtonType.NO);
    confirm.showAndWait().ifPresent(buttonType -> {
        if(buttonType.equals(ButtonType.YES)){

            Task<Void> task = new Task<>() {
                @Override
                protected Void call() throws Exception {
                    phoneDAO.deleteAll(c.getId());
                    dao.delete(c);
                    return null;
                }
            };

            task.setOnSucceeded(event1 -> refreshTable());

            Thread deleteThread = new Thread(task);
            deleteThread.start();

        }
    });
}

Amikor a felhasználó megerősíti, hogy ténylegesen ki akarja törölni a kiválasztott sort, akkor egy Task (javafx.concurrent.Task) objektumot hozunk létre. Az IDEA legenerálja a szükséges kódrészletet, és a call metódust kell megvalósítani. Jelen esetben ez csak tovább hív a DAO-ba (eddig is ezt csinálta az itt talált kódrészlet).

Mint ahogy azt már láttuk, a Task objektum önmagában nem futtatható, egy Thread-et kell létrehozni belőle, ami tudja futtatni. Az eseménykezelő feladata, hogy frissítse a táblázatot a Task futásának végeztével (elvárható, hogy a törölt sor nem jelenik meg újra).

AddEditContactController

Új kontakt hozzáadásánál és a módosításánál is a onSave metódus fut le. Ami eltér az előző fejezettől az az,hogy most kellene tudnunk azt is, hogy milyen eredménnyel futott le a módosítás és ennek függvényében jelezni a felhasználó felé, hogy sikeres-e a művelet. Az alábbi kódrészlet a onSave függvényt taglalja.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@FXML
public void onSave(){
    Task<Boolean> task = new Task<>() {
        @Override
        protected Boolean call() throws Exception {
            contact = contactDAO.save(contact);
            if(contact == null){
                return false;
            }
            phoneDAO.deleteAll(contact.getId());

            contact.getPhones().forEach(phone -> {
                phone.setId(0);
                phoneDAO.save(phone, contact.getId());
            });
            return true;
        }
    };

    Thread updateThread = new Thread(task);
    updateThread.start();

    App.<MainWindowController>loadFXML("/fxml/main_window.fxml", App.getStage(), mainWindowController -> {
        task.setOnSucceeded(event -> {
            Boolean result = task.getValue();
            if(result) {
                Alert alert = new Alert(Alert.AlertType.INFORMATION, "Saving contact was successful", ButtonType.OK);
                alert.showAndWait();
                mainWindowController.refreshTable();

            } else {
                Alert alert = new Alert(Alert.AlertType.ERROR, "Saving contact failed", ButtonType.OK);
                alert.showAndWait();
            }
        });
    });
}

A call nem sokban különbözik az előzőtől. Utána viszont hasznát vesszük a megírt loadFXML metódusnak, mivel frissítenünk kell majd a táblázatunkat a fő ablakon. A refreshTable metódust publikusra kellett módosítanunk, de ez rendben van, hiszen miért ne frissíthetnénk a táblázatot egy-egy kérés alkalmával.

A módosítások után a UI reszponzív marad, de 5 másodperc után ad visszajelzést, a kért esemény lefutása után.

Videók

Referenciák


Utolsó frissítés: 2024-03-19 07:29:57