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» используется как мьютекс, и тело не может выполняться одновременно с другими разделами кода, также синхронизированными с «this». Это не влияет на другие поля / методы «this», которые не синхронизированы.
Марк Питерс

13
Да, это действительно вводит в заблуждение. Для реального примера - посмотрите на это - stackoverflow.com/questions/14447095/… - Сводка: блокировка осуществляется только на уровне синхронизированного метода, а переменные экземпляра объекта могут быть доступны из другого потока
mac

5
Первый пример принципиально сломан. Если aи bбыли объектами, например, Integers, вы выполняли синхронизацию на экземплярах, которые вы заменяли другими объектами при применении ++оператора.
Хольгер

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

Может быть, этот ответ должен быть обновлен с объяснением в этом другом о синхронизации на самом объекте: 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
Я не думаю, что «синтаксический сахар» строго определяется как эквивалент байт-кода. Дело в том, что это функционально эквивалентно.
Ишай

1
Если бы Java-дизайнеры знали то, что уже было известно о мониторах, они бы сделали / должны были сделать это иначе, вместо того, чтобы просто эмулировать внутренности Unix. Пер Бринч Хансен сказал: «Я явно потрудился напрасно», когда увидел примитивы параллелизма Java .
Маркиз Лорн

Это верно. Пример, приведенный OP, может блокировать каждый метод, но на самом деле все они блокируются на одном и том же объекте. Очень обманчивый синтаксис. После использования Java более 10 лет я не знал этого. Поэтому я бы избегал синхронизированных методов по этой причине. Я всегда думал, что невидимый объект был создан для каждого метода, который был определен с помощью synchronized.
Питер Квиринг

21

Из «Учебников Java ™» по синхронизированным методам :

Во-первых, невозможно выполнить два вызова синхронизированных методов для одного и того же объекта . Когда один поток выполняет синхронизированный метод для объекта, все остальные потоки, которые вызывают синхронизированные методы для того же блока объекта (приостанавливают выполнение), пока первый поток не завершится с объектом.

Из «Учебников Java ™» по синхронизированным блокам :

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

(Акцент мой)

Предположим, у вас есть 2 не перемежающиеся переменные. Таким образом, вы хотите получить доступ к каждому из разных потоков одновременно. Вы должны определить блокировку не для самого класса объекта, а для класса 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

Доступ к блокировке есть на объекте, а не на методе. Какие переменные доступны в методе, не имеет значения.

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

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

так что вы можете указать объект, чья блокировка должна быть получена.

Если вы хотите избежать блокировки на содержащем объекте, вы можете выбрать между:


7

Из документации оракула ссылка

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

Во-первых, невозможно выполнить два вызова синхронизированных методов для одного и того же объекта. Когда один поток выполняет синхронизированный метод для объекта, все остальные потоки, которые вызывают синхронизированные методы для того же блока объекта (приостанавливают выполнение), пока первый поток не завершится с объектом.

Во-вторых, при выходе из синхронизированного метода он автоматически устанавливает отношение «до и после» с любым последующим вызовом синхронизированного метода для того же объекта. Это гарантирует, что изменения состояния объекта видны всем потокам.

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

Это ответит на ваш вопрос: для одного и того же объекта 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

Да, он будет блокировать другой метод , потому что синхронизирован метод применяется к ВСЕМУ объекту класса , как заостренные .... но в любом случае он будет блокировать другие выполнения потока только при выполнении суммы в любом методе Адда или 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. .

Попробуйте использовать « synchronized » в incrementB и без него, и вы увидите разные результаты. Если incrementB также « синхронизирован », то он должен ждать завершения incrementA (). Запустите несколько раз каждый вариант.

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 и наоборот зависит от JVM, и существует высокая вероятность того, что два разных числа могут быть хэшированы на один и тот же адрес, если они находятся между -128 и 127.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.