Въведение в синхронизацията в Java

Синхронизацията е функция на Java, която ограничава множеството нишки да се опитват да имат достъп до общо споделяните ресурси едновременно. Тук споделените ресурси се отнасят до съдържание на външни файлове, променливи на класа или записи в база данни.

Синхронизацията се използва широко в многонишковото програмиране. „Синхронизиран“ е ключовата дума, която предоставя на вашия код възможността да позволява само на една нишка да работи върху него без намеса от която и да е друга нишка през този период.

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

  • Java е многонишков програмен език. Това означава, че две или повече нишки могат да се движат едновременно към изпълнението на задача. Когато нишките се изпълняват едновременно, има големи шансове за възникване на сценарий, при който вашият код може да осигури неочаквани резултати.
  • Може би се чудите, че ако многопоточността може да доведе до погрешни изходи, тогава защо се счита за важна характеристика в Java?
  • Multithreading прави вашия код по-бърз, като работи няколко нишки паралелно и по този начин намалява времето за изпълнение на вашите кодове и осигурява висока производителност. Използването на многопоточната среда обаче води до неточни изходи поради състояние, обикновено известно като състезателно състояние.

Какво е състезателно състояние?

Когато две или повече нишки работят паралелно, те са склонни да осъществяват достъп и да променят споделени ресурси в този момент. Последователностите, в които се изпълняват нишките, се определят от алгоритъма за планиране на нишката.

Поради това не може да се предвиди редът, в който нишките ще бъдат изпълнени, тъй като се контролира единствено от планиращия нишка. Това се отразява на изхода на кода и води до непоследователни изходи. Тъй като множество нишки се състезават помежду си, за да завършат операцията, условието се нарича „състезателно условие“.

Например, нека разгледаме следния код:

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

Output2:

Текуща нишка се изпълнява нишка 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 се постига с помощта на ключовата дума „синхронизирана“. Тази ключова дума може да се използва за методи или блокове или обекти, но не може да се използва с класове и променливи. Синхронизираният фрагмент от кода позволява само на една нишка достъп и промяна в даден момент.
  • Синхронизираният фрагмент от код обаче влияе върху производителността на кода, тъй като увеличава времето на чакане на други нишки, които се опитват да получат достъп до него. Така че парче код трябва да се синхронизира само когато има шанс да възникне състояние на състезанието. Ако не някой трябва да го избягва.

Как синхронизирането в 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
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

Output2:

Текущата нишка, която се изпълнява нишка 1 Текуща стойност на нишката 1

Текущата нишка, която се изпълнява нишка 3 Стойност на текущата нишка 2

Текущата нишка, която се изпълнява нишка 2 Стойност на текущата нишка 3

Забележете, че кодът ни осигурява очакваната продукция. Тук всяка нишка увеличава стойността с 1 за променливата „myVar“ (в клас „Промяна“).

Забележка: Синхронизацията е необходима, когато на един обект работят няколко нишки. Ако множество нишки работят върху множество обекти, тогава не се изисква синхронизация.

Например, нека променим кода в клас „RaceCondition“, както е описано по-долу, и да работим с преди това несинхронизирания клас „Промени“.

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. Координация на нишката (комуникация между нишки в Java)

Взаимно изключващи се:

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

аз. Синхронизиран метод: Можем да използваме ключовата дума „синхронизиран“ за метод, като по този начин го правим синхронизиран метод. Всяка нишка, която извиква синхронизирания метод, ще получи заключването за този обект и ще го освободи, след като неговата операция приключи. В горния пример можем да направим нашия метод "run ()" като синхронизиран, като използваме ключовата дума "синхронизиран" след модификатора на достъп.

@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. Статичен синхронизиран метод: За да се синхронизират статичните методи, човек трябва да придобие своето заключване на ниво клас. След като нишката получи заключване на ниво клас само тогава, тя ще може да изпълни статичен метод. Докато нишката поддържа заключването на ниво клас, никоя друга нишка не може да изпълни друг статичен синхронизиран метод от този клас. Въпреки това, другите нишки могат да изпълняват всеки друг редовен метод или редовен статичен метод или дори нестатичен синхронизиран метод от този клас.

Например, нека разгледаме нашия клас „Промени“ и да направим промени в него, като преобразуваме нашия метод „нарастване“ в статичен синхронизиран метод. Промените в кода са както следва:

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 (), освобождава заключването на обекта и преминава в състояние на изчакване. Той има два метода претоварвания:

  • публичното окончателно невалидно изчакване () хвърля InterruptException
  • публичното окончателно невалидно изчакване (дълго време) изхвърля InterruptedException
  • публичното окончателно невалидно изчакване (дълго време, int nanos) хвърля InterruptedException

notify (): Нишка изпраща сигнал до друга нишка в състояние на изчакване, като използва метода notify (). Той изпраща известието само до една нишка, така че тази нишка да продължи въвеждането му. Коя нишка ще получи известието между всички нишки в състояние на чакане зависи от виртуалната машина на Java.

  • публично окончателно недействително известие ()

notifyAll (): Когато нишка се позовава на метода notifyAll (), всяка нишка в състояние на изчакване се известява. Тези нишки ще се изпълняват една след друга въз основа на реда, определен от виртуалната машина на Java.

  • публично окончателно невалидно уведомлениеAll ()

заключение

В тази статия видяхме как работата в многонишкова среда може да доведе до несъответствие на данни поради състезателно състояние. Как синхронизацията ни помага да преодолеем това, като ограничаваме една нишка да работи върху споделен ресурс в даден момент. Също така как синхронизираните нишки комуникират помежду си.

Препоръчани статии:

Това е ръководство за това какво е синхронизация в Java ?. Тук обсъждаме въвеждането, разбирането, нуждата, работата и видовете синхронизация с някакъв примерен код. Можете да разгледате и другите ни предложени статии, за да научите повече -

  1. Сериализация в Java
  2. Какво е Generics в Java?
  3. Какво е API в Java?
  4. Какво е бинарно дърво в Java?
  5. Примери и как генериците работят в C #