Статическая инициализация и проблема блокировки статического синхронизированного метода - PullRequest
4 голосов
/ 06 января 2011

Я столкнулся с некоторой проблемой блокировки в моем приложении, которая содержит несколько классов, как показано ниже:

public interface AppClient {
    void hello();
}

public class Client implements AppClient {
    public synchronized static AppClient getInstance() {
        return instance;
    }

    public void hello() {
        System.out.println("Hello Client");
    }

    private final static class InnerClient implements AppClient {
        public void hello() {
            System.out.println("Hello InnerClient");
        }
    }
    private static AppClient instance;

    static {
        instance = new InnerClient();
        doSomethingThatWillCallClientGetInstanceSeveralTimes();
    }
}

public class Application {
    new Thread() {
        AppClient c = Client.getInstance();
        c.hello();
    }.start();
    new Thread() {
        AppClient c = Client.getInstance();
        c.hello();
    }.start();
    // ...
    new Thread() {
        AppClient c = Client.getInstance();
        c.hello();
    }.start();
}

В методе doSomethingThatWillCallClientGetInstanceSeveralTimes () он выполнит довольно большую работу по инициализации, включающую много классов и циклическиво время инициализации вызовите статический метод Client.getInstance несколько раз (я понимаю, что это не очень хорошо, однако это устаревшая кодовая база, которая длится более 20 лет).

Вот моя проблема:

1) Я думал, что до завершения инициализации класса Client, только первый поток, который запускает инициализацию класса Client, может получить доступ к методу Client.getInstance, потому что JVM будет синхронизироваться наОбъект Client.class до завершения инициализации класса.Я прочитал JLS по смежной теме и пришел к такому выводу (раздел 12.4.2, Подробная процедура инициализации, http://java.sun.com/docs/books/jls/third_edition/html/execution.html).

2). Однако это было не то поведение, которое я видел в моей реальной среде.Например, есть три потока, вызывающие Client.getInstance (), thread-1 запускает инициализацию Client.class и вызывает Client.getInstance () в методе doSomethingThatWillCallClientGetInstanceSeveralTimes () несколько раз.И до завершения метода doSomethingThatWillCallClientGetInstanceSeveralTimes () поток-2 получает блокировку объекта Client.class (как это возможно? Но это произошло) и входит в метод Client.getInstance (поскольку этот метод является статическим синхронизированным методом),По какой-то причине поток-2 не может вернуть «экземпляр» (я думаю, он ожидает, пока Client.class завершит свою инициализацию).В то же время поток-1 не может продолжить работу, потому что ему все еще нужно вызвать Client.getInstance в doSomethingThatWillCallClientGetInstanceSeveralTimes () и не может получить блокировку, поскольку он принадлежит потоку-2.Threaddump сообщает мне, что thread-2 находится в состоянии RUNNABLE, а thread-1 находится в состоянии BLOCKED, ожидая блокировки, принадлежащей thread-2.

Я могу воспроизвести это поведение только в 64-битной Java 6u23 JVM в Windowsи не может воспроизвести его в 32-битной среде Java 6 JVM + Windows.Может кто-нибудь сказать мне, что мне здесь не хватает?Обречен ли этот вид кода вызывать такую ​​блокировку, если да, то как?Мое понимание JLS для этой части неверно?Или это проблема с JVM?Любая помощь приветствуется.Спасибо.

Ответы [ 2 ]

3 голосов
/ 06 января 2011

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

Я бы посоветовал вам структурировать ваш код, чтобы вам не требовалась такая блокировка при запуске. Звучит довольно сложно. Например, в вашем примере Client не нужно расширять клиент, и экземпляр может быть инициализированв строке, где он объявлен. Я хотел бы рассмотреть следующую структуру.

enum Client implements AppClient {
    INSTANCE;

    public void hello() {
        System.out.println("Hello Client");
    }
}

Вы можете сделать клиент изменяемым или использовать делегирование, чтобы он не раскрывал тот факт, что он может изменить состояние (или реализацию)

1 голос
/ 06 января 2011

JLS 12.4.2 в (6) прямо заявляет, что , пока инициализатор выполняется, блокировки снимаются. Так что я думаю, что вы видите правильный путь выполнения. Вы можете быть лучше

  • убивает статический инициализатор и выполняет синхронизированную ленивую инициализацию в аксессоре
  • вручную синхронизировать код статического инициализатора
    public synchronized static AppClient getInstance() {
      synchronized(Client.class) {
          if (instance == null) {
            instance = new InnerClient();
            doSomethingThatWillCallClientGetInstanceSeveralTimes();
          }
          return instance;
      }
    }

РЕДАКТИРОВАТЬ

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

РЕДАКТИРОВАТЬ

Извините за неправильное представление - даже при более подробном чтении должно быть видно, что в (2) второй поток не может получить блокировку (после обнаружения продолжающейся инициализации он "ждет").

...