English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

java 中锁的性能提高办法

java 中锁的性能提高办法

We strive to think of solutions for the problems encountered by our products, but in this article, I will share several commonly used techniques with you, including lock separation, parallel data structures, protecting data instead of code, narrowing the scope of locks, which can make us not need any tools to detect deadlocks.

Locks are not the root of the problem, the competition between locks is

Generally, when encountering performance issues in multi-threaded code, people tend to complain about the problem of locks. After all, it is well-known that locks can slow down the execution speed of the program and have low scalability. Therefore, if you start optimizing the code with this 'common sense', the result is likely to be讨厌的并发问题 later on.

Therefore, it is very important to understand the difference between competitive locks and non-competitive locks. A lock contention occurs when a thread tries to enter a synchronized block or method that is being executed by another thread. The thread will be forced to enter a waiting state until the first thread completes the synchronized block and releases the monitor. When only one thread tries to execute the synchronized code area at the same time, the lock remains in a non-competitive state.

In fact, in non-competitive situations and most applications, JVM has optimized synchronization. Non-competitive locks do not incur any additional costs during execution. Therefore, you should not complain about locks due to performance issues, but rather complain about lock contention. After gaining this understanding, let's see what we can do to reduce the possibility of contention or reduce the duration of contention.

Protect data instead of code

One quick method to solve thread safety issues is to lock the accessibility of the entire method. For example, in the following example, it tries to establish an online poker game server using this method:

class GameServer {
 public Map<<String, List<Player>> tables = new HashMap<String, List<Player>>();
 public synchronized void join(Player player, Table table) {
  if (player.getAccountBalance() > table.getLimit()) {
   List<Player> tablePlayers = tables.get(table.getId());
   if (tablePlayers.size() < 9) {
    tablePlayers.add(player);
   }
  }
 }
 public synchronized void leave(Player player, Table table) {/*body skipped for brevity*/}
 public synchronized void createTable() {/*body skipped for brevity*/}
 public synchronized void destroyTable(Table table) {/*body skipped for brevity*/}
}

يهدف الكاتب إلى تحقيق نوايا جيدة - يجب التأكد من أن عدد اللاعبين على الطاولة لا يتجاوز إجمالي عدد اللاعبين الذين يمكن أن يتسع لهم الطاولة 9 عند انضمام لاعب جديد إلى الطاولة.

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

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

class GameServer {
 public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();
 public void join(Player player, Table table) {
  synchronized (tables) {
   if (player.getAccountBalance() > table.getLimit()) {
    List<Player> tablePlayers = tables.get(table.getId());
    if (tablePlayers.size() < 9) {
     tablePlayers.add(player);
    }
   }
  }
 }
 public void leave(Player player, Table table) {/* جسم الكود مكتوب بشكل مختصر */}
 public void createTable() {/* جسم الكود مكتوب بشكل مختصر */}
 public void destroyTable(Table table) {/* جسم الكود مكتوب بشكل مختصر */}
}

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

تقليل نطاق القفل

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

public class GameServer {
 public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();
 public void join(Player player, Table table) {
  if (player.getAccountBalance() > table.getLimit()) {
   synchronized (tables) {
    List<Player> tablePlayers = tables.get(table.getId());
    if (tablePlayers.size() < 9) {
     tablePlayers.add(player);
    }
   }
  }
 }
 //other methods skipped for brevity
}

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

قفل التفرع

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

للعثور على حل بسيط، يمكننا إدخال مفاتيح منفصلة لكل جدول اللعب، كما هو موضح في هذا المثال:}}

public class GameServer {
 public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();
 public void join(Player player, Table table) {
  if (player.getAccountBalance() > table.getLimit()) {
   List<Player> tablePlayers = tables.get(table.getId());
   synchronized (tablePlayers) {
    if (tablePlayers.size() < 9) {
     tablePlayers.add(player);
    }
   }
  }
 }
 //other methods skipped for brevity
}

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

استخدام بنية البيانات آمنة ضد التداخل المتسلسل

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

public class GameServer {
 public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>();
 public synchronized void join(Player player, Table table) {/*Method body skipped for brevity*/}
 public synchronized void leave(Player player, Table table) {/*Method body skipped for brevity*/}
 public synchronized void createTable() {
  Table table = new Table();
  tables.put(table.getId(), table);
 }
 public synchronized void destroyTable(Table table) {
  tables.remove(table.getId());
 }
}

فيما يزال الكتلة المتزامنة في methods join() و leave() مثل المثال السابق، لأننا نريد ضمان سلامة بيانات الطاولة الفردية. لا يقدم ConcurrentHashMap أي مساعدة في هذا الصدد. ولكننا سنستخدم ConcurrentHashMap لإنشاء وإزالة الطاولات الجديدة في methods increaseTable() و destroyTable()، وكل هذه العمليات متزامنة تمامًا لـ ConcurrentHashMap، مما يسمح لنا بإضافة أو إنشاء عدد الطاولات بشكل متوازي.

نصائح أخرى وتقنيات

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

لنلقي نظرة على API لـ java.util.concurrent.locks لمعرفة ما إذا كانت هناك استراتيجيات قفل مسبقة الإعداد أخرى يمكن استخدامها لتحسين الحل السابق.

استخدام العمليات الذرية. في المثال البسيط لعدد الإضافة الذي يستخدم حاليًا، لا يتطلب هذا العدد رفع العلامة الحمراء. يفضل استخدام AtomicInteger بدلاً من Integer كعدد.

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

شكرًا على القراءة، آمل أن تكون هذه المعلومات مفيدة لك، شكرًا لدعمك لموقعنا!