Очень хардкорные технические знания - идея CopyOnWrite

Java
Очень хардкорные технические знания - идея CopyOnWrite

«Сегодня я расскажу об очень фундаментальных технических знаниях и проанализирую, что такое идея CopyOnWrite и ее конкретное воплощение в параллельных пакетах Java, в том числе, как использовать эту идею в исходном коде ядра Kafka для оптимизации производительности параллелизма.

Во время собеседования этот CopyOnWrite, вероятно, станет козырем интервьюера, чтобы убить кандидата одним ударом, а также, вероятно, станет уникальным секретом для кандидата, чтобы выиграть предложение, которое является относительно продвинутым знанием.

1. Какие проблемы вызывает сценарий «больше читать и меньше писать»?

Вы можете себе представить, что сейчас в нашей памяти есть список ArrayList.Этот ArrayList по умолчанию определенно небезопасен для потоков.Если несколько потоков читают и пишут этот ArrayList одновременно, могут возникнуть проблемы.

Итак, вопрос в том, как сделать этот ArrayList потокобезопасным?

Существует очень простой способ добавить управление синхронизацией потоков к доступу к этому ArrayList.

Например, доступ к ArrayList должен осуществляться в сегменте синхронизированного кода, чтобы поток мог работать с ним одновременно, или он может контролироваться с помощью ReadWriteLock.

Мы предполагаем, что используем ReadWriteLock для управления доступом к этому ArrayList.

Таким образом, можно выполнять несколько запросов на чтение для чтения данных из ArrayList одновременно, но запрос на чтение и запрос на запись являются взаимоисключающими, а запрос на запись и запрос на запись также являются взаимоисключающими.

Давайте посмотрим, код, вероятно, похож на следующий:

public Object  read() {
   lock.readLock().lock();
   // 对ArrayList读取
   lock.readLock().unlock();
}

public void write() {
   lock.writeLock().lock();
   // 对ArrayList写
   lock.writeLock().unlock();
}

Подумайте об этом, что не так с кодом выше?

Самой большой проблемой на самом деле является взаимное исключение блокировок записи и чтения. Предполагая, что частота операций записи очень низкая, а частота операций чтения высока, это сценарий меньшего количества записи и большего количества чтения.

Тогда при эпизодическом выполнении операции записи не будет ли добавлена ​​блокировка записи, а большое количество операций чтения будет заблокировано и не может быть выполнено в это время?

Это самая большая проблема, с которой может столкнуться блокировка чтения-записи.

2. Внедрить идею CopyOnWrite для решения проблемы

В это время для решения проблемы следует внедрить идею CopyOnWrite.

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

Так как же он обеспечивает безопасность многопоточного параллелизма?

Очень просто, как следует из названия, с использованием метода «CopyOnWrite», этот перевод с английского на китайский, вероятно, ** «использовать копию копии для выполнения при записи данных». **

Когда вы читаете данные, не имеет значения, не блокируете ли вы их, все читают одни и те же данные, и это не влияет друг на друга.

Проблема в основном при записи.Поскольку вы не можете заблокировать при записи, вы должны принять стратегию.

Если нижний слой вашего ArrayList представляет собой массив для хранения данных вашего списка, то, например, если вы хотите изменить данные в этом массиве, вы должны сначала скопировать копию этого массива.

Затем вы можете записать данные, которые хотите изменить, в копию массива, но в процессе вы фактически работаете с копией.

В этом случае могут ли операции чтения нормально выполняться одновременно? Эта операция записи не влияет на операцию чтения!

Давайте посмотрим на картинку ниже, чтобы вместе испытать процесс:

Ключевой вопрос заключается в том, что пишущий поток изменил массив копирования, как теперь читающий поток может воспринять это изменение?

Ключевой момент здесь, сосредоточьтесь на нем! Здесь, чтобы сотрудничать с использованием ключевого слова volatile.

Автор уже писал статью ранее, объясняющую использование ключевого слова volatile.Смысл в том, чтобы позволить другим потокам читать последнее значение, на которое ссылается переменная, сразу после того, как переменная модифицируется записывающим потоком.Это основная роль volatile. .

Таким образом, как только поток записи завершит модификацию массива копирования, он может использовать метод записи volatile для присвоения массива копирования ссылочной переменной изменяемого массива volatile.

Пока он назначен изменяемой переменной volatile, он будет немедленно виден читающему потоку, и каждый сможет увидеть последний массив.

Ниже приведен исходный код CopyOnWriteArrayList в JDK.

Давайте посмотрим, как он копирует копию массива при записи данных, затем изменяет копию, а затем обновляет измененную копию массива, присваивая volatile переменную, делая ее немедленно видимой для других потоков.

// 这个数组是核心的,因为用volatile修饰了
  // 只要把最新的数组对他赋值,其他线程立马可以看到最新的数组
  private transient volatile Object[] array;

  public boolean add(E e) {

      final ReentrantLock lock = this.lock;
      lock.lock();

      try {
          Object[] elements = getArray();
          int len = elements.length;

          // 对数组拷贝一个副本出来
          Object[] newElements = Arrays.copyOf(elements, len + 1);

          // 对副本数组进行修改,比如在里面加入一个元素
          newElements[len] = e;

          // 然后把副本数组赋值给volatile修饰的变量
          setArray(newElements);
          return true;


      } finally {
          lock.unlock();
      }
  }

Тогда все подумали, ведь он обновляется через копию, а вдруг несколько потоков надо обновлять одновременно? Будет ли проблема сделать несколько копий?

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

Итак, при обновлении это как-то повлияет на операции чтения?

Абсолютно нет, потому что операция чтения — это очень простое чтение этого массива без каких-либо блокировок. И пока он обновляет присвоение изменяемой volatile переменной, читающий поток может сразу увидеть последний измененный массив, что гарантируется volatile.

Это прекрасно решает проблему больше читать и меньше писать, чем мы говорили ранее.

Если блокировка чтения-записи используется для взаимного исключения, это приведет к тому, что блокировка записи заблокирует большое количество операций чтения и повлияет на одновременную производительность.

Но если используется CopyOnWriteArrayList, он должен использовать пространство для времени, обновлять на основе копирования, избегать блокировок и, наконец, использовать volatile переменные для присвоения значений для обеспечения видимости, При обновлении это не влияет на поток чтения!

3. Применение идеи CopyOnWrite в исходном коде Kafka

В исходниках ядра Kafka есть такой сценарий: когда клиент записывает данные в Kafka, он сначала записывает сообщение в локальный буфер памяти клиента, а затем формирует пакет в буфере памяти и затем отправляет его в Kafka за один раз Сервер включен, что помогает повысить пропускную способность.

Без лишних слов давайте взглянем на картинку ниже:

Какую структуру данных использует буфер памяти Kafka в настоящее время? Посмотрите на исходный код:

private final ConcurrentMap<topicpartition, deque<="" span="">

         batches = new CopyOnWriteMap<TopicPartition, Deque>();

Эта структура данных является основной структурой данных, используемой для хранения сообщений, записанных в буфер памяти.Чтобы понять эту структуру данных, необходимо объяснить многие концепции исходного кода ядра Kafka, которые не будут здесь подробно раскрываться.

Но все обращают на это внимание.Он сам реализовал CopyOnWriteMap.Это CopyOnWriteMap перенимает идею CopyOnWrite.

Давайте посмотрим на реализацию исходного кода этого CopyOnWriteMap:

  // 典型的volatile修饰普通Map
  private volatile Mapmap;

  @Override
  public synchronized V put(K k, V v) {

      // 更新的时候先创建副本,更新副本,然后对volatile变量赋值写回去
      Mapcopy= new HashMap(this.map);
      V prev = copy.put(k, v);
      this.map = Collections.unmodifiableMap(copy);
      return prev;
  }

  @Override
  public V get(Object k) {

      // 读取的时候直接读volatile变量引用的map数据结构,无需锁
      return map.get(k);

  }

Поэтому основная структура данных Kafka реализована здесь по идее CopyOnWriteMap, потому что пара ключ-значение этой Map обновляется не так часто.

То есть пару клавишной пары TopicPartition-deque имеет очень низкую частоту обновления.

Однако его операция получения является высокочастотным запросом на чтение, потому что структура данных Deque, соответствующая TopicPartition, будет часто считываться для выполнения таких операций, как постановка в очередь и удаление из нее очереди, поэтому для этой карты high. Наиболее частым является ее получение. операция.

В настоящее время Kafka использует идею CopyOnWrite для реализации этой карты, избежания блокировки высокочастотных операций чтения при обновлении значения ключа, достижения эффекта блокировки без блокировки и оптимизации производительности параллелизма потоков.

Я полагаю, что после прочтения этой статьи у вас будет личный опыт использования идеи CopyOnWrite и применимых сценариев, включая реализацию в JDK и приложение в исходном коде Kafka.

Если вы сможете внятно объяснить эту идею и ее воплощение в JDK во время интервью, а затем объяснить ее интервьюеру в сочетании с лежащим в основе исходным кодом известного проекта с открытым кодом Kafka, то впечатление о вас у интервьюера определенно будет большим. плюс.

Оригинал из публичного аккаунта Shishan's Architecture Notes WeChat