هل من الآمن الحصول على قيم من java.util.HashMap من عدة سلاسل (بدون تعديل)؟

StackOverflow https://stackoverflow.com/questions/104184

سؤال

هناك حالة يتم فيها إنشاء الخريطة، وبمجرد تهيئتها، لن يتم تعديلها مرة أخرى أبدًا.ومع ذلك، سيتم الوصول إليه (عبر get(key) فقط) من عدة سلاسل رسائل.هل من الآمن استخدام أ java.util.HashMap في هذا الطريق؟

(في الوقت الحالي، أنا سعيد باستخدام ملف java.util.concurrent.ConcurrentHashMap, ، وليس لدي حاجة محسوبة لتحسين الأداء، ولكنني ببساطة أشعر بالفضول إذا كان الأمر بسيطًا HashMap يكفي.ومن هنا هذا السؤال لا "أي واحد يجب أن أستخدم؟" ولا هو سؤال أداء.بل السؤال هو "هل سيكون الأمر آمنًا؟")

هل كانت مفيدة؟

المحلول

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

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

على سبيل المثال، تخيل أنك تنشر الخريطة مثل هذا:

class SomeClass {
   public static HashMap<Object, Object> MAP;

   public synchronized static setMap(HashMap<Object, Object> m) {
     MAP = m;
   }
}

...وفي مرحلة ما setMap() يسمى بخريطة، وتستخدم المواضيع الأخرى SomeClass.MAP للوصول إلى الخريطة، والتحقق من وجود قيمة خالية مثل هذا:

HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
  .. use the map
} else {
  .. some default behavior
}

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

لنشر الخريطة بأمان، تحتاج إلى إنشاء يحدث من قبل العلاقة بين كتابة المرجع إلى HashMap (أي النشر) والقراء اللاحقين لذلك المرجع (أي الاستهلاك).ومن الملائم أنه لا يوجد سوى عدد قليل من الطرق التي يسهل تذكرها ينجز الذي - التي[1]:

  1. تبادل المرجع من خلال حقل مغلق بشكل صحيح (جى ال اس 17.4.5)
  2. استخدم المُهيئ الثابت للقيام بتخزين التهيئة (جى ال اس 12.4)
  3. تبادل المرجع عبر حقل متقلب (جى ال اس 17.4.5)، أو كنتيجة لهذه القاعدة، عبر فئات AtomicX
  4. تهيئة القيمة في الحقل النهائي (جى ال اس 17.5).

الأكثر إثارة للاهتمام بالنسبة للسيناريو الخاص بك هي (2)، (3)، و (4).على وجه الخصوص، (3) ينطبق مباشرة على الكود الموجود أعلاه:إذا قمت بتحويل إعلان MAP ل:

public static volatile HashMap<Object, Object> MAP;

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

تعمل الطرق الأخرى على تغيير دلالات طريقتك، نظرًا لأن كلاً من (2) (باستخدام البادئ الثابت) و(4) (باستخدام أخير) يعني أنه لا يمكنك التعيين MAP ديناميكيا في وقت التشغيل.إذا لم تفعل ذلك يحتاج للقيام بذلك، ثم أعلن فقط MAP ك static final HashMap<> ويضمن لك النشر الآمن.

من الناحية العملية، القواعد بسيطة للوصول الآمن إلى "الكائنات التي لم يتم تعديلها مطلقًا":

إذا كنت تنشر كائنًا ليس كذلك غير قابل للتغيير بطبيعته (كما هو الحال في جميع المجالات المعلنة final) و:

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

هذا كل شيء!

في الممارسة العملية، أنها فعالة جداً.استخدام أ static final الحقل، على سبيل المثال، يسمح لـ JVM بافتراض أن القيمة لم تتغير طوال عمر البرنامج وتحسينها بشكل كبير.استخدام أ final يسمح مجال الأعضاء معظم بنيات لقراءة الحقل بطريقة مكافئة لقراءة الحقل العادي ولا تمنع إجراء المزيد من التحسيناتج.

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

لمزيد من التفاصيل الدموية، راجع شيبيليف أو هذه الأسئلة الشائعة من مانسون وجوتز.


[1] نقلاً مباشراً شيبيليف.


أ قد يبدو هذا معقدًا، ولكن ما أعنيه هو أنه يمكنك تعيين المرجع في وقت الإنشاء - إما عند نقطة الإعلان أو في المنشئ (حقول الأعضاء) أو المُهيئ الثابت (الحقول الثابتة).

ب اختياريًا، يمكنك استخدام أ synchronized طريقة للحصول على/تعيين، أو AtomicReference أو شيء من هذا القبيل، ولكننا نتحدث عن الحد الأدنى من العمل الذي يمكنك القيام به.

ج بعض البنيات ذات نماذج الذاكرة الضعيفة جدًا (أنظر إلى أنت, ، Alpha) قد يتطلب نوعًا ما من حاجز القراءة قبل أ final اقرأ - ولكن هذه نادرة جدًا اليوم.

نصائح أخرى

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

لمزيد من المعلومات حول هذا الموضوع، اقرأ منشورات مدونة جيريمي:

الجزء الأول عن الثبات في جافا:http://jeremymanson.blogspot.com/2008/04/immutability-in-Java.html

الجزء الثاني عن الثبات في جافا:http://jeremymanson.blogspot.com/2008/07/immutability-in-Java-part-2.html

الجزء 3 حول الثبات في جافا:http://jeremymanson.blogspot.com/2008/07/immutability-in-Java-part-3.html

القراءات آمنة من وجهة نظر المزامنة ولكن ليس من وجهة نظر الذاكرة.هذا شيء يساء فهمه على نطاق واسع بين مطوري Java بما في ذلك هنا على Stackoverflow.(لاحظ تصنيف هذه الإجابة للإثبات.)

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

يرى مقالة بريان جويتز عن نموذج ذاكرة جافا الجديد للتفاصيل.

وبعد قليل من البحث، وجدت هذا في وثيقة جافا (التأكيد على الألغام):

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

ويبدو أن هذا يعني ضمناً أنها ستكون آمنة، على افتراض أن عكس العبارة هناك صحيح.

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

http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html

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

وفق http://www.ibm.com/developerworks/Java/library/j-jtp03304/ # أمان التهيئة، يمكنك جعل HashMap الخاص بك حقلاً نهائيًا وبعد انتهاء المنشئ سيتم نشره بأمان.

...ضمن نموذج الذاكرة الجديد، هناك شيء مشابه لعلاقة ما قبل حدوثه بين كتابة الحقل النهائي في المُنشئ والتحميل الأولي للمرجع المشترك لذلك الكائن في مؤشر ترابط آخر....

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

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

كن حذرًا، حتى في التعليمات البرمجية ذات الخيط الواحد، قد لا يكون استبدال ConcurrentHashMap بـ HashMap آمنًا.يحظر ConcurrentHashMap القيمة الخالية كمفتاح أو قيمة.HashMap لا يمنعهم (لا تسأل).

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

ومع ذلك، بشرط ألا تفعل شيئًا آخر، فإن عمليات القراءة المتزامنة من HashMap آمنة.

[يحرر:من خلال "القراءات المتزامنة"، أعني أنه لا توجد أيضًا تعديلات متزامنة.

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

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

http://www.docjar.com/html/api/Java/util/HashMap.java.html

هنا مصدر HashMap.كما يمكنك أن تقول، لا يوجد على الإطلاق رمز قفل/كائن المزامنة هناك.

هذا يعني أنه على الرغم من أنه من المقبول القراءة من HashMap في موقف متعدد الخيوط، إلا أنني بالتأكيد سأستخدم ConcurrentHashMap إذا كانت هناك عمليات كتابة متعددة.

الأمر المثير للاهتمام هو أن كلا من .NET HashTable وDictionary<K,V> يحتويان على كود مزامنة مدمج.

إذا تمت مزامنة التهيئة وكل وضع، فسيتم حفظك.

يتم حفظ الكود التالي لأن أداة تحميل الفصل ستهتم بالمزامنة:

public static final HashMap<String, String> map = new HashMap<>();
static {
  map.put("A","A");

}

يتم حفظ الكود التالي لأن كتابة المتغيرة ستهتم بالمزامنة.

class Foo {
  volatile HashMap<String, String> map;
  public void init() {
    final HashMap<String, String> tmp = new HashMap<>();
    tmp.put("A","A");
    // writing to volatile has to be after the modification of the map
    this.map = tmp;
  }
}

سيعمل هذا أيضًا إذا كان متغير العضو نهائيًا لأن Final متغير أيضًا.وإذا كانت الطريقة منشئًا.

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