concurrency java semaphore
Αυτό το σεμινάριο θα συζητήσει στοιχεία του πακέτου java.util.concurrent όπως Java Semaphore, Executor Framework, ExecutorService για την εφαρμογή Concurrency στην Java:
Από τα προηγούμενα μαθήματα Java, γνωρίζουμε ότι η πλατφόρμα Java υποστηρίζει ταυτόχρονο προγραμματισμό από την αρχή. Η βασική μονάδα ταυτότητας είναι ένα νήμα και έχουμε συζητήσει λεπτομερώς τα νήματα και τα multithreading στην Java.
Από την Java 5 και μετά, προστέθηκε ένα πακέτο που ονομάζεται «java.util.concurrent» στην πλατφόρμα Java. Αυτό το πακέτο περιέχει μια σειρά από τάξεις και βιβλιοθήκες που διευκολύνουν τον προγραμματιστή να αναπτύξει ταυτόχρονες (πολλαπλών νημάτων) εφαρμογές. Χρησιμοποιώντας αυτό το πακέτο, δεν χρειάζεται να γράφουμε πολύπλοκες τάξεις, καθώς έχουμε έτοιμες υλοποιήσεις των περισσότερων ταυτόχρονων εννοιών.
=> Ελέγξτε ΟΛΑ τα Εκπαιδευτικά Java εδώ.
Σε αυτό το σεμινάριο, θα συζητήσουμε τα διάφορα συστατικά του πακέτου java.util.concurrent σχετικά με την ταυτόχρονη και multithreading στην Java.
Τι θα μάθετε:
πακέτο java.util.concurrent
Παρακάτω αναφέρονται τα διάφορα συστατικά του πακέτου java.util.concurrent σχετικά με την ταυτόχρονη και multithreading στην Java. Ας διερευνήσουμε κάθε στοιχείο λεπτομερώς με τη βοήθεια απλών παραδειγμάτων προγραμματισμού. Μερικά από τα συστατικά που θα
συζήτηση είναι:
- Πλαίσιο εκτελεστή
- Εξυπηρέτηση
- ThreadPool
- Καλείται
- Κλειδαριές - ReentrantLock
- Σηματοφόρος
- ForkJoinPool
Executor Framework στην Java
Το Executor Framework στην Java κυκλοφόρησε με την έκδοση JDK 5. Το Executor Framework (java.util.concurrent.Executor) είναι ένα πλαίσιο που αποτελείται από στοιχεία που μας βοηθούν να χειριστούμε αποτελεσματικά πολλά νήματα.
Χρησιμοποιώντας το Executor Framework, μπορούμε να εκτελέσουμε αντικείμενα που μπορούν να εκτελεστούν επαναχρησιμοποιώντας τα ήδη υπάρχοντα νήματα. Δεν χρειάζεται να δημιουργούμε νέα νήματα κάθε φορά που πρέπει να εκτελούμε αντικείμενα.
Το API Executor διαχωρίζει ή αποσυνδέει την εκτέλεση μιας εργασίας από την πραγματική εργασία χρησιμοποιώντας ένα Εκτελεστής διαθήκης . Ένας εκτελεστής επικεντρώνεται στη διεπαφή Executor και έχει δευτερεύουσες διεπαφές, δηλαδή Εξυπηρέτηση και την τάξη ThreadPoolExecutor.
Χρησιμοποιώντας έτσι το Executor, πρέπει απλώς να δημιουργήσουμε αντικείμενα με δυνατότητα εκτέλεσης εκτέλεσης και στη συνέχεια να τα στείλουμε στον εκτελεστή που τα εκτελεί.
δείγματα σεναρίων δοκιμής για δοκιμές λογισμικού
Μερικές από τις βέλτιστες πρακτικές που πρέπει να ακολουθούνται κατά τη χρήση του πλαισίου Executor είναι,
- Θα πρέπει να διασταυρώσουμε και να σχεδιάσουμε έναν κώδικα για να ελέγξουμε τις κορυφαίες λίστες, ώστε να μπορούμε να εντοπίσουμε το αδιέξοδο καθώς και το ζωντανό κλειδί στον κώδικα.
- Ο κώδικας Java πρέπει πάντα να εκτελείται με εργαλεία στατικής ανάλυσης. Παραδείγματα των εργαλείων στατικής ανάλυσης είναι το FindBugs και το PMD.
- Δεν πρέπει να πιάσουμε μόνο εξαιρέσεις, αλλά και λάθη σε προγράμματα πολλαπλών νημάτων.
Τώρα ας συζητήσουμε τα στοιχεία του Executor Framework στην Java.
Εκτελεστής διαθήκης
Ο εκτελεστής μπορεί να οριστεί ως μια διεπαφή που χρησιμοποιείται για την αναπαράσταση ενός αντικειμένου που εκτελεί τις εργασίες που του παρέχονται. Το αν η εργασία θα εκτελεστεί στο τρέχον ή στο νέο νήμα εξαρτάται από το σημείο από το οποίο ξεκίνησε η επίκληση που εξαρτάται περαιτέρω από την εφαρμογή.
Έτσι, χρησιμοποιώντας το Executor, μπορούμε να αποσυνδέσουμε τις εργασίες από την πραγματική εργασία και στη συνέχεια να τις εκτελέσουμε ασύγχρονα.
Ωστόσο, η εκτέλεση της εργασίας με χρήση του Executor δεν χρειάζεται να είναι ασύγχρονη. Οι εκτελεστές μπορούν επίσης να επικαλεστούν την εργασία αμέσως χρησιμοποιώντας το νήμα επίκλησης.
Δίνεται παρακάτω ένα παράδειγμα κομμάτι κώδικα για τη δημιουργία παρουσίας Executor:
public class Invoker implements Executor { @Override public void execute (Runnable r_interface) { r_interface.run(); } }
Μόλις δημιουργηθεί ο εισβολέας, όπως φαίνεται παραπάνω, μπορούμε να τον χρησιμοποιήσουμε για να εκτελέσουμε την εργασία ως εξής.
public void execute () { Executor executor = new Invoker (); executor.execute ( () -> { //perform task }); }
Σημειώστε ότι εάν η εργασία δεν γίνει αποδεκτή από τον Εκτελεστή, τότε ρίχνει το RejectedExecutionException.
Εξυπηρέτηση
Ένα ExecutorService (java.util.concurrent.ExecutorService) προγραμματίζει τις υποβληθείσες εργασίες σύμφωνα με τη διαθεσιμότητα των νημάτων και διατηρεί επίσης μια ουρά μνήμης. Το ExecutorService ενεργεί ως μια ολοκληρωμένη λύση για την ασύγχρονη επεξεργασία εργασιών.
Για να χρησιμοποιήσουμε το ExecutorService σε κώδικα, δημιουργούμε μια κλάση με δυνατότητα εκτέλεσης. Το ExecutorService διατηρεί ένα νήμα και αναθέτει επίσης τις εργασίες στα νήματα. Οι εργασίες μπορούν επίσης να περιμένουν στην ουρά σε περίπτωση που το νήμα δεν είναι διαθέσιμο.
Δίνεται παρακάτω είναι ένα απλό παράδειγμα του ExecutorService.
import java.util.concurrent.*; public class Main { public static void main(String() args) { //create ExecutorService instance with 10 threads ExecutorService executor_Service = Executors.newFixedThreadPool(10); //assign the service to Runnable instance executor_Service.execute(new Runnable() { @Override public void run() { //print the message System.out.println('Simple Example of ExecutorService!!!'); } }); //shutdown executorService executor_Service.shutdown(); } }
Παραγωγή
Στο παραπάνω πρόγραμμα, δημιουργούμε μια απλή παρουσία ExecutorService με μια ομάδα νήματος που αποτελείται από 10 νήματα. Στη συνέχεια αντιστοιχίζεται στην εκτέλεση με δυνατότητα εκτέλεσης και εκτελείται για την εκτύπωση του παραπάνω μηνύματος. Μετά την εκτύπωση του μηνύματος, το ExecutorService τερματίζεται.
Πισίνα νήματος
Μια ομάδα νήματος στην Java είναι μια ομάδα νημάτων εργαζομένων που μπορούν να επαναχρησιμοποιηθούν πολλές φορές και να εκχωρηθούν εργασίες.
Μια ομάδα νημάτων περιέχει μια ομάδα νημάτων σταθερού μεγέθους. Κάθε νήμα αφαιρείται από το νήμα και εκχωρείται μια εργασία από τον πάροχο υπηρεσιών. Μόλις ολοκληρωθεί η εργασία που έχει ανατεθεί, το νήμα επιστρέφεται ξανά στην ομάδα νήματος.
Το νήμα είναι πλεονεκτικό καθώς δεν χρειάζεται να δημιουργούμε ένα νέο νήμα κάθε φορά που η εργασία είναι διαθέσιμη, επομένως η απόδοση βελτιώνεται. Χρησιμοποιείται σε εφαρμογές σε πραγματικό χρόνο που χρησιμοποιούν Servlet και JSP όπου χρησιμοποιούνται ομαδοποιήσεις νήματος για την επεξεργασία αιτημάτων.
Σε εφαρμογές πολλαπλών νημάτων, το Thread Pool εξοικονομεί πόρους και βοηθά στον περιορισμό του παραλληλισμού εντός προκαθορισμένων ορίων.
Το παρακάτω πρόγραμμα Java δείχνει το Thread pool στην Java.
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class WorkerThreadClass implements Runnable { private String message; //thread class constructor public WorkerThreadClass(String s){ this.message=s; } //run method for thread public void run() { System.out.println(' Start: '+message); processmessage(); //sleep between start and end System.out.println(' End: '+ message); } //processmessage method => sleeps the thread for 2 sec private void processmessage() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } public class Main { public static void main(String() args) { //create a ExecutorService instance ExecutorService executor = Executors.newFixedThreadPool(5);//creating a pool of 5 threads //create thread instances and execute them for (int i = 0; i <5; i++) { Runnable workerThrd = new WorkerThreadClass('Thread_' + i); executor.execute(workerThrd);//calling execute method of ExecutorService } //shutdown ExecutorService executor.shutdown(); while (!executor.isTerminated()) { } System.out.println('Finished all threads'); } }
Παραγωγή
Στα παραπάνω προγράμματα, υπάρχει μια ομάδα νήματος από 5 νήματα που δημιουργούνται χρησιμοποιώντας τη μέθοδο 'newFixedThreadPool'. Στη συνέχεια, τα νήματα δημιουργούνται και προστίθενται στην ομάδα και αντιστοιχίζονται στο ExecutorService για εκτέλεση.
Καλείται σε Java
Γνωρίζουμε ήδη ότι μπορούμε να δημιουργήσουμε νήματα χρησιμοποιώντας δύο προσεγγίσεις. Μια προσέγγιση είναι με την επέκταση της κλάσης νήματος, ενώ η δεύτερη προσέγγιση με την εφαρμογή διεπαφής Runnable.
Ωστόσο, τα νήματα που δημιουργήθηκαν χρησιμοποιώντας τη διεπαφή Runnable δεν διαθέτουν ένα χαρακτηριστικό, δηλαδή δεν επιστρέφει αποτέλεσμα όταν το νήμα τερματιστεί ή η εκτέλεση () ολοκληρώσει την εκτέλεση. Εδώ έρχεται η εικόνα του Callable interface.
Χρησιμοποιώντας μια διεπαφή με δυνατότητα κλήσης ορίζουμε μια εργασία έτσι ώστε να επιστρέφει ένα αποτέλεσμα. Μπορεί επίσης να ρίξει μια εξαίρεση. Η διεπαφή Callable είναι μέρος του πακέτου java.util.concurrent.
Η διασύνδεση με δυνατότητα κλήσης παρέχει μια μέθοδο κλήσης () που βρίσκεται στις ίδιες γραμμές με τη μέθοδο εκτέλεσης () που παρέχεται από τη διεπαφή Runnable με τη μόνη διαφορά ότι η μέθοδος κλήσης () επιστρέφει μια τιμή και ρίχνει την επιλεγμένη εξαίρεση
Η μέθοδος κλήσης () του Callable interface έχει το ακόλουθο πρωτότυπο.
public Object call () throws Exception;
Δεδομένου ότι η μέθοδος κλήσης () επιστρέφει ένα αντικείμενο, το κύριο νήμα πρέπει να το γνωρίζει.
Ως εκ τούτου, η τιμή επιστροφής θα πρέπει να αποθηκεύεται σε ένα άλλο αντικείμενο που είναι γνωστό στο κύριο νήμα. Αυτός ο σκοπός εξυπηρετείται χρησιμοποιώντας ένα αντικείμενο 'Future'. Ένα μελλοντικό αντικείμενο είναι ένα αντικείμενο που κρατά το αποτέλεσμα που επιστρέφεται από ένα νήμα. Ή με άλλα λόγια, θα κρατήσει το αποτέλεσμα όταν επιστρέφεται το Callable.
Το Callable ενσωματώνει μια εργασία που πρέπει να εκτελείται σε άλλο νήμα. Ένα αντικείμενο Future αποθηκεύει το αποτέλεσμα που επιστρέφεται από διαφορετικό νήμα.
Δεν είναι δυνατή η χρήση διεπαφής με δυνατότητα κλήσης για τη δημιουργία νήματος. Χρειαζόμαστε το Runnable για να δημιουργήσουμε ένα νήμα. Στη συνέχεια, για να αποθηκεύσετε το αποτέλεσμα απαιτείται ένα αντικείμενο Future. Η Java παρέχει έναν συγκεκριμένο τύπο που ονομάζεται 'FutureTask' που συνδυάζει τη λειτουργικότητα εφαρμόζοντας τόσο το Runnable όσο και το Future.
Δημιουργούμε μια FutureTask παρέχοντας στον κατασκευαστή Callable. Αυτό το αντικείμενο FutureTask δίνεται στη συνέχεια στον κατασκευαστή της κλάσης Thread για να δημιουργήσει ένα αντικείμενο Thread.
Δίνεται παρακάτω ένα πρόγραμμα Java που δείχνει το Callable interface και το Future αντικείμενο. Χρησιμοποιούμε επίσης το αντικείμενο FutureTask σε αυτό το πρόγραμμα.
Όπως ήδη αναφέρθηκε, στο πρόγραμμα δημιουργούμε μια κλάση που εφαρμόζει μια διεπαφή με δυνατότητα κλήσης με μια μέθοδο παράκαμψης κλήσεων (). Στην κύρια μέθοδο, δημιουργούμε 10 αντικείμενα FutureTask. Κάθε κατασκευαστής αντικειμένων έχει ένα αντικείμενο Callable class ως επιχείρημά του. Στη συνέχεια, το αντικείμενο FutureTask σχετίζεται με μια παρουσία νήματος.
Ως εκ τούτου εμμέσως δημιουργούμε ένα νήμα χρησιμοποιώντας ένα αντικείμενο διασύνδεσης με δυνατότητα κλήσης.
import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; //create a class implementing Callable interface class CallableDemo implements Callable { //define call () method public Object call() throws Exception { Random generator = new Random(); Integer randomNumber = generator.nextInt(10); Thread.sleep(randomNumber * 1000); return randomNumber; } } public class Main { public static void main(String() args) throws Exception { // Array of FutureTask objects FutureTask() randomNumberTasks = new FutureTask(10); for (int i = 0; i <10; i++) { Callable callable = new CallableDemo(); // Create the FutureTask with Callable class randomNumberTasks(i) = new FutureTask(callable); // create thread with FutureTask Thread t = new Thread(randomNumberTasks(i)); //start the thread t.start(); } System.out.println('The contents of FutureTask objects:'); for (int i = 0; i < 10; i++) { // get() contents of FutureTask System.out.print(randomNumberTasks(i).get() + ' '); } } }
Παραγωγή
Όπως φαίνεται στο παραπάνω πρόγραμμα, η μέθοδος κλήσης () του Callable που παρακάμπτεται στην κλάση υλοποίησης Callable δημιουργεί τυχαίους αριθμούς. Μόλις ξεκινήσει το νήμα, εμφανίζει αυτούς τους τυχαίους αριθμούς.
Επίσης, χρησιμοποιούμε αντικείμενα FutureTask στην κύρια λειτουργία. Καθώς εφαρμόζει τη διεπαφή Future, δεν χρειάζεται να αποθηκεύουμε τα αποτελέσματα στα αντικείμενα του νήματος. Παρομοίως, μπορούμε να ακυρώσουμε την εργασία, να ελέγξουμε εάν εκτελείται ή να ολοκληρωθεί και επίσης να λάβουμε το αποτέλεσμα χρησιμοποιώντας το αντικείμενο FutureTask.
ReentrantLock Στην Ιάβα
Συζητήσαμε λεπτομερώς τον συγχρονισμό νήματος χρησιμοποιώντας τη συγχρονισμένη λέξη-κλειδί στο τελευταίο μας σεμινάριο. Η χρήση της συγχρονισμένης λέξης για συγχρονισμό νημάτων είναι η βασική μέθοδος και είναι κάπως άκαμπτη.
Χρησιμοποιώντας τη συγχρονισμένη λέξη-κλειδί, ένα νήμα μπορεί να κλειδώσει μόνο μία φορά. Επίσης, αφού ένα νήμα βγει από το συγχρονισμένο μπλοκ, το επόμενο νήμα παίρνει το κλείδωμα. Δεν υπάρχει ουρά αναμονής. Αυτά τα ζητήματα μπορεί να προκαλέσουν λιμοκτονία κάποιου άλλου νήματος, καθώς ενδέχεται να μην έχουν πρόσβαση στους πόρους για μεγάλο χρονικό διάστημα.
Για να αντιμετωπίσουμε αυτά τα ζητήματα, χρειαζόμαστε μια ευέλικτη μέθοδο συγχρονισμού των νημάτων. Το 'Reentrant Locks' είναι αυτή η μέθοδος στην Java που παρέχει συγχρονισμό με πολύ μεγαλύτερη ευελιξία.
Η τάξη 'ReentrantLock' εφαρμόζει κλειδώματα Reentrant και αποτελεί μέρος του πακέτου 'import java.util.concurrent.locks'. Το ReentrantLock class παρέχει τη μέθοδο συγχρονισμού για πρόσβαση σε κοινόχρηστους πόρους. Τα μαθήματα έχουν επίσης τις μεθόδους κλειδώματος και ξεκλειδώματος για το κλείδωμα / ξεκλείδωμα πόρων όταν έχουν πρόσβαση μέσω νημάτων.
Ένα ιδιαίτερο χαρακτηριστικό του ReentrantLock είναι ότι το νήμα μπορεί να κλειδώσει τον κοινόχρηστο πόρο περισσότερες από μία φορές χρησιμοποιώντας το ReentrantLock. Παρέχει απαρίθμηση που ορίζεται σε ένα όταν το νήμα κλειδώνει τον πόρο.
Το νήμα μπορεί να εισέλθει ξανά και να αποκτήσει πρόσβαση στον πόρο πριν από το ξεκλείδωμα. Κάθε φορά που το νήμα αποκτά πρόσβαση στον πόρο χρησιμοποιώντας το κλείδωμα Reentrant, ο αριθμός αναμονής αυξάνεται από έναν. Για κάθε ξεκλείδωμα, ο αριθμός αναμονής μειώνεται από ένα.
Όταν το πλήθος αναμονής φτάσει στο 0, ο κοινόχρηστος πόρος ξεκλειδώνεται.
Η τάξη ReentrantLock παρέχει επίσης μια παράμετρο δικαιοσύνης που είναι μια δυαδική τιμή που μπορεί να περάσει με τον κατασκευαστή της κλειδαριάς. Όταν η παράμετρος δικαιοσύνης έχει οριστεί ως αληθής, τότε κάθε φορά που ένα νήμα απελευθερώνει την κλειδαριά, η κλειδαριά μεταφέρεται στο νήμα με τη μεγαλύτερη αναμονή Αυτό αποτρέπει την πείνα.
Οι κλειδαριές Reentrant μπορούν να χρησιμοποιηθούν ως εξής:
return_type method_name() { reentrantlock.lock(); try { //Do some work } catch(Exception e) { e.printStackTrace(); } finally { reentrantlock.unlock(); } }
Σημειώστε ότι η δήλωση ξεκλειδώματος για το ReentrantLock βρίσκεται πάντα στο τελευταίο μπλοκ. Αυτό εγγυάται την απελευθέρωση της κλειδαριάς ακόμη και αν υπάρχει εξαίρεση.
Ας εφαρμόσουμε ένα πρόγραμμα Java για να κατανοήσουμε το ReentrantLock.
import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.*; import java.util.concurrent.locks.ReentrantLock; //thread class that implements Runnable interface class ThreadClass implements Runnable { String task_name; //define ReentrantLock object ReentrantLock thrd_lck; //ThreadClass constructor initialized lock and task name public ThreadClass(ReentrantLock r_lock, String t_name) { thrd_lck = r_lock; task_name = t_name; } //thread run () method public void run() { boolean bool_val = false; while (!bool_val) { //check for Outer Lock boolean tryLock_val = thrd_lck.tryLock(); // if lock is free, do the following if(tryLock_val) { try { for(int i=0;i<=6;i++) { if(i>=2) { thrd_lck.lock(); Thread thread_one = new Thread(); System.out.println('Thread Created.....'); if(i==3) { thread_one.setName('Maint Thread2'); System.out.println('Thread Created.....'); } } if(i==4) thrd_lck.unlock(); break; } System.out.println('ReentrantLock=>Is locked after sleep(1500) : ' + thrd_lck.isLocked()); System.out.println('Work done for task : ' + task_name ); bool_val = true; } catch(Exception e) { e.printStackTrace(); } } } } } public class Main { public static void main(String() args) { //define ReentrantLock lock object and service pool ReentrantLock reentrant_lock = new ReentrantLock(); ExecutorService pool = Executors.newFixedThreadPool(2); //create thread instance and pass lock and task name Runnable worker_thread = new ThreadClass(reentrant_lock, 'ThreadJob'); //execute the thread in exec pool pool.execute(worker_thread); //shut down the pool pool.shutdown(); } }
Παραγωγή
Στο παραπάνω πρόγραμμα, δημιουργήσαμε ένα νήμα και χρησιμοποιήσαμε το ReentrantLock για αυτό. Χρησιμοποιώντας το ReentrantLock μπορείτε να αποκτήσετε πρόσβαση στον κοινόχρηστο πόρο.
Σηματοφόρος στην Ιάβα
Η επόμενη μέθοδος συγχρονισμού νημάτων είναι με τη χρήση του Semaphore. Χρησιμοποιώντας αυτήν την κατασκευή που ονομάζεται semaphore, η πρόσβαση σε έναν κοινόχρηστο πόρο ελέγχεται μέσω ενός μετρητή. Τα σήματα αποστέλλονται μεταξύ των νημάτων, έτσι ώστε να μπορούμε να προστατεύουμε το κρίσιμο τμήμα και επίσης να αποφεύγουμε τα χαμένα σήματα.
Ένα semaphore μπορεί να οριστεί ως μια μεταβλητή που χρησιμοποιείται για τη διαχείριση ταυτόχρονων διεργασιών συγχρονίζοντας αυτές τις διαδικασίες. Τα Semaphores χρησιμοποιούνται επίσης για το συγχρονισμό της πρόσβασης στον κοινόχρηστο πόρο και, επομένως, την αποφυγή μιας κατάστασης αγώνα. Η άδεια που δίνεται σε ένα νήμα για πρόσβαση στον κοινόχρηστο πόρο από το semaphore ονομάζεται επίσης άδεια.
Ανάλογα με τις λειτουργίες που εκτελούν, οι σηματοφόροι μπορούν να χωριστούν σε δύο τύπους:
# 1) Δυαδικό Semaphore: Ένα δυαδικό σηματοφόρο χρησιμοποιείται για τον συγχρονισμό ταυτόχρονων διαδικασιών και την εφαρμογή αμοιβαίου αποκλεισμού. Ένα δυαδικό σηματοφόρο υποθέτει μόνο δύο τιμές, δηλαδή 0 και 1.
# 2) Μετρώντας Semaphore: Το semaphore μέτρησης έχει μια τιμή που δείχνει τον αριθμό των διεργασιών που μπορούν να εισέλθουν στην κρίσιμη ενότητα. Σε οποιοδήποτε σημείο, η τιμή δείχνει τον μέγιστο αριθμό διαδικασιών που εισέρχονται στην κρίσιμη ενότητα.
Πώς λειτουργεί λοιπόν το Semaphore;
Η εργασία ενός Semaphore μπορεί να συνοψιστεί στα ακόλουθα βήματα:
- Εάν ο αριθμός των σηματοφόρων> 0, αυτό σημαίνει ότι το νήμα έχει άδεια πρόσβασης σε κρίσιμη ενότητα και, στη συνέχεια, ο αριθμός μειώνεται.
- Διαφορετικά, το νήμα είναι μπλοκαρισμένο έως ότου αποκτήσει η άδεια.
- Όταν το νήμα ολοκληρωθεί με την πρόσβαση στον κοινόχρηστο πόρο, η άδεια αποδεσμεύεται και ο αριθμός των σηματοφόρων αυξάνεται έτσι ώστε ένα άλλο νήμα να μπορεί να επαναλάβει τα παραπάνω βήματα και να αποκτήσει την άδεια.
Τα παραπάνω βήματα της λειτουργίας των σημαφοφόρων μπορούν να συνοψιστούν στον παρακάτω διάγραμμα ροής.
Στην Java, δεν χρειάζεται να εφαρμόσουμε το σηματοφόρο μας, αλλά παρέχει ένα Σηματοφόρος τάξη που εφαρμόζει τη λειτουργικότητα του σηματοφόρου. Η κατηγορία Semaphore είναι μέρος του java.util.concurrent πακέτο.
Η κλάση Semaphore παρέχει τους ακόλουθους κατασκευαστές χρησιμοποιώντας τους οποίους μπορούμε να δημιουργήσουμε αντικείμενο semaphore:
Semaphore (int num_value) Semaphore (int num_value, boolean how)
Εδώ,
num_value => αρχική τιμή του αριθμού αδειών που καθορίζει τον αριθμό των νημάτων που μπορούν να έχουν πρόσβαση στον κοινόχρηστο πόρο.
πώς => ορίζει τη σειρά με την οποία θα δοθούν άδειες στα νήματα (how = true). Εάν how = false, τότε δεν ακολουθείται τέτοια παραγγελία.
Τώρα θα εφαρμόσουμε ένα πρόγραμμα Java που θα δείξει το Semaphore που χρησιμοποιείται για τη διαχείριση της πρόσβασης κοινόχρηστων πόρων και την αποτροπή της κατάστασης του αγώνα.
import java.util.concurrent.*; //class for shared resource class SharedRes { static int count = 0; } class ThreadClass extends Thread { Semaphore sem; String threadName; public ThreadClass(Semaphore sem, String threadName) { super(threadName); this.sem = sem; this.threadName = threadName; } @Override public void run() { // Thread T1 processing if(this.getName().equals('T1')) { System.out.println('Start: ' + threadName); try { System.out.println(threadName + ' :waiting for a permit.'); // acquire the permit sem.acquire(); System.out.println(threadName + ':Acquired permit'); // access shared resource for(int i=0; i <5; i++) { SharedRes.count++; System.out.println(threadName + ': ' + SharedRes.count); Thread.sleep(10); } } catch (InterruptedException exc) { System.out.println(exc); } // Release the permit. System.out.println(threadName + ':Released the permit'); sem.release(); } // Thread T2 processing else { System.out.println('Start: ' + threadName); try { System.out.println(threadName + ':waiting for a permit.'); // acquire the lock sem.acquire(); System.out.println(threadName + ':Acquired permit'); // process the shared resource for(int i=0; i < 5; i++) { SharedRes.count--; System.out.println(threadName + ': ' + SharedRes.count); Thread.sleep(10); } } catch (InterruptedException exc) { System.out.println(exc); } // Release the permit. System.out.println(threadName + ':Released the permit.'); sem.release(); } } } public class Main { public static void main(String args()) throws InterruptedException { //create Semaphore=> #permits = 1 Semaphore sem = new Semaphore(1); // Create thread instances T1 & T2 //T1=> Increments the count; T2=> Decrements the count ThreadClass thread1 = new ThreadClass(sem, 'T1'); ThreadClass thread2 = new ThreadClass(sem, 'T2'); // start T1 & T2 thread1.start(); thread2.start(); // Wait T1 & T2 thread1.join(); thread2.join(); System.out.println('count: ' + SharedRes.count); // display final count. } }
Παραγωγή
Αυτό το πρόγραμμα δήλωσε μια τάξη για τον κοινόχρηστο πόρο. Δηλώνει επίσης μια κλάση νήματος στην οποία έχουμε μια μεταβλητή semaphore που αρχικοποιείται στον κατασκευαστή κλάσης.
Στην μέθοδο overridden run () της κλάσης Thread, γίνεται επεξεργασία της παρουσίας νήματος στην οποία το νήμα αποκτά την άδεια, αποκτά πρόσβαση σε έναν κοινόχρηστο πόρο και στη συνέχεια απελευθερώνει την άδεια.
Στην κύρια μέθοδο, δηλώσαμε δύο παρουσίες νήματος. Και τα δύο νήματα ξεκινούν και στη συνέχεια περιμένουν χρησιμοποιώντας τη μέθοδο σύνδεσης. Τέλος, εμφανίζεται η μέτρηση, δηλ. 0 που δείχνει ότι και τα δύο νήματα έχουν τελειώσει με τον κοινόχρηστο πόρο.
Πιρούνι και εγγραφή στην Java
Το πλαίσιο fork / join παρουσιάστηκε για πρώτη φορά στην Java 7. Αυτό το πλαίσιο αποτελείται από εργαλεία που μπορούν να επιταχύνουν την παράλληλη επεξεργασία. Χρησιμοποιεί όλους τους διαθέσιμους πυρήνες επεξεργαστή στο σύστημα και ολοκληρώνει την εργασία. Το πλαίσιο fork / join χρησιμοποιεί την προσέγγιση διαίρεσης και κατάκτησης.
Η βασική ιδέα πίσω από το πλαίσιο Fork / Join είναι ότι το πρώτο πλαίσιο 'Forks', δηλαδή αναδρομικά διασπά την εργασία σε μικρότερες μεμονωμένες δευτερεύουσες εργασίες έως ότου οι εργασίες είναι ατομικές, ώστε να μπορούν να εκτελεστούν ασύγχρονα.
Αφού γίνει αυτό, οι εργασίες 'ενώνονται', δηλαδή όλες οι δευτερεύουσες εργασίες ενώνονται αναδρομικά σε μία μόνο εργασία ή τιμή επιστροφής.
Το πλαίσιο fork / join έχει ένα σύνολο νημάτων γνωστό ως 'ForkJoinPool'. Αυτή η ομάδα διαχειρίζεται τον τύπο 'ForkJoinWorkerThread' των νημάτων εργαζομένων παρέχοντας έτσι αποτελεσματική παράλληλη επεξεργασία.
Το ForkJoinPool διαχειρίζεται τα νήματα των εργαζομένων και μας βοηθά επίσης να λαμβάνουμε πληροφορίες σχετικά με την απόδοση και την κατάσταση του νήματος. Το ForkJoinPool είναι μια εφαρμογή του 'ExecutorService' που συζητήσαμε παραπάνω.
Σε αντίθεση με τα νήματα εργαζομένων, το ForkJoinPool δεν δημιουργεί ξεχωριστό νήμα για κάθε δευτερεύουσα εργασία. Κάθε νήμα στο ForkJoinPool διατηρεί το deque του (ουρά διπλού άκρου) για την αποθήκευση εργασιών.
Η deque λειτουργεί ως εξισορρόπηση του φόρτου εργασίας του νήματος και το κάνει με τη βοήθεια ενός «αλγόριθμου κλοπής εργασίας» που περιγράφεται παρακάτω.
Εργασία κλοπής αλγόριθμος
Μπορούμε να ορίσουμε τον αλγόριθμο κλοπής εργασίας με απλές λέξεις ως 'Εάν ένα νήμα είναι δωρεάν,' κλέψτε 'το έργο από πολυάσχολα νήματα'.
Ένα νήμα εργαζομένων θα παίρνει πάντα τις εργασίες από το deque του. Όταν όλες οι εργασίες στο deque έχουν εξαντληθεί και η deque είναι κενή, το νήμα εργαζομένου θα αναλάβει μια εργασία από την ουρά μιας άλλης deque ή από την «καθολική ουρά εισόδου».
Με αυτόν τον τρόπο ελαχιστοποιείται η πιθανότητα ανταγωνισμού νήματος για εργασίες και επίσης μειώνεται ο αριθμός των φορών που πρέπει να αναζητήσει το νήμα για εργασία. Αυτό συμβαίνει επειδή το νήμα έχει ήδη το μεγαλύτερο κομμάτι της διαθέσιμης εργασίας και το έχει τελειώσει.
Πώς μπορούμε λοιπόν να χρησιμοποιήσουμε το ForkJoinPool σε ένα πρόγραμμα;
Ο γενικός ορισμός του ForkJoinPool έχει ως εξής:
public class ForkJoinPool extends AbstractExecutorService
Η τάξη ForkJoinPool είναι μέρος του πακέτου 'java.util.concurrent'.
Στην Java 8, δημιουργούμε μια παρουσία του ForkJoinPool χρησιμοποιώντας τη στατική μέθοδο «common-pool ()» που παρέχει μια αναφορά στην κοινή πισίνα ή στην προεπιλεγμένη ομάδα νήματος.
ForkJoinPool commonPool = ForkJoinPool.commonPool ();
Στην Java 7, δημιουργούμε μια παρουσία ForkJoinPool και την εκχωρούμε στο πεδίο της κλάσης βοηθητικών προγραμμάτων όπως φαίνεται παρακάτω.
public static ForkJoinPool forkJoinPool = new ForkJoinPool(2);
Ο παραπάνω ορισμός υποδεικνύει ότι το σύνολο έχει επίπεδο παραλληλισμού 2 έτσι ώστε το σύνολο θα χρησιμοποιεί 2 πυρήνες επεξεργαστή.
Για να αποκτήσετε πρόσβαση στην παραπάνω ομάδα, μπορούμε να δώσουμε την ακόλουθη δήλωση.
ForkJoinPool forkJoinPool = PoolUtil.forkJoinPool;
Ο βασικός τύπος για εργασίες ForkJoinPool είναι 'ForkJoinTask'. Πρέπει να επεκτείνουμε μία από τις υποκατηγορίες του, δηλαδή για άκυρες εργασίες, το RecursiveAction και για εργασίες που επιστρέφουν μια τιμή, το RecursiveTask. Και οι δύο εκτεταμένες τάξεις παρέχουν μια αφηρημένη μέθοδο υπολογισμού () στην οποία ορίζουμε τη λογική της εργασίας.
Δίνεται παρακάτω ένα παράδειγμα για την επίδειξη του ForkJoinPool.
import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; //class declaration for ForkJoinPool tasks class FJPoolTask extends RecursiveAction { private long Load = 0; public FJPoolTask(long Load) { this.Load = Load; } @Override protected void compute() { //if threshold is reached, break tasks into smaller tasks List subtasks = new ArrayList(); subtasks.addAll(createSubtasks()); for(RecursiveAction subtask : subtasks){ subtask.fork(); } } //create subtasks private List createSubtasks() { List sub_tasks =new ArrayList(); FJPoolTask sub_task1 = new FJPoolTask(this.Load / 2); FJPoolTask sub_task2 = new FJPoolTask(this.Load / 2); FJPoolTask sub_task3 = new FJPoolTask(this.Load / 2); sub_tasks.add(sub_task1); sub_tasks.add(sub_task2); sub_tasks.add(sub_task3); return sub_tasks; } } public class Main { public static void main(final String() arguments) throws InterruptedException { //get count of available processors int proc = Runtime.getRuntime().availableProcessors(); System.out.println('Processors available:' +proc); //declare forkJoinPool ForkJoinPool Pool = ForkJoinPool.commonPool(); System.out.println(' Active Threads (Before invoke):' +Pool.getActiveThreadCount()); //Declare ForkJoinPool task object FJPoolTask t = new FJPoolTask(400); //submit the tasks to the pool Pool.invoke(t); System.out.println(' Active Threads (after invoke):' +Pool.getActiveThreadCount()); System.out.println('Common Pool Size :' +Pool.getPoolSize()); } }
Παραγωγή
Στο παραπάνω πρόγραμμα, εντοπίζουμε τον αριθμό των ενεργών νημάτων στο σύστημα πριν και μετά την κλήση της μεθόδου 'invoke ()'. Η μέθοδος invoke () χρησιμοποιείται για την υποβολή των εργασιών στην ομάδα. Βρίσκουμε επίσης τον αριθμό των διαθέσιμων πυρήνων επεξεργαστή στο σύστημα.
Συχνές Ερωτήσεις
Q # 1) Τι είναι το Java Util Concurrent;
Απάντηση: Το πακέτο 'java.util.concurrent' είναι ένα σύνολο κατηγοριών και διεπαφών που παρέχονται από την Java για τη διευκόλυνση της ανάπτυξης ταυτόχρονων (πολλαπλών νημάτων) εφαρμογών. Χρησιμοποιώντας αυτό το πακέτο μπορούμε να χρησιμοποιήσουμε απευθείας τη διεπαφή και τις τάξεις καθώς και τα API χωρίς να χρειαστεί να γράψουμε τα μαθήματά μας.
Q # 2) Ποιο από τα παρακάτω είναι ταυτόχρονες υλοποιήσεις που υπάρχουν στο java.util. ταυτόχρονο πακέτο;
Απάντηση: Σε υψηλό επίπεδο, το πακέτο java.util.concurrent περιέχει βοηθητικά προγράμματα όπως Executors, Synchronizers, Queues, Timings και Concurrent Συλλογές.
Q # 3) Τι είναι το Future Java;
Απάντηση: Ένα αντικείμενο Future (java.util.concurrent.Future) χρησιμοποιείται για την αποθήκευση του αποτελέσματος που επιστρέφεται από ένα νήμα όταν εφαρμόζεται η διεπαφή Callable.
Q # 4) Τι είναι ασφαλές στο νήμα στην Java;
Απάντηση: Ένας κώδικας ή κλάση ασφαλούς νήματος στην Java είναι ένας κωδικός ή κλάση που μπορεί να μοιραστεί σε ένα περιβάλλον πολλαπλών νημάτων ή ταυτόχρονα χωρίς προβλήματα και παράγει αναμενόμενα αποτελέσματα.
Q # 5) Ποια είναι η συγχρονισμένη συλλογή στην Java;
Απάντηση: Μια συγχρονισμένη συλλογή είναι μια συλλογή ασφαλών για νήματα. Η μέθοδος συγχρονισμένη συλλογή () της κλάσης java.util.Collections επιστρέφει μια συγχρονισμένη (ασφαλή νήμα) συλλογή.
συμπέρασμα
Με αυτό το σεμινάριο, έχουμε ολοκληρώσει το θέμα του multi-threading και του concurrency στην Java. Έχουμε συζητήσει λεπτομερώς το multithreading στα προηγούμενα σεμινάρια μας. Εδώ, συζητήσαμε τη συνάφεια και την εφαρμογή που σχετίζεται με την ταυτόχρονη συνένωση και τα πολλαπλά νήματα που αποτελούν μέρος του πακέτου java.util.concurrent.
Συζητήσαμε δύο ακόμη μεθόδους συγχρονισμού, τους σηματοφόρους και το ReentrantLock. Συζητήσαμε επίσης το ForkJoinPool που χρησιμοποιείται για την εκτέλεση των εργασιών, χωρίζοντάς τα σε απλούστερες εργασίες και, στη συνέχεια, ενώνουμε το αποτέλεσμα.
Το πακέτο java.util.concurrent υποστηρίζει επίσης το πλαίσιο Executor και τους εκτελεστές που μας βοηθούν να εκτελέσουμε νήματα. Συζητήσαμε επίσης την εφαρμογή του νήματος που αποτελείται από επαναχρησιμοποιήσιμα νήματα που επιστρέφονται στην ομάδα όταν ολοκληρωθεί η εκτέλεση.
Συζητήσαμε μια άλλη διεπαφή παρόμοια με το Runnable που μας βοηθά επίσης να επιστρέψουμε ένα αποτέλεσμα από το νήμα και το αντικείμενο Future που χρησιμοποιήθηκε για την αποθήκευση του αποτελέσματος του νήματος.
=> Παρακολουθήστε εδώ την απλή εκπαίδευση Java.
Συνιστώμενη ανάγνωση
- Thread.Sleep () - Thread Sleep () Μέθοδος στην Java με παραδείγματα
- Ανάπτυξη Java: Δημιουργία και εκτέλεση αρχείου Java JAR
- Βασικά στοιχεία Java: Java Syntax, Java Class και Core Java Concepts
- Java Virtual Machine: Πώς βοηθά το JVM στην εκτέλεση της εφαρμογής Java
- Πρόσβαση τροποποιητών σε Java - Εκμάθηση με παραδείγματα
- Java Synchronized: Τι είναι ο συγχρονισμός νημάτων στην Java
- Εκπαιδευτικό πρόγραμμα JAVA για αρχάριους: 100+ πρακτικά εκπαιδευτικά βίντεο Java
- Java Integer και Java BigInteger Class με παραδείγματα