سؤال

كلما ظهر سؤال حول SO حول مزامنة Java، فإن بعض الأشخاص حريصون جدًا على الإشارة إلى ذلك synchronized(this) يجب اجتنابها.بدلاً من ذلك، يزعمون أن القفل على مرجع خاص هو المفضل.

بعض الأسباب المذكورة هي:

أشخاص آخرون، بما فيهم أنا، يجادلون بذلك synchronized(this) هو مصطلح يستخدم كثيرًا (أيضًا في مكتبات Java)، وهو آمن ومفهوم جيدًا.لا ينبغي تجنب ذلك لأن لديك خطأ وليس لديك أدنى فكرة عما يحدث في برنامجك متعدد مؤشرات الترابط.بعبارة أخرى:إذا كان قابلاً للتطبيق، فاستخدمه.

أنا مهتم برؤية بعض الأمثلة الواقعية (بدون أشياء foobar) حيث يتم تجنب القفل this ويفضل عندما synchronized(this) سوف يقوم بهذه المهمة أيضًا.

لذلك: يجب عليك تجنب دائما synchronized(this) واستبداله بقفل على مرجع خاص؟


بعض المعلومات الإضافية (يتم تحديثها عند تقديم الإجابات):

  • نحن نتحدث عن مزامنة المثال
  • كلاهما ضمني (synchronized طرق) وشكل واضح من synchronized(this) مأخوذة في عين الأعتبار
  • إذا اقتبست من Bloch أو غيره من المراجع حول هذا الموضوع، فلا تتجاهل الأجزاء التي لا تعجبك (على سبيل المثال:جافا فعالة، عنصر على سلامة الموضوع: عادةً ما يكون هذا هو القفل الموجود على المثيل نفسه، ولكن هناك استثناءات.)
  • إذا كنت بحاجة إلى التفاصيل في القفل الخاص بك بخلاف synchronized(this) يقدم إذن synchronized(this) لا ينطبق لذلك هذه ليست القضية
هل كانت مفيدة؟

المحلول

سأغطي كل نقطة على حدة.

  1. Some evil code may steal your lock (very popular this one, also has an "accidentally" variant)

    أنا أكثر قلقا بشأن بطريق الخطأ.ما يرقى إليه هو أن هذا الاستخدام ل this هو جزء من الواجهة المكشوفة لفصلك، ويجب توثيقه.في بعض الأحيان تكون قدرة التعليمات البرمجية الأخرى على استخدام القفل الخاص بك مرغوبة.وهذا صحيح بالنسبة لأشياء مثل Collections.synchronizedMap (انظر جافادوك).

  2. All synchronized methods within the same class use the exact same lock, which reduces throughput

    هذا تفكير مفرط في التبسيط.مجرد التخلص من synchronized(this) لن يحل المشكلة.سوف تتطلب المزامنة الصحيحة للإنتاجية مزيدًا من التفكير.

  3. أنت (دون داع) تعرض الكثير من المعلومات

    هذا هو البديل رقم 1.استخدام synchronized(this) هو جزء من الواجهة الخاصة بك.إذا كنت لا تريد/تحتاج إلى كشف هذا الأمر، فلا تفعل ذلك.

نصائح أخرى

حسناً، أولاً لا بد من الإشارة إلى ما يلي:

public void blah() {
  synchronized (this) {
    // do stuff
  }
}

يعادل لغويا ما يلي:

public synchronized void blah() {
  // do stuff
}

وهو أحد أسباب عدم الاستخدام synchronized(this).قد تجادل بأنه يمكنك القيام بأشياء حول synchronized(this) حاجز.السبب المعتاد هو محاولة تجنب الاضطرار إلى إجراء التحقق المتزامن على الإطلاق، مما يؤدي إلى جميع أنواع مشكلات التزامن، وتحديدًا مشكلة القفل المزدوج, ، والذي يوضح مدى صعوبة إجراء فحص بسيط نسبيًا لمؤشر الترابط الآمن.

القفل الخاص هو آلية دفاعية، وهي ليست فكرة سيئة أبدًا.

وأيضًا، كما أشرت، يمكن للأقفال الخاصة التحكم في التفاصيل.قد تكون مجموعة واحدة من العمليات على كائن ما غير مرتبطة تمامًا بأخرى ولكن synchronized(this) سوف يستبعد بشكل متبادل الوصول إلى كل منهم.

synchronized(this) فقط حقا لا يعطيك أي شيء.

أثناء استخدامك syncronized(this) فإنك تستخدم مثيل الفئة كقفل بحد ذاته.هذا يعني أنه أثناء الحصول على القفل الموضوع 1, ، ال الموضوع 2 يجب أن ينتظر.

افترض الكود التالي:

public void method1() {
    // do something ...
    synchronized(this) {
        a ++;      
    }
    // ................
}


public void method2() {
    // do something ...
    synchronized(this) {
        b ++;      
    }
    // ................
}

الطريقة الأولى تعديل المتغير أ والطريقة الثانية تعديل المتغير ب, ، يجب تجنب التعديل المتزامن لنفس المتغير بواسطة خيطين وهو كذلك.ولكن في حين الموضوع1 تعديل أ و موضوع2 تعديل ب يمكن إجراؤه دون أي حالة سباق.

لسوء الحظ، فإن الكود أعلاه لن يسمح بذلك لأننا نستخدم نفس المرجع للقفل؛هذا يعني أن الخيوط حتى لو لم تكن في حالة سباق يجب أن تنتظر ومن الواضح أن الكود يضحي بتزامن البرنامج.

الحل هو الاستخدام 2 أقفال مختلفة ل اثنين متغيرات مختلفة:

public class Test {

    private Object lockA = new Object();
    private Object lockB = new Object();

    public void method1() {
        // do something ...
        synchronized(lockA) {
            a ++;      
        }
        // ................
    }


    public void method2() {
        // do something ...
        synchronized(lockB) {
            b ++;      
        }
        // ................
    }

}

يستخدم المثال أعلاه أقفالًا أكثر دقة (قفلان بدلاً من قفل واحد (lockA و قفلB للمتغيرات أ و ب على التوالي) ونتيجة لذلك يسمح بتزامن أفضل، ومن ناحية أخرى أصبح الأمر أكثر تعقيدا من المثال الأول ...

على الرغم من أنني أوافق على عدم الالتزام بشكل أعمى بالقواعد العقائدية، فهل يبدو سيناريو "سرقة القفل" غريبًا جدًا بالنسبة لك؟يمكن أن يحصل الخيط بالفعل على قفل الكائن الخاص بك "خارجيًا"(synchronized(theObject) {...})، حظر سلاسل الرسائل الأخرى التي تنتظر أساليب المثيل المتزامنة.

إذا كنت لا تؤمن بوجود تعليمات برمجية ضارة، فضع في اعتبارك أن هذه التعليمات البرمجية يمكن أن تأتي من أطراف ثالثة (على سبيل المثال، إذا قمت بتطوير نوع ما من خادم التطبيقات).

تبدو النسخة "العرضية" أقل احتمالا، ولكن كما يقولون، "اصنع شيئًا مقاومًا للأحمق وسيخترع شخص ما أحمقًا أفضل".

لذلك أنا أتفق مع المدرسة الفكرية التي تعتمد على ما يفعله الفصل.


قم بتحرير التعليقات الثلاثة الأولى لـ eljenso:

لم أواجه مطلقًا مشكلة سرقة القفل ولكن إليك سيناريو وهميًا:

لنفترض أن نظامك عبارة عن حاوية servlet، والكائن الذي نفكر فيه هو ServletContext تطبيق.إنه getAttribute يجب أن تكون الطريقة آمنة لمؤشر الترابط، حيث أن سمات السياق هي بيانات مشتركة؛لذلك تعلن ذلك synchronized.لنتخيل أيضًا أنك تقدم خدمة استضافة عامة بناءً على تنفيذ الحاوية الخاصة بك.

أنا عميلك وأقوم بنشر servlet "الجيد" الخاص بي على موقعك.يحدث أن الكود الخاص بي يحتوي على مكالمة إلى getAttribute.

يقوم أحد المتسللين، متنكرًا في هيئة عميل آخر، بنشر servlet الضار الخاص به على موقعك.يحتوي على الكود التالي في init طريقة:

synchronized (this.getServletConfig().getServletContext()) {
   while (true) {}
}

بافتراض أننا نشارك نفس سياق servlet (مسموح به بموجب المواصفات طالما أن اثنين من servlet موجودان على نفس المضيف الظاهري)، فإن مكالمتي على getAttribute مغلق إلى الأبد.لقد حقق المتسلل DoS على servlet الخاص بي.

هذا الهجوم غير ممكن إذا getAttribute تتم مزامنتها على قفل خاص، لأن رمز الطرف الثالث لا يمكنه الحصول على هذا القفل.

أعترف أن المثال مفتعل ووجهة نظر مفرطة في التبسيط لكيفية عمل حاوية servlet، ولكن IMHO يثبت هذه النقطة.

لذلك سأقوم باختيار التصميم الخاص بي بناءً على الاعتبارات الأمنية:هل سيكون لدي سيطرة كاملة على الكود الذي يمكنه الوصول إلى المثيلات؟ما هي نتيجة احتفاظ الخيط بقفل على مثيل إلى أجل غير مسمى؟

يبدو أن هناك إجماعًا مختلفًا في معسكرات C# وJava حول هذا الأمر. غالبية استخدامات كود Java التي رأيتها:

// apply mutex to this instance
synchronized(this) {
    // do work here
}

في حين أن غالبية كود C# تختار ما يمكن القول إنه أكثر أمانًا:

// instance level lock object
private readonly object _syncObj = new object();

...

// apply mutex to private instance level field (a System.Object usually)
lock(_syncObj)
{
    // do work here
}

من المؤكد أن لغة C# أكثر أمانًا.كما ذكرنا سابقًا، لا يمكن الوصول بشكل ضار/غير مقصود إلى القفل من خارج المثيل.كود جافا لديه هذا الخطر أيضًا، ولكن يبدو أن مجتمع Java قد انجذب بمرور الوقت إلى الإصدار الأقل أمانًا، ولكنه أكثر إيجازًا.

هذا لا يعني انتقادًا لجافا، بل هو مجرد انعكاس لتجربتي في العمل على كلتا اللغتين.

تعتمد على الموقف.
إذا كان هناك كيان مشترك واحد فقط أو أكثر.

انظر مثال العمل الكامل هنا

مقدمة صغيرة.

المواضيع والكيانات القابلة للمشاركة
من الممكن لخيوط متعددة الوصول إلى نفس الكيان، على سبيل المثال، خيوط اتصال متعددة تتشارك في قائمة انتظار رسائل واحدة.نظرًا لأن سلاسل الرسائل تعمل بشكل متزامن، فقد تكون هناك فرصة لتجاوز بيانات أحد الأشخاص بواسطة بيانات أخرى، وهو ما قد يكون موقفًا فوضويًا.
لذلك نحن بحاجة إلى طريقة ما لضمان الوصول إلى الكيان القابل للمشاركة من خلال مؤشر ترابط واحد فقط في كل مرة.(التزامن).

كتلة متزامنة
تعد الكتلة المتزامنة () طريقة لضمان الوصول المتزامن للكيان القابل للمشاركة.
أولا، تشبيه صغير
لنفترض أن هناك شخصين P1 وP2 (خيوط) وحوض غسيل (كيان قابل للمشاركة) داخل الحمام ويوجد باب (قفل).
نريد الآن أن يستخدم شخص واحد حوض الغسيل في كل مرة.
تتمثل إحدى الطرق في قفل الباب بواسطة P1 عندما يكون الباب مقفلاً، وينتظر P2 حتى يكمل P1 عمله
يفتح P1 الباب
عندها فقط p1 يمكنه استخدام حوض الغسيل.

بناء الجملة.

synchronized(this)
{
  SHARED_ENTITY.....
}

"هذا" يوفر القفل الجوهري المرتبط بالفئة (صمم مطور Java فئة الكائن بطريقة يمكن لكل كائن أن يعمل كشاشة).يعمل الأسلوب أعلاه بشكل جيد عندما يكون هناك كيان مشترك واحد وسلاسل رسائل متعددة (1:ن).
enter image description here N كيانات قابلة للمشاركة-M المواضيع
فكر الآن في الموقف الذي يوجد فيه حوضان للمغسلة داخل الحمام وباب واحد فقط.إذا كنا نستخدم النهج السابق، فيمكن لـ p1 فقط استخدام حوض غسيل واحد في كل مرة بينما سينتظر p2 في الخارج.إنه إهدار للموارد حيث لا أحد يستخدم B2 (حوض الغسيل).
قد يكون النهج الأكثر حكمة هو إنشاء غرفة أصغر داخل الحمام وتزويدهم بباب واحد لكل حوض غسيل.وبهذه الطريقة، يمكن لـ P1 الوصول إلى B1 ويمكن لـ P2 الوصول إلى B2 والعكس صحيح.

washbasin1;  
washbasin2;

Object lock1=new Object();
Object lock2=new Object();

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

enter image description here
enter image description here

رؤية المزيد عن المواضيع----> هنا

ال java.util.concurrent لقد قللت الحزمة إلى حد كبير من تعقيد الكود الآمن لمؤشر الترابط الخاص بي.ليس لدي سوى أدلة غير مؤكدة لأستمر بها، لكن معظم الأعمال التي رأيتها معها synchronized(x) يبدو أنه يعيد تنفيذ القفل أو الإشارة أو المزلاج، ولكن باستخدام الشاشات ذات المستوى الأدنى.

مع أخذ ذلك في الاعتبار، فإن المزامنة باستخدام أي من هذه الآليات تشبه المزامنة على كائن داخلي، بدلاً من تسريب القفل.وهذا مفيد لأنه لديك يقين مطلق بأنك تتحكم في الدخول إلى الشاشة من خلال خيطين أو أكثر.

  1. اجعل بياناتك غير قابلة للتغيير إذا كان ذلك ممكنًا ( final المتغيرات)
  2. إذا لم تتمكن من تجنب تحور البيانات المشتركة عبر سلاسل رسائل متعددة، فاستخدم بنيات برمجة عالية المستوى [على سبيل المثال.حبيبي Lock واجهة برمجة التطبيقات]

يوفر القفل وصولاً حصريًا إلى مورد مشترك:يمكن لمؤشر ترابط واحد فقط في كل مرة الحصول على القفل ويتطلب الوصول الكامل إلى المورد المشترك الحصول على القفل أولاً.

رمز العينة للاستخدام ReentrantLock الذي ينفذ Lock واجهه المستخدم

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

مزايا القفل على المزامنة(هذا)

  1. يؤدي استخدام الأساليب أو البيانات المتزامنة إلى فرض عملية الحصول على القفل وتحريره بطريقة منظمة.

  2. توفر تطبيقات القفل وظائف إضافية عبر استخدام الأساليب والبيانات المتزامنة من خلال توفيرها

    1. محاولة غير معيقة للحصول على قفل (tryLock())
    2. محاولة الحصول على القفل الذي يمكن مقاطعته (lockInterruptibly())
    3. محاولة للحصول على القفل الذي يمكن أن تنتهي المهلة (tryLock(long, TimeUnit)).
  3. يمكن أن توفر فئة القفل أيضًا سلوكًا ودلالات مختلفة تمامًا عن تلك الخاصة بقفل الشاشة الضمني، مثل

    1. طلب مضمون
    2. استخدام عدم إعادة الوافدين
    3. كشف الجمود

قم بإلقاء نظرة على سؤال SE هذا فيما يتعلق بأنواع مختلفة من Locks:

المزامنة مقابل القفل

يمكنك تحقيق أمان سلسلة المحادثات باستخدام واجهة برمجة التطبيقات المتزامنة المتقدمة بدلاً من الكتل المتزامنة.هذه الوثائق صفحة يوفر بنيات برمجة جيدة لتحقيق سلامة الخيط.

قفل الكائنات دعم التعابير القفلية التي تبسط العديد من التطبيقات المتزامنة.

المنفذين تحديد واجهة برمجة تطبيقات عالية المستوى لبدء وإدارة سلاسل الرسائل.توفر تطبيقات المنفذ المقدمة من java.util.concurrent إدارة تجمع مؤشرات الترابط المناسبة للتطبيقات واسعة النطاق.

المجموعات المتزامنة تسهيل إدارة مجموعات كبيرة من البيانات، ويمكن أن يقلل بشكل كبير من الحاجة إلى المزامنة.

المتغيرات الذرية تحتوي على ميزات تقلل من المزامنة وتساعد على تجنب أخطاء تناسق الذاكرة.

ThreadLocalRandom (في JDK 7) يوفر توليدًا فعالاً للأرقام العشوائية الزائفة من سلاسل رسائل متعددة.

تشير إلى java.util.concurrent و java.util.concurrent.atomic الحزم أيضًا لبنيات البرمجة الأخرى.

إذا كنت قد قررت ذلك:

  • و
  • تريد قفله بحبيبية أصغر من طريقة كاملة ؛

ثم لا أرى من المحرمات المزامنة (هذا).

يستخدم بعض الأشخاص عمدًا المزامنة (هذا) (بدلاً من وضع علامة على الطريقة المتزامنة) داخل المحتويات الكاملة للطريقة لأنهم يعتقدون أنه "أوضح للقارئ" ما هو الكائن الذي تتم مزامنته بالفعل.طالما أن الناس يتخذون قرارًا مستنيرًا (على سبيل المثالأدرك أنه من خلال القيام بذلك، فإنهم يقومون بالفعل بإدخال رموز ثانوية إضافية في الطريقة وقد يكون لهذا تأثير غير مباشر على التحسينات المحتملة)، ولا أرى مشكلة في هذا بشكل خاص.يجب عليك دائمًا توثيق السلوك المتزامن لبرنامجك، لذلك لا أرى أن حجة ""المزامنة" تنشر السلوك" مقنعة جدًا.

فيما يتعلق بمسألة قفل الكائن الذي يجب عليك استخدامه، أعتقد أنه لا يوجد خطأ في المزامنة على الكائن الحالي إذا كان هذا متوقعًا من خلال منطق ما تفعله وكيفية استخدام فصلك عادةً.على سبيل المثال، مع المجموعة، فإن الكائن الذي تتوقع تأمينه بشكل منطقي هو المجموعة نفسها بشكل عام.

أعتقد أن هناك تفسيرًا جيدًا لسبب كون كل من هذه التقنيات حيوية تحت حزامك في كتاب يسمى Java Concurrency In Practice بقلم بريان جويتز.لقد أوضح نقطة واحدة للغاية - يجب عليك استخدام نفس القفل "في كل مكان" لحماية حالة الكائن الخاص بك.غالبًا ما تسير الطريقة المتزامنة والمزامنة على كائن ما جنبًا إلى جنب.على سبيل المثاليقوم Vector بمزامنة جميع أساليبه.إذا كان لديك مؤشر لكائن متجه وستفعل "وضعه في حالة غيابه"، فإن مجرد مزامنة Vector لأساليبه الفردية لن يحميك من تلف الحالة.تحتاج إلى المزامنة باستخدام المتزامن (vectorHandle).سيؤدي هذا إلى الحصول على القفل SAME بواسطة كل مؤشر ترابط له مقبض للمتجه وسيحمي الحالة العامة للمتجه.وهذا ما يسمى القفل من جانب العميل.نحن نعلم في الواقع أن المتجه يقوم بالمزامنة (هذا)/يزامن جميع أساليبه، وبالتالي فإن المزامنة على كائن VectorHandle ستؤدي إلى مزامنة مناسبة لحالة الكائنات المتجهة.من الحماقة الاعتقاد بأنك آمن للخيوط لمجرد أنك تستخدم مجموعة آمنة للخيوط.هذا هو بالضبط السبب وراء قيام ConcurrentHashMap بتقديم طريقة putIfAbsent بشكل صريح - لجعل مثل هذه العمليات ذرية.

في ملخص

  1. تسمح المزامنة على مستوى الطريقة بالقفل من جانب العميل.
  2. إذا كان لديك كائن قفل خاص - فهذا يجعل القفل من جانب العميل مستحيلاً.يعد هذا أمرًا جيدًا إذا كنت تعلم أن فصلك لا يحتوي على نوع الوظيفة "وضع في حالة الغياب".
  3. إذا كنت تصمم مكتبة - فغالبًا ما تكون المزامنة أو مزامنة الطريقة أكثر حكمة.لأنك نادرًا ما تكون في وضع يسمح لك بتحديد كيفية استخدام فصلك.
  4. لو استخدم Vector كائن قفل خاص - لكان من المستحيل الحصول على "وضع في حالة الغياب" بشكل صحيح.لن يتمكن رمز العميل مطلقًا من التحكم في القفل الخاص، مما يؤدي إلى كسر القاعدة الأساسية لاستخدام القفل نفسه تمامًا لحماية حالته.
  5. تواجه المزامنة على هذا أو الطرق المتزامنة مشكلة كما أشار آخرون - يمكن أن يحصل شخص ما على قفل ولا يطلقه أبدًا.ستظل جميع الخيوط الأخرى في انتظار تحرير القفل.
  6. لذا اعرف ما تفعله واعتمد ما هو صحيح.
  7. جادل أحدهم بأن وجود كائن قفل خاص يمنحك دقة أفضل - على سبيل المثال.إذا كانت هناك عمليتان غير مرتبطتين - فيمكن حمايتهما بأقفال مختلفة مما يؤدي إلى إنتاجية أفضل.ولكن أعتقد أن هذا هو رائحة التصميم وليس رائحة الكود - إذا كانت هناك عمليتان غير مرتبطتين تمامًا، فلماذا تكونان جزءًا من نفس الفئة؟لماذا يجب على نادي الطبقة وظائف غير ذات صلة على الإطلاق؟قد تكون فئة المنفعة؟هممم - بعض الأدوات المساعدة توفر معالجة السلسلة وتنسيق تاريخ التقويم من خلال نفس المثيل؟؟...لا معنى له بالنسبة لي على الأقل!!

لا، لا ينبغي لك دائماً.ومع ذلك، فإنني أميل إلى تجنب ذلك عندما تكون هناك مخاوف متعددة بشأن كائن معين يحتاج فقط إلى أن يكون آمنًا فيما يتعلق بنفسه.على سبيل المثال، قد يكون لديك كائن بيانات قابل للتغيير يحتوي على حقلي "التسمية" و"الأصل"؛يجب أن تكون هذه المواضيع آمنة، لكن تغيير أحدهما لا يلزم أن يمنع الآخر من الكتابة/القراءة.(من الناحية العملية، سأتجنب ذلك من خلال الإعلان عن الحقول المتطايرة و/أو استخدام أغلفة AtomicFoo الخاصة بـ java.util.concurrent).

تعتبر المزامنة بشكل عام خرقاء بعض الشيء، لأنها تؤدي إلى إغلاق كبير بدلاً من التفكير بالضبط في كيفية السماح للخيوط بالعمل حول بعضها البعض.استخدام synchronized(this) بل إنها أكثر خرقًا ومعادية للمجتمع، حيث تقول "لا يمكن لأحد أن يتغير". أي شئ في هذا الفصل بينما أحمل القفل".كم مرة تحتاج فعلا للقيام بذلك؟

أفضل أن أحصل على المزيد من الأقفال الحبيبية؛حتى لو كنت تريد إيقاف كل شيء من التغيير (ربما تقوم بإجراء تسلسل للكائن)، يمكنك فقط الحصول على جميع الأقفال لتحقيق نفس الشيء، بالإضافة إلى أن الأمر أكثر وضوحًا بهذه الطريقة.عندما تستخدم synchronized(this), ، ليس من الواضح بالضبط سبب قيامك بالمزامنة، أو ما هي الآثار الجانبية المحتملة.إذا كنت تستخدم synchronized(labelMonitor), ، أو حتى أفضل labelLock.getWriteLock().lock(), ، فمن الواضح ما تفعله وما تقتصر عليه تأثيرات القسم النقدي الخاص بك.

اجابة قصيرة:عليك أن تفهم الفرق وتختار بناءً على الكود.

اجابة طويلة:بشكل عام أفضل أن أحاول تجنبه مزامنة (هذا) لتقليل التنافس ولكن الأقفال الخاصة تضيف تعقيدًا يجب أن تكون على دراية به.لذا استخدم المزامنة الصحيحة للوظيفة المناسبة.إذا لم تكن لديك خبرة كبيرة في البرمجة متعددة الخيوط، فأنا أفضل الالتزام بقفل المثيل وقراءة هذا الموضوع.(هكذا قال:مجرد استخدام مزامنة (هذا) لا يجعل صفك آمنًا تمامًا لسلسلة الرسائل تلقائيًا.) هذا ليس موضوعًا سهلاً ولكن بمجرد أن تعتاد عليه، فإن الإجابة عما إذا كنت تريد استخدامه مزامنة (هذا) أو لا يأتي بشكل طبيعي.

يتم استخدام القفل لأي منهما الرؤية أو لحماية بعض البيانات من التعديل المتزامن مما قد يؤدي إلى العرق.

عندما تحتاج فقط إلى إجراء عمليات الكتابة البدائية لتكون ذرية، فهناك خيارات متاحة مثل AtomicInteger وأمثال.

ولكن لنفترض أن لديك عددين صحيحين مرتبطين ببعضهما البعض x و y الإحداثيات التي ترتبط ببعضها البعض ويجب تغييرها بطريقة ذرية.ومن ثم ستحميهم باستخدام نفس القفل.

يجب أن يحمي القفل فقط الحالة المرتبطة ببعضها البعض.لا أقل ولا أكثر.إذا كنت تستخدم synchronized(this) في كل طريقة، حتى لو كانت حالة الفئة غير مرتبطة، ستواجه جميع سلاسل الرسائل تنافسًا حتى في حالة تحديث الحالة غير المرتبطة.

class Point{
   private int x;
   private int y;

   public Point(int x, int y){
       this.x = x;
       this.y = y;
   }

   //mutating methods should be guarded by same lock
   public synchronized void changeCoordinates(int x, int y){
       this.x = x;
       this.y = y;
   }
}

في المثال أعلاه لدي طريقة واحدة فقط تعمل على تغيير كليهما x و y وليس طريقتين مختلفتين كما x و y ترتبط ببعضها البعض وإذا كنت قد قدمت طريقتين مختلفتين للتحول x و y بشكل منفصل فإنه لم يكن من الممكن أن يكون آمنًا.

هذا المثال هو فقط للتوضيح وليس بالضرورة الطريقة التي ينبغي تنفيذها بها.أفضل طريقة للقيام بذلك هي القيام بذلك غير قابل للتغيير.

الآن معارضة ل Point على سبيل المثال، هناك مثال على ذلك TwoCounters تم توفيرها بالفعل بواسطةAndreas حيث تكون الحالة محمية بواسطة قفلين مختلفين لأن الحالة غير مرتبطة ببعضها البعض.

تسمى عملية استخدام أقفال مختلفة لحماية الحالات غير المرتبطة قفل الشريط أو قفل الانقسام

سبب عدم المزامنة هذا هو أنك في بعض الأحيان تحتاج إلى أكثر من قفل واحد (غالبًا ما تتم إزالة القفل الثاني بعد بعض التفكير الإضافي، لكنك لا تزال بحاجة إليه في الحالة المتوسطة).إذا قمت بقفل هذا, ، عليك دائمًا أن تتذكر أيًا من القفلين هو هذا;إذا قمت بقفل كائن خاص، فإن اسم المتغير يخبرك بذلك.

من وجهة نظر القارئ، إذا رأيت القفل هذا, ، عليك دائمًا الإجابة على السؤالين:

  1. ما هو نوع الوصول المحمي بواسطة هذا?
  2. هل قفل واحد يكفي حقًا، ألم يقم أحد بإدخال خطأ؟

مثال:

class BadObject {
    private Something mStuff;
    synchronized setStuff(Something stuff) {
        mStuff = stuff;
    }
    synchronized getStuff(Something stuff) {
        return mStuff;
    }
    private MyListener myListener = new MyListener() {
        public void onMyEvent(...) {
            setStuff(...);
        }
    }
    synchronized void longOperation(MyListener l) {
        ...
        l.onMyEvent(...);
        ...
    }
}

إذا بدأ موضوعين longOperation() في حالتين مختلفتين BadObject, ، يحصلون على أقفالهم.عندما يحين وقت الاستدعاء l.onMyEvent(...), ، لدينا طريق مسدود لأنه لم يتمكن أي من الخيوط من الحصول على قفل الكائن الآخر.

في هذا المثال، يمكننا إزالة حالة الجمود باستخدام قفلين، أحدهما للعمليات القصيرة والآخر للعمليات الطويلة.

كما قلنا سابقًا، يمكن للكتلة المتزامنة استخدام متغير محدد من قبل المستخدم ككائن قفل، عندما تستخدم الوظيفة المتزامنة "هذا" فقط.وبالطبع يمكنك التعامل مع مناطق وظيفتك التي يجب مزامنتها وما إلى ذلك.

لكن الجميع يقول أنه لا يوجد فرق بين الوظيفة المتزامنة والكتلة التي تغطي الوظيفة بأكملها باستخدام "هذا" ككائن قفل.هذا ليس صحيحا، والفرق هو في رمز البايت الذي سيتم إنشاؤه في كلتا الحالتين.في حالة استخدام الكتلة المتزامنة، يجب تخصيص متغير محلي يحمل إشارة إلى "هذا".ونتيجة لذلك، سيكون لدينا حجم أكبر قليلاً من الوظائف (لا ينطبق ذلك إذا كان لديك عدد قليل فقط من الوظائف).

شرح أكثر تفصيلاً للفرق يمكنك العثور عليه هنا:http://www.artima.com/insidejvm/ed2/threadsynchP.html

كما أن استخدام الكتلة المتزامنة ليس جيدًا نظرًا لوجهة النظر التالية:

الكلمة الأساسية المتزامنة محدودة للغاية في منطقة واحدة:عند الخروج من كتلة متزامنة، يجب إلغاء حظر جميع الخيوط التي تنتظر هذا القفل، ولكن واحد فقط من تلك الخيوط يمكنه الحصول على القفل؛يرى جميع الآخرين أن القفل قد تم أخذه ويعودون إلى الحالة المحظورة.هذا ليس مجرد الكثير من دورات المعالجة الضائعة:غالبًا ما يتضمن تبديل السياق لإلغاء حظر سلسلة رسائل أيضًا ذاكرة ترحيل من القرص، وهذا مكلف للغاية.

لمزيد من التفاصيل في هذا المجال أنصحك بقراءة هذا المقال:http://java.dzone.com/articles/synchronized-considered

يعد هذا في الحقيقة مجرد تكملة للإجابات الأخرى، ولكن إذا كان اعتراضك الرئيسي على استخدام الكائنات الخاصة للقفل هو أنه يزدحم صفك بحقول لا تتعلق بمنطق الأعمال، فإن مشروع لومبوك لديه @Synchronized لإنشاء النموذج المعياري في وقت الترجمة:

@Synchronized
public int foo() {
    return 0;
}

يجمع ل

private final Object $lock = new Object[0];

public int foo() {
    synchronized($lock) {
        return 0;
    }
}

مثال جيد للاستخدام المتزامن (هذا).

// add listener
public final synchronized void addListener(IListener l) {listeners.add(l);}
// remove listener
public final synchronized void removeListener(IListener l) {listeners.remove(l);}
// routine that raise events
public void run() {
   // some code here...
   Set ls;
   synchronized(this) {
      ls = listeners.clone();
   }
   for (IListener l : ls) { l.processEvent(event); }
   // some code here...
}

كما ترون هنا، نحن نستخدم المزامنة في هذا لتسهيل التعاون المطول (ربما طريقة حلقة لا نهائية من التشغيل) مع بعض الطرق المتزامنة هناك.

بالطبع يمكن إعادة كتابتها بسهولة شديدة باستخدام المزامنة في المجال الخاص.لكن في بعض الأحيان، عندما يكون لدينا بالفعل بعض التصميم باستخدام أساليب متزامنة (على سبيل المثال،الفئة القديمة التي نشتق منها، يمكن أن تكون المزامنة (هذا) هي الحل الوحيد).

يعتمد الأمر على المهمة التي تريد القيام بها، لكنني لن أستخدمها.تحقق أيضًا مما إذا كان حفظ الخيط الذي تريد تحقيقه لا يمكن تنفيذه عن طريق المزامنة (هذا) في المقام الأول؟وهناك أيضا بعض لطيفة أقفال في API قد يساعدك ذلك :)

أريد فقط أن أذكر حلاً ممكنًا للمراجع الخاصة الفريدة في الأجزاء الذرية من التعليمات البرمجية بدون تبعيات.يمكنك استخدام Hashmap ثابت مع أقفال وطريقة ثابتة بسيطة تسمى atomic() التي تنشئ المراجع المطلوبة تلقائيًا باستخدام معلومات المكدس (اسم الفئة الكامل ورقم السطر).ثم يمكنك استخدام هذه الطريقة في مزامنة البيانات دون كتابة كائن قفل جديد.

// Synchronization objects (locks)
private static HashMap<String, Object> locks = new HashMap<String, Object>();
// Simple method
private static Object atomic() {
    StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point 
    StackTraceElement exepoint = stack[2];
    // creates unique key from class name and line number using execution point
    String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber()); 
    Object lock = locks.get(key); // use old or create new lock
    if (lock == null) {
        lock = new Object();
        locks.put(key, lock);
    }
    return lock; // return reference to lock
}
// Synchronized code
void dosomething1() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 1
        ...
    }
    // other command
}
// Synchronized code
void dosomething2() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 2
        ...
    }
    // other command
}

تجنب استخدام synchronized(this) كآلية قفل:يؤدي هذا إلى تأمين مثيل الفصل بأكمله ويمكن أن يسبب حالة توقف تام.في مثل هذه الحالات، قم بإعادة بناء الكود لقفل طريقة أو متغير محدد فقط، وبهذه الطريقة لا يتم قفل الفصل بأكمله. Synchronised يمكن استخدامها داخل مستوى الطريقة.
بدلا من استخدام synchronized(this), ، يوضح الكود أدناه كيف يمكنك قفل الطريقة.

   public void foo() {
if(operation = null) {
    synchronized(foo) { 
if (operation == null) {
 // enter your code that this method has to handle...
          }
        }
      }
    }

سنتي في عام 2019 على الرغم من أنه كان من الممكن تسوية هذا السؤال بالفعل.

قفل "هذا" ليس سيئًا إذا كنت تعرف ما تفعله ولكن قفل "هذا" خلف الكواليس هو (وهو للأسف ما تسمح به الكلمة الرئيسية المتزامنة في تعريف الطريقة).

إذا كنت تريد بالفعل أن يتمكن مستخدمو صفك من "سرقة" القفل الخاص بك (على سبيل المثال،منع سلاسل الرسائل الأخرى من التعامل معها)، فأنت تريد بالفعل أن تنتظر جميع الطرق المتزامنة أثناء تشغيل طريقة مزامنة أخرى وما إلى ذلك.يجب أن تكون مقصودة ومدروسة جيدًا (وبالتالي موثقة لمساعدة المستخدمين على فهمها).

لمزيد من التفاصيل، في المقابل، يجب أن تعرف ما "تكسبه" (أو "تخسره") إذا قمت بقفل قفل لا يمكن الوصول إليه (لا يستطيع أحد "سرقة" قفلك، فأنت تتمتع بالسيطرة الكاملة وما إلى ذلك. ..).

المشكلة بالنسبة لي هي أن الكلمة الرئيسية المتزامنة في توقيع تعريف الطريقة تجعل الأمر سهلاً للغاية بالنسبة للمبرمجين لا تفكر حول ما يجب قفله، وهو أمر مهم للغاية يجب التفكير فيه إذا كنت لا ترغب في مواجهة مشكلات في برنامج متعدد الخيوط.

لا يمكن للمرء أن يجادل بأنك "عادةً" لا تريد أن يتمكن مستخدمو صفك من القيام بهذه الأشياء أو ما تريده "عادةً"... يعتمد الأمر على الوظيفة التي تقوم بترميزها.لا يمكنك وضع قاعدة أساسية لأنه لا يمكنك التنبؤ بجميع حالات الاستخدام.

خذ بعين الاعتبار على سبيل المثال.كاتب الطباعة الذي يستخدم قفلًا داخليًا ولكن بعد ذلك يكافح الأشخاص لاستخدامه من عدة سلاسل إذا كانوا لا يريدون أن تتداخل مخرجاتهم.

هل يمكن الوصول إلى القفل الخاص بك خارج الفصل أم لا، فهذا هو قرارك كمبرمج على أساس الوظيفة التي يتمتع بها الفصل.إنه جزء من واجهة برمجة التطبيقات.لا يمكنك الابتعاد على سبيل المثال من المزامنة (هذا) إلى المزامنة (provateObjet) دون المخاطرة بكسر التغييرات في الكود الذي يستخدمه.

ملاحظة 1:أعلم أنه يمكنك تحقيق كل ما "يحققه" المتزامن (هذا) باستخدام كائن قفل صريح وكشفه ولكن أعتقد أنه غير ضروري إذا كان سلوكك موثقًا جيدًا وأنت تعرف بالفعل ما يعنيه القفل على "هذا".

ملاحظة 2:أنا لا أتفق مع الحجة القائلة بأنه إذا قام بعض التعليمات البرمجية بسرقة قفلك عن طريق الخطأ، فهذا خطأ ويجب عليك حله.هذه بطريقة ما هي نفس الحجة التي تقول إنني أستطيع أن أجعل كل أساليبي علنية حتى لو لم يكن من المفترض أن تكون علنية.إذا اتصل شخص ما "عن طريق الخطأ" بطريقتي المقصودة أن تكون خاصة، فهذا خطأ.لماذا تمكين هذا الحادث في المقام الأول !!!إذا كانت القدرة على سرقة القفل الخاص بك تمثل مشكلة لفصلك فلا تسمح بذلك.بهذه البساطة.

أعتقد أن النقطتين الأولى (شخص آخر يستخدم القفل الخاص بك) والنقطتين (جميع الطرق التي تستخدم نفس القفل دون داع) يمكن أن تحدث في أي تطبيق كبير إلى حد ما.خاصة عندما لا يكون هناك تواصل جيد بين المطورين.

إنها ليست مسألة ثابتة، إنها في الغالب مسألة تتعلق بالممارسات الجيدة ومنع الأخطاء.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top