Синхронизация Java - часть 1

Перед тем, как перейти к синхронизации, я расскажу о нескольких потоках с помощью простого кода.

Первым классом будет класс «Обратный отсчет», а класс «ThreadColor» с цветовым трюком будет выглядеть так.

public class ThreadColor {
    public static final 
    public static final String ANSI_RED = "\u001B[31m";
    public static final String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_YELLOW = "\u001b[33m";
    public static final String ANSI_BLUE = "\u001B[34m";
    public static final String ANSI_PURPLE = "\u001B[35m";
    public static final String ANSI_CYAN = "\u001B[36m";
    public static final String ANSI_WHITE = "\u001b[37m";
}

Здесь… я создал второй класс, расширяющий поток.

Перейдите к основному методу, поскольку мы создали 2 класса, а затем запустили два потока.
Из оператора switch (изображение 1), который мы получили от одного потока с именем «Поток 1», который будет печатать текст голубым цветом, а второй поток будет печатать текст фиолетовым цветом.

Вот… теперь посмотрим, что происходит.

Здесь вы можете видеть, что поток 1 - CYAN, а поток 2 - PURPLE. Но мы не можем предсказать, каким будет результат, т.е. порядок двух цветов. Вы можете видеть, что когда мы запускаем код несколько раз, он дает нам совершенно другие журналы.

Теперь мы собираемся добавить переменную экземпляра «Private int I;», которая изменяет локальную переменную «I» на переменную экземпляра. Посмотрим на новый результат.

Теперь мы можем заметить, что на этот раз результат другой. Вместо od = f в каждом потоке, считая до 10, числа кажутся как бы дублированными.

Почему ??
Как видите, цифра 10 дублируется. Не только 10, вы можете видеть, что несколько других чисел тоже были продублированы. Единственное, что мы сделали, это изменили локальную переменную на переменную экземпляра следующим образом:

class Countdown {
    private int i;

Куча: это память приложения, которая используется всеми потоками, и каждый поток имеет стек потоков. Это память, которая уникальна для каждого потока. Проще говоря, ни один поток не может получить доступ к стеку другого потока. Но потоки могут получить доступ к куче. Локальная переменная хранится в стеке потоков, то есть каждый поток имеет свою собственную копию локальной переменной. В то время как переменные экземпляра хранятся в куче. Поэтому, когда несколько потоков работают вместе с одним и тем же объектом, они, вероятно, используют один и тот же объект. В этом случае, если один поток изменяет значение, другой поток примет новое значение, которое было изменено первым потоком. Точно так же, когда «i» действовала как локальная переменная, потоки имели свою собственную версию «i». После того как мы изменили «i» в качестве переменной экземпляра, 2 потока должны были получить доступ к этому общему ресурсу, который хранится в куче. Вот почему кажется, что в каждой цепочке пропущены какие-то числа.

for statement
Он уменьшает I и проверяет выполнение условия i ›0; идея, стоящая за оператором for, состоит в том, что он состоит из нескольких шагов, таких как уменьшение, проверка и т. д. Таким образом, поток может быть приостановлен между шагами. Его можно приостановить после уменьшения «i», перед проверкой условия или сразу после выполнения всех кодов и затем печати. Уменьшение «i», проверка условия и распечатка значений: эти 3 шага могут приостановить текущий поток.

Как вы понимаете, в первой попытке оба потока считали значение «i» равным 10, поэтому поток 1 печатает 9, а поток 2 - 8. Пока поток 1 выполнял оператор for, поток 2 должен был быть без выхода за пределы потока 1. Поэтому он принимает значение «i» как 9, выполняет блок for и печатает 8. Я думаю, что это объяснение дает вам представление об этом.

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

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

public class Main {

    public static void main(String[] args) {
        Countdown countdown1 = new Countdown();
        Countdown countdown2 = new Countdown();

        CountdownThread t1 = new CountdownThread(countdown1);
        t1.setName("Thread 1");
        CountdownThread t2 = new CountdownThread(countdown2);
        t2.setName("Thread 2");

        t1.start();
        t2.start();
    }
}

Взгляните на приведенный выше пример, где есть два новых объекта для потоков, не использующих кучу. Проверьте результаты, никаких помех нет! Каждый поток успешно отсчитывает от 10 до 1.

Настоящий вопрос в том, будет ли это применимо в реальном мире? Будет ли это работать для банковского счета, когда кто-то кладет деньги на ваш счет, пока вы снимаете деньги в банкомате? Следовательно, похоже, что мы должны использовать один и тот же объект, чтобы поддерживать целостность данных. Потому что это единственный способ узнать точный баланс банка после выполнения нескольких потоков (транзакций), верно? Может быть несколько потоков, которые одновременно ждут изменения банковского баланса. Поэтому нам нужно разрешить нескольким потокам изменять его, не допуская возникновения «состояния гонки».

Синхронизация

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

class Countdown {
    private int i;
    public synchronized void doCountdown() {
        String color;   
}

И проверьте результаты.

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

Таким образом, в этом сценарии эти 2 потока никогда не столкнутся!

Но разве это единственный способ остановить состояние гонки?

Что ж, мы можем добавить ключевое слово synchronized в блок оператора, чем добавлять его ко всему методу.

Каждый объект Java имеет внутреннюю блокировку (также известную как блокировка монитора), когда синхронизированный метод вызывается из потока, который ему необходим для получения внутренней блокировки. блокировка будет снята, когда поток завершит выполнение метода. . Таким образом, мы можем синхронизировать блок операторов, который работает с объектом, заставляя потоки получать внутреннюю блокировку до того, как они выполнят блок операторов. Помните, что только один поток может удерживать блокировку одновременно, поэтому другие потоки, которые хотят получить блокировку, будут приостановлены, пока текущий поток не закончит с ней. Только после этого объект, который ожидал, мог получить блокировку и продолжить выполнение.

Единственный блок кода, который мы можем добавить ключевое слово synchronized в метод doCountdown, - это блок for loop. Итак, какой объект мы должны использовать для синхронизации цикла for? Переменная «i»? Я так не думаю, потому что это примитивный тип, а не объект. Только объекты имеют внутреннюю блокировку. А как насчет этого «цветного» объекта? позволяет просто удалить синхронизацию из объявления метода и добавить такую ​​синхронизацию.

synchronized (color){
    for(i=10; i > 0; i--) {
        System.out.println(color + Thread.currentThread().getName() + ": i =" + i);
    }
}

Видите результаты? такие же, как результаты (изображение 5) в коде, который вообще не использовал синхронизацию? это почему? мы используем локальную переменную «цвет» для синхронизации. Как я объяснил выше о стеке потоков и обо всем, что использует локальную переменную, здесь вообще не работает, но объекты String повторно используются в jvm, jvm использует пулы строк для выделения строковых объектов. Да, иногда это тоже может быть рабочим решением.
Как правило, помните, что для синхронизации нельзя использовать локальные переменные.

Итак, давайте просто обновим синхронизированный блок кода следующим образом:

synchronized (this){
    for(i=10; i > 0; i--) {
        System.out.println(color + Thread.currentThread().getName() + ": i =" + i);
    }
}

И посмотрите результаты. вы можете видеть, что потоки не будут конфликтовать и пропускать числа. просто отлично работает. Только один поток может запускать блок цикла for одновременно.

Также мы можем синхронизировать статические методы и использовать статические объекты.