דוגמת נעילת Java – ReentrantLock

ברוך הבא למדריך לדוגמה של Java Lock. בדרך כלל, כשאתה עובד עם סביבה שבה פועלים רב-תהליכית, אנו משתמשים ב-keyword synchronized לשמירה על בטיחות התהליכים.

Java Lock

רוב הזמן, מילת המפתח synchronized היא הדרך ללכת, אך יש לה מגבלות שמביאות לכלול של Lock API בחבילת ה-Concurrency של Java. ב-Java 1.5 Concurrency API נוצרה חבילת java.util.concurrent.locks עם ממשק ה-Lock וכמה מחלקות ניבויות כדי לשפר את מנגנון נעילת האובייקט. ממשקים ומחלקות חשובות ב-Lock API של Java הם:

  1. Lock: זהו ממשק הבסיס ל-Lock API. הוא מספק את כל התכונות של מילת המפתח synchronized עם אפשרויות נוספות ליצירת תנאים שונים לנעילה, ספק timeout לתהליך המחכה לנעילה. כמה מהשיטות החשובות הם lock() לרכישת הנעילה, unlock() לשחרור הנעילה, tryLock() להמתנה לנעילה לפרק זמן מסוים, newCondition() ליצירת התנאי וכו'

  2. תנאי: אובייקטי תנאי דומים למודל של התכונה wait-notify של אובייקט עם תכונה נוספת ליצירת סטים שונים של המתנה. אובייקט תנאי תמיד נוצר על ידי אובייקט נעילה. חלק מהשיטות החשובות הן await() שדומה ל wait() ו-signal(), signalAll() שדומים ל-notify() ולשיטות notifyAll().

  3. נעילת קריאה/כתיבה: היא מכילה זוג של נעילות משויכות, אחת לפעולות קריאה בלבד והשנייה לכתיבה. הנעילה לקריאה עשויה להיות בעת זו נחזקת על ידי מספר של תהליכי קוראים בו זמנית כל עוד אין תהליכי כתיבה. הנעילה לכתיבה היא בלעדית.

  4. נעילתחזור: זוהי קבוצת היישום הנפלאה ביותר של ממשק הנעילה. קבוצה זו מיישמת את ממשק הנעילה באותה הדרך כמו מילת המפתח synchronized. בנוסף ליישום של ממשק הנעילה, נעילתחזור מכילה מספר שיטות עזר לקבלת התהליך החולק את הנעילה, התהליכים הממתינים לרכוש את הנעילה וכו'. הבלוקים synchronized הם באופיים חוזרים, כלומר אם לתהליך יש נעילה על אובייקט המוניטור ואם בלוק synchronized אחר דורש לקבל את הנעילה על אותו אובייקט מוניטור, התהליך יכול להיכנס לבלוק קוד זה. אני חושב שזהו הסיבה שלשם הקבוצה הוא נעילתחזור. בואו נבין את התכונה הזו בדוגמה פשוטה.

    ציבוריוביםבמחלקה Test{
    
    public synchronized foo(){
        //עשה משהו
        bar();
      }
    
      public synchronized bar(){
        //עשה משהו נוסף
      }
    }
    

    אם תהליך נכנס ל־foo(), יש לו נעילה על אובייקט ה־Test, כלומר כאשר הוא מנסה לבצע את שיטת bar(), התהליך רשאי לבצע את שיטת bar() מאחר והוא כבר מחזיק בנעילה על אובייקט ה־Test, כלומר זהה ל־synchronized(this).

דוגמה לנעילת Java – ReentrantLock ב־Java

עכשיו בואו נראה דוגמה פשוטה שבה נחליף את המילה synchronized ב־API הנעילה של Java. בואו נניח שיש לנו מחלקת משאב עם מספר פעולות שבהן נרצה שיהיה thread-safe וכמה שיטות שבהן אין צורך בביטחון לסיבות מקומיות.

package com.journaldev.threads.lock;

public class Resource {

	public void doSomething(){
		//בצע פעולה מסוימת, קריאת DB, כתיבה וכו'
	}
	
	public void doLogging(){
		//לוגים, אין צורך בביטחון לסיבות מקומיות
	}
}

עכשיו נניח שיש לנו כיתה Runnable בה נשתמש בשיטות המשאב.

package com.journaldev.threads.lock;

public class SynchronizedLockExample implements Runnable{

	private Resource resource;
	
	public SynchronizedLockExample(Resource r){
		this.resource = r;
	}
	
	@Override
	public void run() {
		synchronized (resource) {
			resource.doSomething();
		}
		resource.doLogging();
	}
}

שימו לב שאני משתמש בבלוק מסונכרן כדי לקבל את הנעילה על אובייקט המשאב. יכולנו ליצור אובייקט מזוייף בכיתה ולהשתמש בו למטרת נעילה. עכשיו נראה כיצד אנו יכולים להשתמש ב- API Lock של Java ולכתוב מחדש את התוכנית הנ"ל מבלי להשתמש במילת המפתח synchronized. נשתמש ב- ReentrantLock ב-Java.

package com.journaldev.threads.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConcurrencyLockExample implements Runnable{

	private Resource resource;
	private Lock lock;
	
	public ConcurrencyLockExample(Resource r){
		this.resource = r;
		this.lock = new ReentrantLock();
	}
	
	@Override
	public void run() {
		try {
			if(lock.tryLock(10, TimeUnit.SECONDS)){
			resource.doSomething();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			//שחרור נעילה
			lock.unlock();
		}
		resource.doLogging();
	}

}

כפי שניתן לראות, אני משתמש בשיטת tryLock() כדי לוודא שהתהליך שלי ממתין רק לזמן מוגדר ואם הוא לא מקבל את הנעילה על האובייקט, הוא פשוט מייחס ויוצא. נקודה חשובה נוספת לשים לב אליה היא שימוש בבלוק try-finally כדי לוודא שהנעילה משוחררת גם אם קריאה לשיטת doSomething() זורקת חריגה כלשהי.

Java Lock נגד synchronized

בהתבסס על הפרטים והתוכנית לעיל, נוכל בקלות להסיק את ההבדלים הבאים בין Java Lock ובין synchronized.

  1. Java Lock API מספק יותר גישות ואפשרויות נעילה, להבדל מ synchronized שבו תהליך עשוי להסתיים בהמתנה לא סופית על הנעילה, נוכל להשתמש ב- tryLock() כדי לוודא שהתהליך ממתין רק לזמן ספציפי בלבד.
  2. הקוד של synchronized הוא נקי וקל לתחזוקה יותר בעוד עם Lock אנחנו חייבים להשתמש בבלוק try-finally כדי לוודא שהנעילה משוחררת גם אם יש המון חריגות בין קריאות לשיטות lock() ו-unlock().
  3. בלוקי סנכרון או שיטות יכולים לכסות רק שיטה אחת, בעוד שאנו יכולים לרכוש את הנעילה בשיטה אחת ולשחרר אותה בשיטה אחרת עם ממשק הנעילה API.
  4. מילת המפתח synchronized לא מספקת הגיוניות, בעוד שאנו יכולים להגדיר הגיוניות ל־true בזמן יצירת אובייקט ReentrantLock כך שלארך זמן ההמתנה הארוך ביותר יקבל הלוך וקבל קודם.
  5. אנו יכולים ליצור תנאים שונים עבור הנעילה ולהמתין כל תהליך לתנאים שונים באמצעות הפעולה await().

זהו הכל לדוגמה של נעילת Java, ReentrantLock ב־java וניתוח השוואתי עם מילת המפתח synchronized.

Source:
https://www.digitalocean.com/community/tutorials/java-lock-example-reentrantlock