Треды существуют в мире технологий с 1967 года, за несколько десятилетий до того, как «Threads by Instagram» стали чем-то особенным.

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

Безопасность потоков — ключевое понятие в компьютерных науках, и оно существует почти столько же, сколько существуют потоки (опять же, оригинальные, а не Instagram). Безопасность потоков гарантирует, что данные, к которым обращаются несколько потоков, не будут повреждены из-за параллельного доступа. Это гарантирует, что ненормальные и непредсказуемые результаты не возникнут, когда два потока одновременно и неатомарно обновляют одни и те же данные.

Теперь вы можете спросить: «Подожди, что ты имеешь в виду под неатомарным

Вот краткое руководство по атомарным, атомарными неатомарнымоперациям в Java.

Примитивные типы данных Java, такие как int, long, boolean, char и т. д., являются атомарными для операций чтения и записи. По сути, это означает, что когда один поток читает или записывает экземпляр этих типов данных, эти операции выполняются за один шаг, который нельзя прервать.

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

Неатомарные относятся к операциям, которые невозможно выполнить за один непрерывный шаг. Например, увеличение целого числа — это неатомарная операция, поскольку она состоит из нескольких отдельных шагов:

  1. Чтение текущего значения целого числа.
  2. Добавление единицы к этому значению.
  3. Запись нового значения обратно в ячейку памяти целого числа.

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

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

Теперь вы можете задаться вопросом: «Зачем мне заботиться о том, какой поток читает и обновляет мое целое число?»».

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

Теперь, если вы похожи на меня, вам может быть интересно, как выглядит «поточно-безопасная» версия «неатомарной» операции, такой как приращение.

Что ж, вам повезло, потому что именно это следует за этой строкой :)

Поточно-безопасная реализация Integer в Java

В Java вы можете реализовать потокобезопасную версию целочисленного типа данных, используя ключевое слово synchronized или используя классы, предоставляемые пакетом java.util.concurrent.atomic, например AtomicInteger.

Вот простая реализация поточно-ориентированного целого числа и его приращения с использованиемsynchronized ключевого слова:

import java.util.concurrent.*;
public class ThreadSafeInteger {
  private int value;

  public synchronized int getValue() {
    return value;
  }

  public synchronized void setValue(int value) {
    this.value = value;
  }

  public synchronized void increment() {
    value++;
  }
}

Потокобезопасные и небезопасные для потоков приращения в действии

Ниже представлен основной класс , в котором мы можем увидеть приращения Thread-safe и Thread-unsafe в действии:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        final ThreadSafeInteger threadSafeInt = new ThreadSafeInteger();
        final int[] nonSafeInt = new int[1];

        class ThreadSafeIntIncrementer implements Runnable {
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    threadSafeInt.increment();
                }
            }
        }

        class NonSafeIntIncrementer implements Runnable {
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    nonSafeInt[0]++;
                }
            }
        }

        Thread[] threads = new Thread[1000];
        // Creating 1000 threads that increment the integer 10000 times each
        for (int i = 0; i < 500; i++) {
            threads[i * 2] = new Thread(new ThreadSafeIntIncrementer());
            threads[i * 2 + 1] = new Thread(new NonSafeIntIncrementer());
            threads[i * 2].start();
            threads[i * 2 + 1].start();
        }

        // Wait for all threads to finish
        for (int i = 0; i < 500; i++) {
            threads[i * 2].join();
            threads[i * 2 + 1].join();
        }

        System.out.println("Value of thread safe integer: " + threadSafeInt.getValue());
        System.out.println("Value of non-thread safe integer: " + nonSafeInt[0]);
    }
}

Класс Main запускает два набора по 500 потоков каждый. Один набор увеличивает ThreadSafeInteger в 10 000 раз, а другой набор увеличивает обычный int в 10 000 раз. ThreadSafeInteger использует ключевое слово «synchronized», чтобы избежать условий гонки, разрешая увеличение только одного потока за раз. Обычный int без синхронизации подвержен гонкам, когда одновременные приращения из нескольких потоков могут привести к неверным результатам. После завершения всех потоков (подтверждено методом join()) печатаются окончательные значения.

ThreadSafeInteger правильно показывает 5 000 000 (500 потоков * 10 000 приращений), но обычный int показывает меньше из-за условий гонки, из-за которых некоторые приращения пропускаются.

Ниже приведен вывод для того же самого:

Итак, что мы узнали?

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

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

Java решила эту проблему с помощью класса-оболочки под названием AtomicInteger, который позволяет атомарно обновлять целочисленное значение, что идеально подходит для счетчиков и экземпляров, используемых многими потоками. Если бы мы использовали AtomicInteger в контексте приведенного выше примера, threadSafeInt в классе Main был бы инициализирован как экземпляр класса AtomicInteger и threadSafe. increment() будет заменен на threadSafe.incrementAndGet().

Реальные приложения, такие как финансовые системы, где параллельные транзакции изменяют баланс общего счета, или системы бронирования авиабилетов, где одновременные бронирования могут привести к избыточному бронированию, являются яркими примерами, где безопасность потоков имеет решающее значение. Почти уверен, что никто из нас не хочет видеть необоснованное сообщение «Неудачная транзакция» на наших телефонах, когда мы на работе.

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

Об авторе

Я Сиддхарт Моханти, страстный разработчик Java и больших данных, проявляющий большой интерес к разработке программного обеспечения, DevOps и обработке данных. Я занимаюсь программированием и технологиями с 12 лет и люблю делиться своими мыслями и знаниями.

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