Блокування синхронізованого методу Java на об'єкті чи методі?


191

Якщо у мене в одному класі є 2 синхронізованих методу, але кожен, хто має доступ до різних змінних, може 2 потоку отримати доступ до цих двох методів одночасно? Чи відбувається блокування на об'єкті, чи він стає таким же специфічним, як змінні всередині синхронізованого методу?

Приклад:

class X {

    private int a;
    private int b;

    public synchronized void addA(){
        a++;
    }

    public synchronized void addB(){
        b++;
    }

}

Чи можуть 2 потоки отримати доступ до того самого екземпляра, що виконує клас X x.addA() і x.addB()одночасно?

Відповіді:


199

Якщо ви оголосите метод синхронізованим (як ви робите, вводячи текст public synchronized void addA()), ви синхронізуєте весь об'єкт, тож два потоки, що звертаються до різної змінної цього ж об'єкта, заблокують один одного.

Якщо ви хочете синхронізувати лише одну змінну за один раз, тому два потоки не блокуватимуть один одного під час доступу до різних змінних, ви синхронізуєте їх окремо в synchronized ()блоках. Якщо aі bбули посилання на об'єкти, ви використовували б:

public void addA() {
    synchronized( a ) {
        a++;
    }
}

public void addB() {
    synchronized( b ) {
        b++;
    }
}

Але оскільки вони примітивні, ви цього не можете зробити.

Я б запропонував замість цього скористатися AtomicInteger :

import java.util.concurrent.atomic.AtomicInteger;

class X {

    AtomicInteger a;
    AtomicInteger b;

    public void addA(){
        a.incrementAndGet();
    }

    public void addB(){ 
        b.incrementAndGet();
    }
}

181
Якщо ви синхронізуєте метод, ви заблокуєте весь об'єкт, тож два потоки, що звертаються до різної змінної цього ж об’єкта, заблокували б один одного. Це трохи вводить в оману. Синхронізація методу функціонально еквівалентна наявності synchronized (this)блоку навколо тіла методу. Об'єкт "this" не стає заблокованим, скоріше об'єкт "this" використовується як mutex, і тілу не дозволяється виконувати одночасно з іншими розділами коду, також синхронізованими на "this". Це не впливає на інші поля / методи "цього", які не синхронізуються.
Марк Петерс

13
Так, це справді вводить в оману. Для реального прикладу - Подивіться на це - stackoverflow.com/questions/14447095/… - Резюме: Блокування відбувається лише на рівні синхронізованого методу, а до змінних екземплярів об'єкта можна отримати доступ до іншого потоку
mac

5
Перший приклад принципово порушений. Якщо aі bбули об'єкти, наприклад Integers, ви синхронізували на екземплярах, які ви замінюєте різними об'єктами при застосуванні ++оператора.
Холгер

виправте свою відповідь та ініціалізуйте AtomicInteger: AtomicInteger a = новий AtomicInteger (0);
Мехді

Може бути , це anwser повинна бути оновлена з пояснено в цій іншій про синхронізацію на самому об'єкті: stackoverflow.com/a/10324280/1099452
lucasvc

71

Синхронізований в оголошенні методу є синтаксичним цукром для цього:

 public void addA() {
     synchronized (this) {
          a++;
     }
  }

Для статичного методу це синтаксичний цукор для цього:

 ClassA {
     public static void addA() {
          synchronized(ClassA.class) {
              a++;
          }
 }

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


3
Неправда. синхронізований метод генерує інший байт-код, ніж синхронізований (об'єкт). Хоча функціональність еквівалентна, це більше, ніж просто синтаксичний цукор.
Стів Куо

10
Я не думаю, що "синтаксичний цукор" суворо визначається як еквівалент байт-коду. Справа в тому, що вона функціонально рівнозначна.
Yishai

1
Якби дизайнери Java знали те, що вже було відомо про монітори, вони мали б / мали би зробити це по-іншому, замість того, щоб наслідувати внутрішню частину Unix. Пер Брінч Хансен сказав: "Я чітко працював даремно", коли побачив примітивні паралелі Java .
Маркіз Лорн

Це правда. Приклад, наведений ОП, мабуть, блокує кожен метод, але насправді всі вони фіксуються на одному об'єкті. Дуже оманливий синтаксис. Після використання Java протягом 10+ років я цього не знав. Тому я б уникав синхронізованих методів з цієї причини. Я завжди думав, що для кожного методу, який визначався синхронізованим, створювався невидимий об’єкт.
Пітер Квірінг

21

З "Підручники Java ™" про синхронізовані методи :

По-перше, неможливо переплутати два виклики синхронізованих методів на одному об’єкті . Коли одна нитка виконує синхронізований метод для об'єкта, всі інші потоки, які викликають синхронізовані методи для одного блоку об'єктів (призупинення виконання), поки перший потік не буде виконано з об'єктом.

З «Підручники Java ™» про синхронізовані блоки :

Синхронізовані висловлювання також корисні для поліпшення паралельності з дрібнозернистою синхронізацією. Припустимо, наприклад, клас MsLunch має два екземплярні поля, c1 і c2, які ніколи не використовуються разом. Усі оновлення цих полів повинні бути синхронізовані, але немає жодних причин запобігати переплетенню оновлення c1 з оновленням c2 - і це зменшує одночасність, створюючи непотрібне блокування. Замість того, щоб використовувати синхронізовані методи або використовувати інший спосіб, пов'язаний з цим блокуванням, ми створюємо два об'єкти виключно для забезпечення блокування.

(Наголос мій)

Припустимо, у вас є дві непереміжні змінні. Отже, ви хочете отримати доступ до кожного з різних потоків одночасно. Вам потрібно визначити блокування не на самому об'єктному класі, а на класі Object, як показано нижче (приклад із другого посилання Oracle):

public class MsLunch {

    private long c1 = 0;
    private long c2 = 0;

    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}

14

Доступ до блокування знаходиться на об'єкті, а не на методі. До яких змінних можна отримати доступ до методу, не має значення.

Додавання методу "синхронізовано" означає, що нитка, що працює з кодом, повинна придбати замок на об'єкті, перш ніж продовжувати. Додавання "статичного синхронізованого" означає, що нитка, що працює з кодом, повинна придбати замок на об'єкті класу, перш ніж продовжувати. Можна також загорнути код у такий блок:

public void addA() {
    synchronized(this) {
        a++;
    }
}

щоб ви могли вказати об’єкт, замок якого потрібно придбати.

Якщо ви хочете уникнути блокування об'єкта, який міститься, ви можете вибрати:


7

З посилання на документацію Oracle

Синхронізація методів має два ефекти:

По-перше, неможливо переплутати два виклики синхронізованих методів на одному об’єкті. Коли одна нитка виконує синхронізований метод для об'єкта, всі інші потоки, які викликають синхронізовані методи для одного блоку об'єктів (призупинення виконання), поки перший потік не буде виконано з об'єктом.

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

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

Це дасть відповідь на ваше запитання: На одному об’єкті x ви не можете одночасно викликати x.addA () і x.addB (), коли виконується виконання одного з синхронізованих методів.


4

Якщо у вас є кілька методів, які не синхронізовані і отримують доступ і змінюють змінні екземпляра. У вашому прикладі:

 private int a;
 private int b;

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

 public void changeState() {
      a++;
      b++;
    }

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

У нижченаведеному сценарії:

class X {

        private int a;
        private int b;

        public synchronized void addA(){
            a++;
        }

        public synchronized void addB(){
            b++;
        }
     public void changeState() {
          a++;
          b++;
        }
    }

Тільки один з потоків може бути або в методі addA, або addB, але в той же час будь-яка кількість потоків може ввести метод changeState. Немає двох потоків можуть одночасно вводити addA та addB (через блокування рівня Об'єкта), але в той же час будь-яка кількість потоків може вводити changeState.


3

Ви можете зробити щось на зразок наступного. У цьому випадку ви використовуєте блокування на a і b для синхронізації замість блокування на "this". Ми не можемо використовувати int, оскільки у примітивних значень немає замків, тому ми використовуємо Integer.

class x{
   private Integer a;
   private Integer b;
   public void addA(){
      synchronized(a) {
         a++;
      }
   }
   public synchronized void addB(){
      synchronized(b) {
         b++;
      }
   }
}

3

Так, він заблокує інший метод, оскільки синхронізований метод застосовується до об'єкта класу WHOLE, як вказано .... але все одно він блокує виконання іншого потоку ТІЛЬКИ , виконуючи суму в будь-якому методі addA або addB, який він вводить, тому що коли він закінчиться ... одна нитка звільнить об’єкт, а інша отримає доступ до іншого методу і так далі ідеально працює.

Я маю на увазі, що "синхронізований" зроблений саме для блокування доступу іншого потоку до іншого під час виконання конкретного коду. ОКОНЧАТО ЦИЙ КОД ТРЕБУЄ ДЛЯ ТОЧНО.

На завершення, якщо є змінні 'a' і 'b', а не лише унікальна змінна 'a' або будь-яке інше ім'я, синхронізувати ці методи не потрібно, тому що це абсолютно безпечний доступ до інших var (Інша пам'ять Розташування).

class X {

private int a;
private int b;

public void addA(){
    a++;
}

public void addB(){
    b++;
}}

Також буде працювати


2

Цей приклад (хоча і не дуже) може дати більше розуміння механізму блокування. Якщо incrementA буде синхронізована , і incrementB це не синхронізовані , то incrementB буде виконуватися як можна швидше, але якщо incrementB також синхронізовані , то він повинен «чекати» incrementA до кінця, до того incrementB може робити свою роботу.

Обидва способи викликаються в одному екземплярі - об'єкті, в цьому прикладі це: job , а "конкуруючі" потоки - aThread та main .

Спробуйте зі " синхронізованим " у прирістB і без нього, і ви побачите різні результати. Якщо прирістB буде " синхронізований ", тоді йому доведеться чекати завершення збільшення ( A ). Виконайте кілька разів кожен варіант.

class LockTest implements Runnable {
    int a = 0;
    int b = 0;

    public synchronized void incrementA() {
        for (int i = 0; i < 100; i++) {
            this.a++;
            System.out.println("Thread: " + Thread.currentThread().getName() + "; a: " + this.a);
        }
    }

    // Try with 'synchronized' and without it and you will see different results
    // if incrementB is 'synchronized' as well then it has to wait for incrementA() to finish

    // public void incrementB() {
    public synchronized void incrementB() {
        this.b++;
        System.out.println("*************** incrementB ********************");
        System.out.println("Thread: " + Thread.currentThread().getName() + "; b: " + this.b);
        System.out.println("*************** incrementB ********************");
    }

    @Override
    public void run() {
        incrementA();
        System.out.println("************ incrementA completed *************");
    }
}

class LockTestMain {
    public static void main(String[] args) throws InterruptedException {
        LockTest job = new LockTest();
        Thread aThread = new Thread(job);
        aThread.setName("aThread");
        aThread.start();
        Thread.sleep(1);
        System.out.println("*************** 'main' calling metod: incrementB **********************");
        job.incrementB();
    }
}

1

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


0

Це може не працювати, оскільки бокс та автобоксинг від Integer до int та viceversa залежать від JVM, і існує велика ймовірність, що два різних числа можуть отримати хешировану адресу на одну адресу, якщо вони знаходяться між -128 та 127.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.