سؤال

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

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

بدا الرمز الأصلي شيئًا مثل هذا:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {

  final String key = "Data-" + email;
  SomeData[] data = (SomeData[]) StaticCache.get(key);

  if (data == null) {
      data = service.getSomeDataForEmail(email);

      StaticCache.set(key, data, CACHE_TIME);
  }
  else {
      logger.debug("getSomeDataForEmail: using cached object");
  }

  return data;
}

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

لقد قمت بتحديث الطريقة لتبدو هكذا:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {


  SomeData[] data = null;
  final String key = "Data-" + email;

  synchronized(key) {      
    data =(SomeData[]) StaticCache.get(key);

    if (data == null) {
        data = service.getSomeDataForEmail(email);
        StaticCache.set(key, data, CACHE_TIME);
    }
    else {
      logger.debug("getSomeDataForEmail: using cached object");
    }
  }

  return data;
}

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

ومع ذلك، لا يبدو أن هذا قد نجح.تحتوي سجلات الاختبار الخاصة بي على مخرجات مثل:

(إخراج السجل هو "اسم الموضوع" "اسم المسجل" "الرسالة")
http-80-Processor253 jsp.view-page - getSomeDataForEmail:على وشك الدخول إلى كتلة المزامنة
http-80-Processor253 jsp.view-page - getSomeDataForEmail:داخل كتلة المزامنة
http-80-Processor253 ذاكرة التخزين المؤقت.StaticCache - احصل على:انتهت صلاحية الكائن عند المفتاح [SomeData-test@test.com].
http-80-Processor253 ذاكرة التخزين المؤقت.StaticCache - احصل على:مفتاح [SomeData-test@test.com] يُرجع القيمة [خالية]
http-80-Processor263 jsp.view-page - getSomeDataForEmail:على وشك الدخول إلى كتلة المزامنة
http-80-Processor263 jsp.view-page - getSomeDataForEmail:داخل كتلة المزامنة
http-80-Processor263 ذاكرة التخزين المؤقت.StaticCache - احصل على:انتهت صلاحية الكائن عند المفتاح [SomeData-test@test.com].
http-80-Processor263 ذاكرة التخزين المؤقت.StaticCache - احصل على:مفتاح [SomeData-test@test.com] يُرجع القيمة [خالية]
http-80-Processor131 jsp.view-page - getSomeDataForEmail:على وشك الدخول إلى كتلة المزامنة
http-80-Processor131 jsp.view-page - getSomeDataForEmail:داخل كتلة المزامنة
http-80-Processor131 ذاكرة التخزين المؤقت.StaticCache - احصل على:انتهت صلاحية الكائن عند المفتاح [SomeData-test@test.com].
http-80-Processor131 ذاكرة التخزين المؤقت.StaticCache - احصل على:مفتاح [SomeData-test@test.com] يُرجع القيمة [خالية]
http-80-Processor104 jsp.view-page - getSomeDataForEmail:داخل كتلة المزامنة
http-80-Processor104 ذاكرة التخزين المؤقت.StaticCache - احصل على:انتهت صلاحية الكائن عند المفتاح [SomeData-test@test.com].
http-80-Processor104 ذاكرة التخزين المؤقت.StaticCache - احصل على:مفتاح [SomeData-test@test.com] يُرجع القيمة [خالية]
http-80-Processor252 jsp.view-page - getSomeDataForEmail:على وشك الدخول إلى كتلة المزامنة
http-80-Processor283 jsp.view-page - getSomeDataForEmail:على وشك الدخول إلى كتلة المزامنة
http-80-Processor2 jsp.view-page - getSomeDataForEmail:على وشك الدخول إلى كتلة المزامنة
http-80-Processor2 jsp.view-page - getSomeDataForEmail:داخل كتلة المزامنة

كنت أرغب في رؤية مؤشر ترابط واحد فقط في كل مرة يدخل/يخرج من كتلة المزامنة حول عمليات الحصول على/تعيين.

هل هناك مشكلة في المزامنة على كائنات السلسلة؟اعتقدت أن مفتاح ذاكرة التخزين المؤقت سيكون خيارًا جيدًا لأنه فريد من نوعه للعملية، وعلى الرغم من أن final String key تم الإعلان عنه ضمن الطريقة، وكنت أفكر في أن كل موضوع سيحصل على مرجع إليه نفس الكائن وبالتالي فإن المزامنة على هذا الكائن الوحيد.

ما الخطأ الذي أفعله هنا؟

تحديث:بعد مزيد من البحث في السجلات، يبدو أنها طرق لها نفس منطق المزامنة حيث يكون المفتاح هو نفسه دائمًا، مثل

final String key = "blah";
...
synchronized(key) { ...

لا تظهر نفس مشكلة التزامن - يتم إدخال مؤشر ترابط واحد فقط في الكتلة في كل مرة.

تحديث 2:شكرا للجميع على المساعدة!قبلت الإجابة الأولى حول intern()ing Strings، والتي حلت مشكلتي الأولية - حيث كانت سلاسل رسائل متعددة تدخل إلى كتل متزامنة حيث اعتقدت أنه لا ينبغي لها ذلك، لأن keyكان لها نفس القيمة.

كما أشار آخرون، باستخدام intern() لمثل هذا الغرض والمزامنة على تلك السلاسل تبين بالفعل أنها فكرة سيئة - عند تشغيل اختبارات JMeter على تطبيق الويب لمحاكاة الحمل المتوقع، رأيت حجم الكومة المستخدمة ينمو إلى ما يقرب من 1 جيجابايت في أقل من 20 دقيقة.

أستخدم حاليًا الحل البسيط المتمثل في مزامنة الطريقة بأكملها - لكنني حقًا مثل نماذج التعليمات البرمجية المقدمة من martinprobst وMBCook، ولكن نظرًا لأن لدي حوالي 7 منها مماثلة getData() الأساليب الموجودة في هذا الفصل حاليًا (نظرًا لأنه يحتاج إلى حوالي 7 أجزاء مختلفة من البيانات من خدمة الويب)، لم أرغب في إضافة منطق مكرر تقريبًا حول الحصول على الأقفال وتحريرها لكل طريقة.ولكن هذه بالتأكيد معلومات قيمة للغاية للاستخدام المستقبلي.أعتقد أن هذه هي في النهاية الإجابات الصحيحة حول أفضل السبل لإجراء عملية مثل هذه السلسلة الآمنة، وسأعطي المزيد من الأصوات لهذه الإجابات إذا استطعت!

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

المحلول

دون أن أضع عقلي في حالة تأهب كامل، من الفحص السريع لما تقوله يبدو كما لو كنت بحاجة إلى تدريب () سلاسلك:

final String firstkey = "Data-" + email;
final String key = firstkey.intern();

سلسلتان لهما نفس القيمة ليسا بالضرورة نفس الكائن.

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

أفترض أنك تعلم أن StaticCache لا يزال بحاجة إلى أن يكون آمنًا.ولكن يجب أن يكون التنافس هناك صغيرًا مقارنة بما سيكون لديك إذا كنت تقوم بتأمين ذاكرة التخزين المؤقت بدلاً من المفتاح فقط أثناء الاتصال بـ getSomeDataForEmail.

الرد على سؤال التحديث:

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

يحرر

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

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

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

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

من الواضح أن هذا النهج يحتاج إلى اختبار قابلية التوسع قبل الاستخدام - وأنا لا أضمن أي شيء.

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

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

// do not attempt double-check locking here. I mean it.
synchronized(StaticObject) {
    data = StaticCache.get(key);
    while (data == IN_PROGRESS) {
        // another thread is getting the data
        StaticObject.wait();
        data = StaticCache.get(key);
    }
    if (data == null) {
        // we must get the data
        StaticCache.put(key, IN_PROGRESS, TIME_MAX_VALUE);
    }
}
if (data == null) {
    // we must get the data
    try {
        data = server.DoSlowThing(key);
    } finally {
        synchronized(StaticObject) {
            // WARNING: failure here is fatal, and must be allowed to terminate
            // the app or else waiters will be left forever. Choose a suitable
            // collection type in which replacing the value for a key is guaranteed.
            StaticCache.put(key, data, CURRENT_TIME);
            StaticObject.notifyAll();
        }
    }
}

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

يمكن استخدام هذا الرمز بشكل مشترك مع ذاكرات تخزين مؤقت متعددة، إذا قمت بتحديد التجريدات المناسبة لذاكرة التخزين المؤقت والقفل المرتبط بها، والبيانات التي تُرجعها، ونموذج IN_PROGRESS، والعملية البطيئة التي سيتم تنفيذها.قد لا يكون تحويل كل شيء إلى طريقة في ذاكرة التخزين المؤقت فكرة سيئة.

نصائح أخرى

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

في حالتك، يمكنك استخدام جدول تجزئة متزامن لتخزين كائنات القفل لمفاتيحك.

على سبيل المثال:

Object data = StaticCache.get(key, ...);
if (data == null) {
  Object lock = lockTable.get(key);
  if (lock == null) {
    // we're the only one looking for this
    lock = new Object();
    synchronized(lock) {
      lockTable.put(key, lock);
      // get stuff
      lockTable.remove(key);
    }
  } else {
    synchronized(lock) {
      // just to wait for the updater
    }
    data = StaticCache.get(key);
  }
} else {
  // use from cache
}

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

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

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

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

أيضًا:

  • آمل أن StaticCache.get() و تعيين() الأساليب آمنة للخيوط.
  • سلسلة.intern() يأتي بتكلفة (تكلفة تختلف بين تطبيقات VM) ويجب استخدامها بحذر.

واقترح آخرون غرس الخيوط، وهذا سوف ينجح.

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

لقد رأيت حلين لهذا:

يمكنك المزامنة على كائن آخر

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

في الواقع، يمكنك الاحتفاظ بخريطة ثانية لقفل الكائنات عليها.شيء من هذا القبيل:

Map<String, Object> emailLocks = new HashMap<String, Object>();

Object lock = null;

synchronized (emailLocks) {
    lock = emailLocks.get(emailAddress);

    if (lock == null) {
        lock = new Object();
        emailLocks.put(emailAddress, lock);
    }
}

synchronized (lock) {
    // See if this email is in the cache
    // If so, serve that
    // If not, generate the data

    // Since each of this person's threads synchronizes on this, they won't run
    // over eachother. Since this lock is only for this person, it won't effect
    // other people. The other synchronized block (on emailLocks) is small enough
    // it shouldn't cause a performance problem.
}

سيؤدي هذا إلى منع 15 عملية جلب على نفس عنوان البريد الإلكتروني مرة واحدة.ستحتاج إلى شيء ما لمنع وصول عدد كبير جدًا من الإدخالات إلى خريطة emailLocks.استخدام LRUMapسيفعل ذلك من Apache Commons.

سيحتاج هذا إلى بعض التغيير والتبديل، ولكنه قد يحل مشكلتك.

استخدم مفتاحًا مختلفًا

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

ملخص

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

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

 private ConcurrentMap<String, Future<SomeData[]> cache;
 private SomeData[] getSomeDataByEmail(final WebServiceInterface service, final String email) throws Exception {

  final String key = "Data-" + email;
  Callable<SomeData[]> call = new Callable<SomeData[]>() {
      public SomeData[] call() {
          return service.getSomeDataForEmail(email);
      }
  }
  FutureTask<SomeData[]> ft; ;
  Future<SomeData[]> f = cache.putIfAbsent(key, ft= new FutureTask<SomeData[]>(call)); //atomic
  if (f == null) { //this means that the cache had no mapping for the key
      f = ft;
      ft.run();
  }
  return f.get(); //wait on the result being available if it is being calculated in another thread
}

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

استخدم إطار تخزين مؤقت لائق مثل ehcache.

إن تنفيذ ذاكرة تخزين مؤقت جيدة ليس بالأمر السهل كما يعتقد البعض.

فيما يتعلق بالتعليق القائل بأن String.intern() هو مصدر لتسرب الذاكرة، فهذا غير صحيح في الواقع.السلاسل الداخلية نكون تم جمع القمامة، قد يستغرق الأمر وقتًا أطول لأنه في بعض JVM'S (SUN) يتم تخزينها في مساحة Perm والتي لا يتم لمسها إلا بواسطة GC الكاملة.

إليك حل Java 8 قصير وآمن يستخدم خريطة لكائنات القفل المخصصة للمزامنة:

private static final Map<String, Object> keyLocks = new ConcurrentHashMap<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

هناك عيب يتمثل في أن المفاتيح وكائنات القفل ستحتفظ بها في الخريطة إلى الأبد.

يمكن حل هذا على النحو التالي:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        try {
            SomeData[] data = StaticCache.get(key);
            if (data == null) {
                data = service.getSomeDataForEmail(email);
                StaticCache.set(key, data);
            }
        } finally {
            keyLocks.remove(key); // vulnerable to race-conditions
        }
    }
    return data;
}

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

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

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

private static final LoadingCache<String, Object> keyLocks = CacheBuilder.newBuilder()
        .expireAfterAccess(10, TimeUnit.MINUTES) // max lock time ever expected
        .build(CacheLoader.from(Object::new));

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.getUnchecked(key)) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

لاحظ أنه من المفترض هنا ذلك StaticCache آمن لمؤشر الترابط ولن يعاني من عمليات القراءة والكتابة المتزامنة لمفاتيح مختلفة.

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

الاتصال:

   final String key = "Data-" + email;

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

وهذا يوضح تعديلك بشكل أكبر.عندما يكون لديك سلسلة ثابتة، فسوف تعمل.

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

http://java.sun.com/j2se/1.4.2/docs/api/Java/lang/String.html#intern()

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

public class ValueLock<T> {

    private Lock lock = new ReentrantLock();
    private Map<T, Condition> conditions  = new HashMap<T, Condition>();

    public void lock(T t){
        lock.lock();
        try {
            while (conditions.containsKey(t)){
                conditions.get(t).awaitUninterruptibly();
            }
            conditions.put(t, lock.newCondition());
        } finally {
            lock.unlock();
        }
    }

    public void unlock(T t){
        lock.lock();
        try {
            Condition condition = conditions.get(t);
            if (condition == null)
                throw new IllegalStateException();// possibly an attempt to release what wasn't acquired
            conditions.remove(t);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

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

لا يستخدم الفصل الإصدار المتزامن من Map, لأن كل وصول إليه محمي بقفل واحد (داخلي).

يرجى ملاحظة، الدلالي lock() طريقة هذه الفئة مختلفة عن ReentrantLock.lock(), ، المتكررة lock() الدعوات دون المقترنة unlock() سوف يتعطل الموضوع الحالي إلى أجل غير مسمى.

تم وصف مثال على الاستخدام الذي قد يكون قابلاً للتطبيق على الموقف

    ValueLock<String> lock = new ValueLock<String>();
    // ... share the lock   
    String email = "...";
    try {
        lock.lock(email);
        //... 
    } finally {
        lock.unlock(email);
    }

هذا متأخر نوعًا ما، ولكن هناك الكثير من التعليمات البرمجية غير الصحيحة المقدمة هنا.

في هذا المثال:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {


  SomeData[] data = null;
  final String key = "Data-" + email;

  synchronized(key) {      
    data =(SomeData[]) StaticCache.get(key);

    if (data == null) {
        data = service.getSomeDataForEmail(email);
        StaticCache.set(key, data, CACHE_TIME);
    }
    else {
      logger.debug("getSomeDataForEmail: using cached object");
    }
  }

  return data;
}

تم تحديد نطاق المزامنة بشكل غير صحيح.بالنسبة لذاكرة التخزين المؤقت الثابتة التي تدعم واجهة برمجة تطبيقات get/put، يجب أن يكون هناك على الأقل مزامنة حول عمليات النوع get وgetIfAbsentPut، للوصول الآمن إلى ذاكرة التخزين المؤقت.سيكون نطاق المزامنة هو ذاكرة التخزين المؤقت نفسها.

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

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

استخدام العقود الآجلة أمر ممكن، ولكنه يبدو محرجًا إلى حد ما، وربما قليلاً من الهندسة المفرطة.تعد Future API جوهر العمليات غير المتزامنة، على وجه الخصوص، العمليات التي قد لا تكتمل على الفور.من المحتمل جدًا أن يضيف إشراك المستقبل طبقة من إنشاء سلاسل الرسائل - وربما تكون هناك تعقيدات غير ضرورية.

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

لماذا لا يتم عرض صفحة html ثابتة يتم تقديمها للمستخدم وإعادة إنشائها كل x دقيقة؟

أقترح أيضًا التخلص من تسلسل السلسلة تمامًا إذا لم تكن بحاجة إليها.

final String key = "Data-" + email;

هل هناك أشياء/أنواع كائنات أخرى في ذاكرة التخزين المؤقت تستخدم عنوان البريد الإلكتروني الذي تحتاجه "البيانات-" الإضافية في بداية المفتاح؟

إذا لم يكن الأمر كذلك، سأقوم بذلك

final String key = email;

وستتجنب إنشاء كل هذه السلسلة الإضافية أيضًا.

طريقة أخرى للمزامنة على كائن السلسلة:

String cacheKey = ...;

    Object obj = cache.get(cacheKey)

    if(obj==null){
    synchronized (Integer.valueOf(Math.abs(cacheKey.hashCode()) % 127)){
          obj = cache.get(cacheKey)
         if(obj==null){
             //some cal obtain obj value,and put into cache
        }
    }
}

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

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

public class KeySynchronizer<T> {

    private Map<T, CounterLock> locks = new ConcurrentHashMap<>();

    public <U> U synchronize(T key, Supplier<U> supplier) {
        CounterLock lock = locks.compute(key, (k, v) -> 
                v == null ? new CounterLock() : v.increment());
        synchronized (lock) {
            try {
                return supplier.get();
            } finally {
                if (lock.decrement() == 0) {
                    // Only removes if key still points to the same value,
                    // to avoid issue described below.
                    locks.remove(key, lock);
                }
            }
        }
    }

    private static final class CounterLock {

        private AtomicInteger remaining = new AtomicInteger(1);

        private CounterLock increment() {
            // Returning a new CounterLock object if remaining = 0 to ensure that
            // the lock is not removed in step 5 of the following execution sequence:
            // 1) Thread 1 obtains a new CounterLock object from locks.compute (after evaluating "v == null" to true)
            // 2) Thread 2 evaluates "v == null" to false in locks.compute
            // 3) Thread 1 calls lock.decrement() which sets remaining = 0
            // 4) Thread 2 calls v.increment() in locks.compute
            // 5) Thread 1 calls locks.remove(key, lock)
            return remaining.getAndIncrement() == 0 ? new CounterLock() : this;
        }

        private int decrement() {
            return remaining.decrementAndGet();
        }
    }
}

في حالة OP، سيتم استخدامه على النحو التالي:

private KeySynchronizer<String> keySynchronizer = new KeySynchronizer<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    String key = "Data-" + email;
    return keySynchronizer.synchronize(key, () -> {
        SomeData[] existing = (SomeData[]) StaticCache.get(key);
        if (existing == null) {
            SomeData[] data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data, CACHE_TIME);
            return data;
        }
        logger.debug("getSomeDataForEmail: using cached object");
        return existing;
    });
}

إذا لم يتم إرجاع أي شيء من التعليمات البرمجية المتزامنة، فيمكن كتابة طريقة المزامنة على النحو التالي:

public void synchronize(T key, Runnable runnable) {
    CounterLock lock = locks.compute(key, (k, v) -> 
            v == null ? new CounterLock() : v.increment());
    synchronized (lock) {
        try {
            runnable.run();
        } finally {
            if (lock.decrement() == 0) {
                // Only removes if key still points to the same value,
                // to avoid issue described below.
                locks.remove(key, lock);
            }
        }
    }
}

لقد أضفت فئة قفل صغيرة يمكنها قفل/مزامنة أي مفتاح، بما في ذلك السلاسل.

راجع تنفيذ Java 8 وJava 6 واختبار صغير.

جافا 8:

public class DynamicKeyLock<T> implements Lock
{
    private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<>();

    private final T key;

    public DynamicKeyLock(T lockKey)
    {
        this.key = lockKey;
    }

    private static class LockAndCounter
    {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        return locksMap.compute(key, (key, lockAndCounterInner) ->
        {
            if (lockAndCounterInner == null) {
                lockAndCounterInner = new LockAndCounter();
            }
            lockAndCounterInner.counter.incrementAndGet();
            return lockAndCounterInner;
        });
    }

    private void cleanupLock(LockAndCounter lockAndCounterOuter)
    {
        if (lockAndCounterOuter.counter.decrementAndGet() == 0)
        {
            locksMap.compute(key, (key, lockAndCounterInner) ->
            {
                if (lockAndCounterInner == null || lockAndCounterInner.counter.get() == 0) {
                    return null;
                }
                return lockAndCounterInner;
            });
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

جافا 6:

الطبقة العامة DynamicKeylock تنفذ قفل {private final static concurrenthashmap locksmap = جديد concurrenthashmap () ؛مفتاح T النهائي الخاص؛

    public DynamicKeyLock(T lockKey) {
        this.key = lockKey;
    }

    private static class LockAndCounter {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        while (true) // Try to init lock
        {
            LockAndCounter lockAndCounter = locksMap.get(key);

            if (lockAndCounter == null)
            {
                LockAndCounter newLock = new LockAndCounter();
                lockAndCounter = locksMap.putIfAbsent(key, newLock);

                if (lockAndCounter == null)
                {
                    lockAndCounter = newLock;
                }
            }

            lockAndCounter.counter.incrementAndGet();

            synchronized (lockAndCounter)
            {
                LockAndCounter lastLockAndCounter = locksMap.get(key);
                if (lockAndCounter == lastLockAndCounter)
                {
                    return lockAndCounter;
                }
                // else some other thread beat us to it, thus try again.
            }
        }
    }

    private void cleanupLock(LockAndCounter lockAndCounter)
    {
        if (lockAndCounter.counter.decrementAndGet() == 0)
        {
            synchronized (lockAndCounter)
            {
                if (lockAndCounter.counter.get() == 0)
                {
                    locksMap.remove(key);
                }
            }
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

امتحان:

public class DynamicKeyLockTest
{
    @Test
    public void testDifferentKeysDontLock() throws InterruptedException
    {
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(new Object());
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(new Object());
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertTrue(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }

    @Test
    public void testSameKeysLock() throws InterruptedException
    {
        Object key = new Object();
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(key);
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(key);
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertFalse(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }
}

في حالتك يمكنك استخدام شيء مثل هذا (وهذا لا يسرب أي ذاكرة):

private Synchronizer<String> synchronizer = new Synchronizer();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    String key = "Data-" + email;

    return synchronizer.synchronizeOn(key, () -> {

        SomeData[] data = (SomeData[]) StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data, CACHE_TIME);
        } else {
          logger.debug("getSomeDataForEmail: using cached object");
        }
        return data;

    });
}

لاستخدامه يمكنك فقط إضافة التبعية:

compile 'com.github.matejtymes:javafixes:1.3.0'

يمكنك استخدام String.intern للمزامنة بأمان إذا كان بإمكانك ضمان أن قيمة السلسلة فريدة عبر نظامك بشكل معقول.UUIDS هي طريقة جيدة للتعامل مع هذا الأمر.يمكنك ربط UUID بمفتاح السلسلة الفعلي الخاص بك، إما عبر ذاكرة تخزين مؤقت أو خريطة أو ربما حتى تخزين uuid كحقل في كائن الكيان الخاص بك.

    @Service   
    public class MySyncService{

      public Map<String, String> lockMap=new HashMap<String, String>();

      public void syncMethod(String email) {

        String lock = lockMap.get(email);
        if(lock==null) {
            lock = UUID.randomUUID().toString();
            lockMap.put(email, lock);
        }   

        synchronized(lock.intern()) {
                //do your sync code here
        }
    }
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top