Введение в синхронизацию в Java

Синхронизация - это функция Java, которая запрещает нескольким потокам одновременно обращаться к общедоступным ресурсам. Здесь общие ресурсы относятся к внешнему содержимому файла, переменным класса или записям базы данных.

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

Зачем нам нужна синхронизация в Java?

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

Что такое состояние гонки?

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

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

Например, давайте рассмотрим следующий код:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "+ Thread.currentThread().getName() + "Current Thread value " + this.getMyVar());
)
)
Class RaceCondition:
package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj, "thread 2");
Thread t3 = new Thread(mObj, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

При последовательном запуске приведенного выше кода выходные данные будут следующими:

Ourput1:

Текущий поток выполняется поток 1 Текущее значение потока 3

Текущий поток выполняется поток 3 Текущее значение потока 2

Текущий поток выполняется поток 2 Текущее значение потока 3

Выход2:

Текущий поток выполняется поток 3 Текущее значение потока 3

Текущий поток выполняется поток 2 Текущее значение потока 3

Текущий поток выполняется поток 1 Текущее значение потока 3

output3:

Текущий поток выполняется поток 2 Текущее значение потока 3

Текущий поток выполняется поток 1 Текущее значение потока 3

Текущий поток выполняется поток 3 Текущее значение потока 3

Output4:

Текущий поток выполняется поток 1 Текущее значение потока 2

Текущий поток выполняется поток 3 Текущее значение потока 3

Текущий поток выполняется поток 2 Текущее значение потока 2

  • Из приведенного выше примера вы можете сделать вывод, что потоки выполняются случайным образом, а также неверное значение. Согласно нашей логике, значение должно быть увеличено на 1. Однако здесь выходное значение в большинстве случаев равно 3, а в некоторых случаях - 2.
  • Здесь переменная myVar является общим ресурсом, на котором выполняются несколько потоков. Потоки одновременно обращаются к значению myVar и изменяют его. Давайте посмотрим, что произойдет, если мы закомментируем две другие темы.

Выход в этом случае:

Текущий поток выполняется поток 1 Текущее значение потока 1

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

Понимание того, что такое синхронизация в Java

  • Синхронизация в Java достигается с помощью ключевого слова «synchronized». Это ключевое слово может использоваться для методов, блоков или объектов, но не может использоваться с классами и переменными. Синхронизированный фрагмент кода позволяет только одному потоку получать доступ и изменять его в данный момент времени.
  • Однако синхронизированный фрагмент кода влияет на производительность кода, поскольку увеличивает время ожидания других потоков, пытающихся получить к нему доступ. Таким образом, фрагмент кода должен быть синхронизирован только тогда, когда есть вероятность возникновения состояния гонки. Если нет, то следует избегать этого.

Как Синхронизация в Java работает внутри?

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

Давайте синхронизируем наш предыдущий пример, синхронизируя код внутри метода run, используя синхронизированный блок в классе «Modify», как показано ниже:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)
)

Код для класса «RaceCondition» остается прежним. Теперь при запуске кода вывод выглядит следующим образом:

Output1:

Текущий поток выполняется поток 1 Текущее значение потока 1

Текущий поток выполняется поток 2 Текущее значение потока 2

Текущий поток выполняется поток 3 Текущее значение потока 3

Выход2:

Текущий поток выполняется поток 1 Текущее значение потока 1

Текущий поток выполняется поток 3 Текущее значение потока 2

Текущий поток выполняется поток 2 Текущее значение потока 3

Обратите внимание, что наш код обеспечивает ожидаемый результат. Здесь каждый поток увеличивает значение на 1 для переменной «myVar» (в классе «Modify»).

Примечание. Синхронизация требуется, когда несколько потоков работают на одном объекте. Если несколько потоков работают с несколькими объектами, синхронизация не требуется.

Например, давайте изменим код в классе «RaceCondition», как показано ниже, и поработаем с ранее несинхронизированным классом «Modify».

package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Modify mObj1 = new Modify();
Modify mObj2 = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj1, "thread 2");
Thread t3 = new Thread(mObj2, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Выход:

Текущий поток выполняется поток 1 Текущее значение потока 1

Текущий поток выполняется поток 2 Текущее значение потока 1

Текущий поток выполняется поток 3 Текущее значение потока 1

Типы синхронизации в Java:

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

1. взаимоисключающие

  • Синхронизированный метод.
  • Статический синхронизированный метод
  • Синхронизированный блок.

Координация 2.Thread (межпотоковое общение в Java)

Взаимоисключающий:

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

я. Синхронизированный метод: мы можем использовать ключевое слово «synchronized» для метода, таким образом делая его синхронизированным методом. Каждый поток, который вызывает синхронизированный метод, получит блокировку для этого объекта и освободит его, как только его операция будет завершена. В приведенном выше примере мы можем сделать наш метод run () синхронизированным с помощью ключевого слова synchronized после модификатора доступа.

@Override
public synchronized void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)

Выход для этого случая будет:

Текущий поток выполняется поток 1 Текущее значение потока 1

Текущий поток выполняется поток 3 Текущее значение потока 2

Текущий поток выполняется поток 2 Текущее значение потока 3

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

Например, давайте рассмотрим наш класс «Modify» и внесем в него изменения путем преобразования нашего метода «increment» в статический синхронизированный метод. Изменения кода, как показано ниже:

package JavaConcepts;
public class Modify implements Runnable(
private static int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public static synchronized void increment() (
myVar++;
System.out.println("Current thread being executed " + Thread.currentThread().getName() + " Current Thread value " + myVar);
)
@Override
public void run() (
// TODO Auto-generated method stub
increment();
)
)

III. Синхронизированный блок. Одним из основных недостатков синхронизированного метода является то, что он увеличивает время ожидания потоков, влияя на производительность кода. Следовательно, чтобы иметь возможность синхронизировать только необходимые строки кода вместо всего метода, необходимо использовать синхронизированный блок. Использование синхронизированного блока сокращает время ожидания потоков, а также повышает производительность. В предыдущем примере мы уже использовали синхронизированный блок при первой синхронизации нашего кода.

Пример:
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)

Координация нити:

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

  • Подождите()
  • поставить в известность()
  • notifyAll ()

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

wait (): поток при вызове метода wait () снимает блокировку объекта и переходит в состояние ожидания. У него есть две перегрузки метода:

  • public final void wait () выбрасывает InterruptedException
  • public final void wait (long timeout) выдает InterruptedException
  • публичное окончание void wait (long timeout, int nanos) выдает InterruptedException

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

  • публичное окончание void notify ()

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

  • публичный финал void notifyAll ()

Вывод

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

Рекомендуемые статьи:

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

  1. Сериализация в Java
  2. Что такое Generics в Java?
  3. Что такое API в Java?
  4. Что такое двоичное дерево в Java?
  5. Примеры и как работают дженерики в C #