Посібник із синхронізованого ключового слова на Java

1. Огляд

Ця коротка стаття буде вступом до використання синхронізованого блоку в Java.

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

Частина логіки, позначена синхронізованим, стає синхронізованим блоком, що дозволяє виконувати лише один потік у будь-який момент часу .

2. Чому синхронізація?

Давайте розглянемо типовий стан перегонів, коли ми обчислюємо суму, а декілька потоків виконують метод Calculate () :

public class BaeldungSynchronizedMethods { private int sum = 0; public void calculate() { setSum(getSum() + 1); } // standard setters and getters } 

І давайте напишемо простий тест:

@Test public void givenMultiThread_whenNonSyncMethod() { ExecutorService service = Executors.newFixedThreadPool(3); BaeldungSynchronizedMethods summation = new BaeldungSynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> service.submit(summation::calculate)); service.awaitTermination(1000, TimeUnit.MILLISECONDS); assertEquals(1000, summation.getSum()); }

Ми просто використовуємо ExecutorService з 3-потоковим пулом, щоб виконувати обчислення () 1000 разів.

Якби ми виконували це послідовно, очікуваний результат був би 1000, але наше багатопотокове виконання майже щоразу виходило з ладу з невідповідним фактичним результатом, наприклад:

java.lang.AssertionError: expected: but was: at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) ...

Цей результат, звичайно, не є несподіваним.

Простий спосіб уникнути перегонового стану - зробити операцію безпечною для потоку за допомогою синхронізованого ключового слова.

3. Синхронізоване ключове слово

Синхронізується ключове слово можна використовувати на різних рівнях:

  • Методи екземпляра
  • Статичні методи
  • Блоки коду

Коли ми використовуємо синхронізований блок, для забезпечення синхронізації Java використовує монітор, також відомий як блокування монітора або внутрішній замок. Ці монітори прив'язані до об'єкта, отже всі синхронізовані блоки одного і того ж об'єкта можуть мати лише один потік, який виконує їх одночасно.

3.1. Синхронізовані методи екземпляра

Просто додайте синхронізоване ключове слово в декларацію методу, щоб зробити метод синхронізованим:

public synchronized void synchronisedCalculate() { setSum(getSum() + 1); }

Зверніть увагу, що як тільки ми синхронізуємо метод, тестовий приклад проходить з фактичним виведенням 1000:

@Test public void givenMultiThread_whenMethodSync() { ExecutorService service = Executors.newFixedThreadPool(3); SynchronizedMethods method = new SynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> service.submit(method::synchronisedCalculate)); service.awaitTermination(1000, TimeUnit.MILLISECONDS); assertEquals(1000, method.getSum()); }

Методи екземпляра синхронізуються над екземпляром класу, що володіє методом. Це означає, що лише один потік на примірник класу може виконувати цей метод.

3.2. Синхронна Стати з Methods

Статичні методи синхронізуються так само, як методи екземпляра:

 public static synchronized void syncStaticCalculate() { staticSum = staticSum + 1; }

Ці методи синхронізуються з об’єктом Class, пов’язаним з класом, і оскільки в JVM на клас існує лише один об’єкт Class , лише один потік може виконуватися всередині статичного синхронізованого методу на клас, незалежно від кількості екземплярів, які він має.

Давайте перевіримо:

@Test public void givenMultiThread_whenStaticSyncMethod() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedMethods::syncStaticCalculate)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedMethods.staticSum); }

3.3. Синхронізовані блоки в межах методів

Іноді ми не хочемо синхронізувати весь метод, а лише деякі інструкції в ньому. Цього можна досягти, застосувавши синхронізовані до блоку:

public void performSynchronisedTask() { synchronized (this) { setCount(getCount()+1); } }

Давайте перевіримо зміни:

@Test public void givenMultiThread_whenBlockSync() { ExecutorService service = Executors.newFixedThreadPool(3); BaeldungSynchronizedBlocks synchronizedBlocks = new BaeldungSynchronizedBlocks(); IntStream.range(0, 1000) .forEach(count -> service.submit(synchronizedBlocks::performSynchronisedTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, synchronizedBlocks.getCount()); }

Зверніть увагу , що ми передали параметр це до синхронного блоку. Це об'єкт монітора, код всередині блоку синхронізується на об'єкті монітора. Простіше кажучи, лише один потік на об'єкт монітора може виконуватися всередині цього блоку коду.

Якщо метод є статичним , ми передаємо ім'я класу замість посилання на об'єкт. І клас буде монітором для синхронізації блоку:

public static void performStaticSyncTask(){ synchronized (SynchronisedBlocks.class) { setStaticCount(getStaticCount() + 1); } }

Давайте протестуємо блок всередині статичного методу:

@Test public void givenMultiThread_whenStaticSyncBlock() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedBlocks::performStaticSyncTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedBlocks.getStaticCount()); }

3.4. Повернення

Блокування за синхронізованими методами та блоками повертається. Тобто поточний потік може знову і знову отримувати один і той же синхронізований замок, утримуючи його:

Object lock = new Object(); synchronized (lock) { System.out.println("First time acquiring it"); synchronized (lock) { System.out.println("Entering again"); synchronized (lock) { System.out.println("And again"); } } }

Як показано вище, поки ми перебуваємо в синхронізованому блоці, ми можемо отримувати один і той же замок монітора неодноразово.

4. Висновок

У цій короткій статті ми побачили різні способи використання синхронізованого ключового слова для досягнення синхронізації потоків.

Ми також дослідили, як умова перегони може вплинути на наш додаток, і як синхронізація допомагає нам цього уникнути. Докладніше про безпеку потоків із використанням блокування в Java див. У нашій статті про java.util.concurrent.Locks .

Повний код цього підручника доступний на GitHub.