Многопоточное поведение со статическими членами

Как ведет себя многопоточность в случае статических членов? Как и в случае с классом Singleton, если я попытаюсь создать экземпляр в статическом блоке и в статическом методе, я верну экземпляр, и два потока попытаются выполнить getInstance() одновременно.. как это будет вести себя внутри, поскольку статические загрузился только один раз

public class SingleTonUsingStaticInitialization {

    private static SingleTonUsingStaticInitialization INSTANCE = null;

    static {
        try {
            INSTANCE = new SingleTonUsingStaticInitialization();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private SingleTonUsingStaticInitialization() {
    }

    public static SingleTonUsingStaticInitialization getInstance() {
        return INSTANCE;
    }
}

person Shubhashish    schedule 19.02.2021    source источник


Ответы (3)


Этот конкретный пример?

С резьбой все в порядке. С точки зрения стиля это плачевно. НЕ пишите такие блоки catch. Это также означает, что если возникнет исключение (здесь этого не может быть — ваш конструктор пуст), ваш код сбросит половину информации в системную ошибку, а затем продолжит работу с экземпляром нулевой ссылки экземпляра Singleton, вызывая другие код для выплевывания исключений NullPointerException (потому что код просто продолжает работать, так как вы поймали исключение, а не позволили ему произойти). Если вы обработаете все исключения таким образом, одна ошибка вызовет сотни ошибок в ваших журналах, и все они не будут иметь значения, кроме первой.

Как только вы позаботитесь об этой проблеме обработки исключений, вы можете сделать переменную final и больше не присваивать ей значение null. Пока вы это делаете, сделайте весь класс final. Фактически это уже (поскольку у вас есть только частный конструктор):

public final class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Single() {}

    public static Singleton getInstance() {
        return INSTANCE;
}

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

Это единственная халява, которую вы получаете: для статических методов, как правило, все потоки могут просто запускать один и тот же метод одновременно, если они этого хотят. И здесь они делают - просто инициализация (которая включает в себя часть ... = new Singleton();) выполняется только один раз.

NB: Если вам нужно делать более сложные вещи, создайте вспомогательные методы:

public final class Singleton {
    private static Singleton INSTANCE = create();

    private Singleton(Data d) {
        // do stuff with 'd'
    }

    private static Singleton create() {
        Data d;
        try {
            d = readStuffFromDataIncludedInMyJar();
        } catch (IOException e) {
            throw new Error("states.txt is corrupted", e);
        }
        return new Singleton(d);
    }
}

Этот:

  1. Сохраняет код простым - статические инициализаторы - это вещь, но довольно экзотический java.
  2. Облегчает тестирование вашего кода.
  3. Это внутренний файл; если он отсутствует/сломан, это так же вероятно/так же проблематично, как один из файлов вашего класса, который ушел на прогулку. Ошибка гарантирована здесь. Это не может произойти, если вы не написали ошибку или не испортили сборку, и жесткий сбой с явным исключением, сообщающим вам, что именно не так, - это именно то, что вы хотите, чтобы произошло в этом случае, а не для слепого продолжения кода в состоянии, когда половина ваше приложение перезаписано абракадаброй из-за сбоя диска или чего-то еще. Лучше просто заключить, что все пошло наперекосяк, так и сказать, и перестать бежать.
person rzwitserloot    schedule 19.02.2021
comment
Блестящие очки. Но меня смущает второй фрагмент кода. Я не вижу, как create возвращает нужный экземпляр класса Singleton. Я что-то упускаю? И ваш комментарий говорит о сложных вещах в конструкторе. Но разве вы уже не делаете сложные вещи в своем методе create а не в конструкторе? - person Basil Bourque; 19.02.2021
comment
Второй фрагмент — это неполный код-заглушка. Я знаю, это звучит грубо, но вы должны быть знакомы с таким кодом, прежде чем переходить к многопоточности. Многопоточность — очень сложная концепция, и ее невозможно изучить, если вы не понимаете основ программирования. - person Torben; 19.02.2021
comment
Да, мои извинения, фрагмент кода был немного поспешным. Я исправил это и исправил некоторые опечатки, пока был там. - person rzwitserloot; 19.02.2021
comment
Я бы использовал ExceptionInInitializerError вместо неопределенного Error. Нет необходимости добавлять дополнительное сообщение (что делает предположение о проблеме). Цепочка IOException уже предоставляет всю информацию. - person Holger; 18.03.2021

Вы в безопасности в том смысле, что getInstance вернет один и тот же экземпляр нескольким потокам. Это гарантируется JLS, и это единственное место, которому вы должны делегировать свое понимание. В частности, в этой главе говорится:

Для каждого класса или интерфейса C существует уникальная блокировка инициализации LC.

И продолжает дальше, говоря, что:

Процедура инициализации C выглядит следующим образом:

Синхронизируйте блокировку инициализации, LC, для C. Это включает ожидание, пока текущий поток не сможет получить LC

Проще говоря, только один поток может инициализировать это поле static. Период.

Освобождение этой блокировки дает надлежащие happens-before гарантии между действием в статическом блоке и любым потоком, который использует это статическое поле. Это следует из той же главы, из:

Реализация может оптимизировать эту процедуру, исключая получение блокировки на шаге 1 (и освобождение на шаге 4/5), когда она может определить, что инициализация класса уже завершена, при условии, что с точки зрения модели памяти все происходит: до порядков, которые существовали бы, если бы блокировка была получена, все еще существуют, когда выполняется оптимизация

Или, опять же, говоря простым языком, все, что происходит в этом блоке static, будет видно всем читающим потокам.


С этой целью у вас будет подходящий инструмент для удаления этого блока static с помощью так называемой постоянной динамики. Инфраструктура для него уже есть, но javac до сих пор ею не пользуется. Подробнее об этом можно прочитать здесь. Некоторые проекты уже используют это - если у вас есть правильный jdk, например, jacoco делает это.

person Eugene    schedule 19.02.2021

См. тонкости, сделанные в ответе rzwitserloot.

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

Безопасность потоков гарантируется из-за того же поведения загрузчика классов, которое обсуждалось в другом ответе. Перечисление загружается один раз и только один раз для каждого загрузчика класса при первой загрузке класса.

Если у вас есть несколько потоков, обращающихся к одному объекту, определенному этим перечислением, первый поток, достигший точки загрузки этого класса, вызовет создание экземпляра нашего объекта перечисления с запущенным методом конструктора. Другие оставшиеся потоки будут блокироваться при попытке доступа к объекту перечисления до тех пор, пока класс перечисления не завершит загрузку, а его единственный именованный объект перечисления не завершит свое построение. JVM автоматически справляется со всеми этими спорами, и нам не требуется дополнительное кодирование. Все это поведение гарантируется спецификациями Java.

package org.vaadin.example;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public enum AppContext
{
    INSTANCE;

    private String wording;

    AppContext ( )
    {
        try
        {
            readStuffFromDataIncludedInMyJar();
        }
        catch ( IOException e )
        {
            throw new Error( "Failed to load from text file. Message # 7a608ddf-8c5f-4f77-a9c9-5ab852fde5b1." , e );
        }
    }

    private void readStuffFromDataIncludedInMyJar ( ) throws IOException
    {
        Path path = Paths.get( "/Users/basilbourque/example.txt" );
        String read = Files.readAllLines( path ).get( 0 );
        this.wording = read;
        System.out.println( "read = " + read );
    }

    public static void main ( String[] args )
    {
        System.out.println( AppContext.INSTANCE.toString() );
    }
}

Когда бег.

read = Wazzup?
INSTANCE
person Basil Bourque    schedule 19.02.2021
comment
Преимущество использования enum для этого заключается в том, что вы получаете поведение сериализации, подходящее для синглетонов, и вы получаете это бесплатно. Однако есть цена: enum здесь не имеет смысла. Этот код представляет собой WTF и требует комментария, чтобы объяснить, почему в blazes можно использовать перечисление. В защиту шаблона, он несколько хорошо известен. Но суть в том, почему вы сериализуете синглтон? Кто вообще использует запеченную в сериализации Java, презираемую даже ее авторами? Да, это случается, но редко, и этот шаблон не следует использовать, если это не важно. - person rzwitserloot; 19.02.2021
comment
Таким образом, я оспариваю утверждение Многие люди рекомендовали перечисление как идеальный способ определения синглтона в Java. - Возможно, «многие люди» рекомендуют его, но обращение к массам не стоит чертовски много, если упомянутые массы не продумали это. Обычно это не то, как вы хотите делать синглтоны. - person rzwitserloot; 19.02.2021
comment
когда вы говорите, что JLS гарантирует это, вы должны быть конкретными. - person Eugene; 19.02.2021