Рубрики
Без рубрики

Ключевое слово Synchronized в Java

Автор оригинала: Chandan Singh.

Вступление

Это вторая статья из серии статей о параллелизме в Java. В предыдущей статье мы узнали о пуле Исполнителей и различных категориях Исполнителей на Java.

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

Что такое синхронизация?

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

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

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

Ключевое слово synchronized может использоваться несколькими различными способами, например синхронизированный блок :

synchronized (someObject) {
    // Thread-safe code here
}

Его также можно использовать с помощью такого метода, как этот:

public synchronized void somemMethod() {
    // Thread-safe code here
}

Как работает синхронизация в JVM

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

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

  • Для блока synchronized блокировка приобретается для объекта, указанного в круглых скобках после ключевого слова synchronized
  • Для синхронизированного статического метода блокировка приобретается для объекта .class
  • Для метода синхронизированного экземпляра блокировка приобретается для текущего экземпляра этого класса, т. е. этого экземпляра

Синхронизированные методы

Определить синхронизированные методы так же просто, как просто включить ключевое слово перед типом возвращаемого значения. Давайте определим метод, который последовательно выводит числа от 1 до 5.

Два потока попытаются получить доступ к этому методу, поэтому давайте сначала посмотрим, как это закончится без их синхронизации, а затем мы заблокируем общий объект и посмотрим, что произойдет:

public class NonSynchronizedMethod {

    public void printNumbers() {
        System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }

        System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
    }
}

Теперь давайте реализуем два пользовательских потока, которые обращаются к этому объекту и хотят запустить метод printNumbers() :

class ThreadOne extends Thread {

    NonSynchronizedMethod nonSynchronizedMethod;

    public ThreadOne(NonSynchronizedMethod nonSynchronizedMethod) {
        this.nonSynchronizedMethod = nonSynchronizedMethod;
    }

    @Override
    public void run() {
        nonSynchronizedMethod.printNumbers();
    }
}

class ThreadTwo extends Thread {

    NonSynchronizedMethod nonSynchronizedMethod;

    public ThreadTwo(NonSynchronizedMethod nonSynchronizedMethod) {
        this.nonSynchronizedMethod = nonSynchronizedMethod;
    }

    @Override
    public void run() {
        nonSynchronizedMethod.printNumbers();
    }
}

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

Чтобы проверить это поведение, давайте напишем основной класс:

public class TestSynchronization {
    public static void main(String[] args) {

        NonSynchronizedMethod nonSynchronizedMethod = new NonSynchronizedMethod();

        ThreadOne threadOne = new ThreadOne(nonSynchronizedMethod);
        threadOne.setName("ThreadOne");

        ThreadTwo threadTwo = new ThreadTwo(nonSynchronizedMethod);
        threadTwo.setName("ThreadTwo");

        threadOne.start();
        threadTwo.start();

    }
}

Запуск кода даст нам что-то вроде:

Starting to print Numbers for ThreadOne
Starting to print Numbers for ThreadTwo
ThreadTwo 0
ThreadTwo 1
ThreadTwo 2
ThreadTwo 3
ThreadTwo 4
Completed printing Numbers for ThreadTwo
ThreadOne 0
ThreadOne 1
ThreadOne 2
ThreadOne 3
ThreadOne 4
Completed printing Numbers for ThreadOne

Поток один начался первым, хотя Поток Два завершился первым.

И запуск его снова встречает нас еще одним нежелательным результатом:

Starting to print Numbers for ThreadOne
Starting to print Numbers for ThreadTwo
ThreadOne 0
ThreadTwo 0
ThreadOne 1
ThreadTwo 1
ThreadOne 2
ThreadTwo 2
ThreadOne 3
ThreadOne 4
ThreadTwo 3
Completed printing Numbers for ThreadOne
ThreadTwo 4
Completed printing Numbers for ThreadTwo

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

Теперь давайте адекватно синхронизируем наш метод:

Git Essentials

Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!

public synchronized void printNumbers() {
    System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());

    for (int i = 0; i < 5; i++) {
        System.out.println(Thread.currentThread().getName() + " " + i);
    }

    System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
}

Абсолютно ничего не изменилось, кроме включения ключевого слова synchronized . Теперь, когда мы запустим код:

Starting to print Numbers for ThreadOne
ThreadOne 0
ThreadOne 1
ThreadOne 2
ThreadOne 3
ThreadOne 4
Completed printing Numbers for ThreadOne
Starting to print Numbers for ThreadTwo
ThreadTwo 0
ThreadTwo 1
ThreadTwo 2
ThreadTwo 3
ThreadTwo 4
Completed printing Numbers for ThreadTwo

Это выглядит примерно так.

Здесь мы видим, что, хотя два потока выполняются одновременно, только один из потоков одновременно входит в синхронизированный метод, которым в данном случае является ThreadOne .

Как только он завершит выполнение, Поток Два может начинаться с выполнения метода printNumbers () .

Синхронизированные Блоки

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

Это снижает пропускную способность и производительность параллельного выполнения приложения. Этого недостатка нельзя полностью избежать из-за общих ресурсов.

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

Мы можем использовать блок synchronized для включения только этой части кода вместо всего метода.

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

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

public class SynchronizedBlockExample {

    public void printNumbers() {

        System.out.println("Starting to print Numbers for " + Thread.currentThread().getName());

        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        }

        System.out.println("Completed printing Numbers for " + Thread.currentThread().getName());
    }
}

Давайте сейчас проверим результат:

Starting to print Numbers for ThreadOne
Starting to print Numbers for ThreadTwo
ThreadOne 0
ThreadOne 1
ThreadOne 2
ThreadOne 3
ThreadOne 4
Completed printing Numbers for ThreadOne
ThreadTwo 0
ThreadTwo 1
ThreadTwo 2
ThreadTwo 3
ThreadTwo 4
Completed printing Numbers for ThreadTwo

Хотя может показаться тревожным, что ThreadTwo “начал” печатать номера до того, как ThreadOne выполнил свою задачу, это только потому, что мы позволили потоку пройти мимо System.out.println(Начало печати номеров для ThreadTwo) оператор перед остановкой ThreadTwo с блокировкой.

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

Вывод

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

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