Статьи

Исследование тупиков. Часть 5. Использование явной блокировки.

В моем последнем блоге я рассмотрел исправление моего сломанного, взаимоблокировочного примера кода переноса баланса с использованием традиционного для Java synchronized ключевого слова и порядка блокировки. Однако существует альтернативный метод, известный как явная блокировка.

Идея вызова механизма блокировки явным, а не неявным, заключается в том, что явное означает, что он не является частью языка Java и что классы были написаны для выполнения функций блокировки. С другой стороны, неявная блокировка может быть определена как блокировка, которая является частью языка и реализуется в фоновом режиме с использованием synchronchized ключевого слова языка.

Вы можете поспорить о том, является ли явная блокировка хорошей идеей. Не следует ли улучшить язык Java, включив в него функции явной блокировки, вместо добавления еще одного набора классов к и без того огромному API? Например: trysynchronized() .

Явная блокировка основана на интерфейсе Lock и его реализации ReentrantLock . Lock содержит множество методов, которые дают вам гораздо больший контроль над блокировкой, чем традиционное synchronized ключевое слово. У него есть методы, которые вы ожидаете получить, такие как lock() , которая создаст точку входа в защищенную часть кода, и unlock() , которая создаст точку выхода. Он также имеет tryLock() , которая будет получать блокировку только в том случае, если она доступна, но еще не получена другим потоком, и tryLock(long time,TimeUnit unit) , которая будет пытаться получить блокировку и, если она недоступна, ожидать указанного таймера. истечь, прежде чем сдаться.

Чтобы реализовать явную блокировку, я сначала добавил интерфейс Lock к классу Account использовался в предыдущих блогах этой серии .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class Account implements Lock {
 
  private final int number;
 
  private int balance;
 
  private final ReentrantLock lock;
 
  public Account(int number, int openingBalance) {
    this.number = number;
    this.balance = openingBalance;
    this.lock = new ReentrantLock();
  }
 
  public void withDrawAmount(int amount) throws OverdrawnException {
 
    if (amount > balance) {
      throw new OverdrawnException();
    }
 
    balance -= amount;
  }
 
  public void deposit(int amount) {
 
    balance += amount;
  }
 
  public int getNumber() {
    return number;
  }
 
  public int getBalance() {
    return balance;
  }
 
  // ------- Lock interface implementation
 
  @Override
  public void lock() {
    lock.lock();
  }
 
  @Override
  public void lockInterruptibly() throws InterruptedException {
    lock.lockInterruptibly();
  }
 
  @Override
  public Condition newCondition() {
    return lock.newCondition();
  }
 
  @Override
  public boolean tryLock() {
    return lock.tryLock();
  }
 
  @Override
  public boolean tryLock(long arg0, TimeUnit arg1) throws InterruptedException {
    return lock.tryLock(arg0, arg1);
  }
 
  @Override
  public void unlock() {
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }
 
}

В приведенном выше коде вы можете видеть, что я ReentrantLock агрегацию, инкапсулируя объект ReentrantLock которому класс Account делегирует функциональность блокировки. Единственная небольшая GOTCHA, о которой нужно знать, — это реализация unlock() :

1
2
3
4
5
6
@Override
  public void unlock() {
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }

Это имеет дополнительный оператор if() который проверяет, является ли вызывающий поток потоком, который в данный момент удерживает блокировку. Если эта строка кода пропущена, вы получите следующее IllegalMonitorStateException :

1
2
3
4
5
6
7
Exception in thread 'Thread-7' java.lang.IllegalMonitorStateException
 at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155)
 at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260)
 at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460)
 at threads.lock.Account.unlock(Account.java:76)
 at threads.lock.TrylockDemo$BadTransferOperation.transfer(TrylockDemo.java:98)
 at threads.lock.TrylockDemo$BadTransferOperation.run(TrylockDemo.java:67)

Итак, как это реализовано? Ниже приведен список моего образца TryLockDemo , основанного на моей оригинальной программе DeadLockDemo .

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
public class TrylockDemo {
 
  private static final int NUM_ACCOUNTS = 10;
  private static final int NUM_THREADS = 20;
  private static final int NUM_ITERATIONS = 100000;
  private static final int LOCK_ATTEMPTS = 10000;
 
  static final Random rnd = new Random();
 
  List<Account> accounts = new ArrayList<Account>();
 
  public static void main(String args[]) {
 
    TrylockDemo demo = new TrylockDemo();
    demo.setUp();
    demo.run();
  }
 
  void setUp() {
 
    for (int i = 0; i < NUM_ACCOUNTS; i++) {
      Account account = new Account(i, 1000);
      accounts.add(account);
    }
  }
 
  void run() {
 
    for (int i = 0; i < NUM_THREADS; i++) {
      new BadTransferOperation(i).start();
    }
  }
 
  class BadTransferOperation extends Thread {
 
    int threadNum;
 
    BadTransferOperation(int threadNum) {
      this.threadNum = threadNum;
    }
 
    @Override
    public void run() {
 
      int transactionCount = 0;
 
      for (int i = 0; i < NUM_ITERATIONS; i++) {
 
        Account toAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
        Account fromAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
        int amount = rnd.nextInt(1000);
 
        if (!toAccount.equals(fromAccount)) {
 
          boolean successfulTransfer = false;
 
          try {
            successfulTransfer = transfer(fromAccount, toAccount, amount);
 
          } catch (OverdrawnException e) {
            successfulTransfer = true;
          }
 
          if (successfulTransfer) {
            transactionCount++;
          }
 
        }
      }
 
      System.out.println("Thread Complete: " + threadNum + " Successfully made " + transactionCount + " out of "
          + NUM_ITERATIONS);
    }
 
    private boolean transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {
 
      boolean success = false;
      for (int i = 0; i < LOCK_ATTEMPTS; i++) {
 
        try {
          if (fromAccount.tryLock()) {
            try {
              if (toAccount.tryLock()) {
 
                success = true;
                fromAccount.withDrawAmount(transferAmount);
                toAccount.deposit(transferAmount);
                break;
              }
            } finally {
              toAccount.unlock();
            }
          }
        } finally {
          fromAccount.unlock();
        }
      }
 
      return success;
    }
 
  }
}

Идея та же, у меня есть список банковских счетов, и я собираюсь случайным образом выбрать два счета и перенести случайную сумму с одного на другой. Суть в том, что мой обновленный метод transfer(...) показан ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private boolean transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {
 
     boolean success = false;
     for (int i = 0; i < LOCK_ATTEMPTS; i++) {
 
       try {
         if (fromAccount.tryLock()) {
           try {
             if (toAccount.tryLock()) {
 
               success = true;
               fromAccount.withDrawAmount(transferAmount);
               toAccount.deposit(transferAmount);
               break;
             }
           } finally {
             toAccount.unlock();
           }
         }
       } finally {
         fromAccount.unlock();
       }
     }
 
     return success;
   }

Идея в том, что я пытаюсь заблокировать fromAccount а затем toAccount . Если это работает, то я делаю перевод, прежде чем забыть разблокировать обе учетные записи. Если тогда учетные записи уже заблокированы, то мой tryLock() завершается ошибкой, и все tryLock() и пытается снова. После 10000 попыток блокировки поток сдается и игнорирует передачу. Я предполагаю, что в реальном приложении вы захотите поместить эту ошибку в какую-то очередь, чтобы ее можно было изучить позже.

Используя явную блокировку, вы должны учитывать, насколько хорошо она работает, поэтому взгляните на результаты ниже…

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
Thread Complete: 17 Successfully made 58142 out of 100000
Thread Complete: 12 Successfully made 57627 out of 100000
Thread Complete: 9 Successfully made 57901 out of 100000
Thread Complete: 16 Successfully made 56754 out of 100000
Thread Complete: 3 Successfully made 56914 out of 100000
Thread Complete: 14 Successfully made 57048 out of 100000
Thread Complete: 8 Successfully made 56817 out of 100000
Thread Complete: 4 Successfully made 57134 out of 100000
Thread Complete: 15 Successfully made 56636 out of 100000
Thread Complete: 19 Successfully made 56399 out of 100000
Thread Complete: 2 Successfully made 56603 out of 100000
Thread Complete: 13 Successfully made 56889 out of 100000
Thread Complete: 0 Successfully made 56904 out of 100000
Thread Complete: 5 Successfully made 57119 out of 100000
Thread Complete: 7 Successfully made 56776 out of 100000
Thread Complete: 6 Successfully made 57076 out of 100000
Thread Complete: 10 Successfully made 56871 out of 100000
Thread Complete: 11 Successfully made 56863 out of 100000
Thread Complete: 18 Successfully made 56916 out of 100000
Thread Complete: 1 Successfully made 57304 out of 100000

Это показывает, что, хотя программа не блокировалась и зависала бесконечно, ей удалось выполнить перевод баланса только чуть более чем в половине запросов на передачу. Это означает, что он сжигает много циклов обработки мощности, циклов и циклов — что в целом не очень эффективно. Кроме того, я недавно сказал, что программа «не зависала и не зависала бесконечно», что не совсем верно. Если вы подумаете о том, что происходит, вы поймете, что ваша программа блокируется, а затем выйдете из этой ситуации.

Вторая версия моего демонстрационного кода с явной блокировкой использует tryLock(long time,TimeUnit unit) .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private boolean transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {
 
      boolean success = false;
 
      try {
        if (fromAccount.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) {
          try {
            if (toAccount.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) {
 
              success = true;
              fromAccount.withDrawAmount(transferAmount);
              toAccount.deposit(transferAmount);
            }
          } finally {
            toAccount.unlock();
          }
        }
      } catch (InterruptedException e) {
        e.printStackTrace();
      } finally {
        fromAccount.unlock();
      }
 
      return success;
    }

В этом коде я заменил цикл tryLock(...) тайм-аутом tryLock(...) 1 миллисекунде. Это означает, что когда tryLock(...) и он не может получить блокировку, он будет ждать 1 мс, прежде чем откатиться и сдаться.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
Thread Complete: 0 Successfully made 26637 out of 100000
Thread Complete: 14 Successfully made 26516 out of 100000
Thread Complete: 3 Successfully made 26552 out of 100000
Thread Complete: 11 Successfully made 26653 out of 100000
Thread Complete: 7 Successfully made 26399 out of 100000
Thread Complete: 1 Successfully made 26602 out of 100000
Thread Complete: 18 Successfully made 26606 out of 100000
Thread Complete: 17 Successfully made 26358 out of 100000
Thread Complete: 19 Successfully made 26407 out of 100000
Thread Complete: 16 Successfully made 26312 out of 100000
Thread Complete: 15 Successfully made 26449 out of 100000
Thread Complete: 5 Successfully made 26388 out of 100000
Thread Complete: 8 Successfully made 26613 out of 100000
Thread Complete: 2 Successfully made 26504 out of 100000
Thread Complete: 6 Successfully made 26420 out of 100000
Thread Complete: 4 Successfully made 26452 out of 100000
Thread Complete: 9 Successfully made 26287 out of 100000
Thread Complete: 12 Successfully made 26507 out of 100000
Thread Complete: 10 Successfully made 26660 out of 100000
Thread Complete: 13 Successfully made 26523 out of 100000

Приведенный выше результат показывает, что при использовании таймера процент успешных операций переноса баланса снижается до чуть более 25%. Хотя это не сжигает стек процессорного времени, это все еще очень неэффективно.

Я мог довольно долго возиться с обоими этими примерами кода, выбирая переменные, которые настраивают приложение и улучшают производительность, но, в конце концов, ничто не заменит правильное упорядочение блокировки. Я лично предпочел бы использовать старомодную synchronized ключевым словам там, где это возможно, и резервировать явную блокировку для тех немногих ситуаций, когда код взаимоблокировки устарел, груб, не поддается расшифровке, я пробовал все остальное, приложение должно быть запущено уже поздно и пора домой…

Для получения дополнительной информации см. Другие блоги в этой серии .

Весь исходный код для этого и других блогов из этой серии доступен на Github по адресу: gitub.com/roghughe/captaindebug.git.

Ссылка: Исследование тупиков — Часть 5. Использование явной блокировки от нашего партнера по JCG Роджера Хьюза в блоге Captain Debug’s Blog .