Давайте поговорим о безопасности потоков в Spring

Java задняя часть программист Spring Язык программирования
Давайте поговорим о безопасности потоков в Spring

Безопасность пружины и резьбы


Spring, как контейнер IOC/DI, помогает нам управлять многими «бинами». Но на самом деле,Spring не гарантирует потокобезопасность этих объектов, и разработчикам необходимо писать код для решения проблем с потокобезопасностью.

Spring предоставляет атрибут области для каждого компонента, чтобы указать область действия компонента.. Это жизненный цикл фасоли. Например, bean-компонент с одноэлементной областью действия будет создан как одноэлементный объект при первом внедрении, и этот объект будет повторно использоваться до конца приложения.

  • singleton: область действия по умолчанию, каждый bean-компонент, чья область действия является singleton, будет определен как объект singleton, и жизненный цикл этого объекта соответствует контейнеру Spring IOC (но он будет создан при первом внедрении).

  • прототип: bean определен для создания нового объекта каждый раз, когда он внедряется.

  • request: bean определен для создания одноэлементного объекта в каждом HTTP-запросе, что означает, что этот одноэлементный объект будет повторно использоваться в одном запросе.

  • сеанс: bean-компонент предназначен для создания одноэлементного объекта во время существования сеанса.

  • application: bean-компонент предназначен для повторного использования одноэлементного объекта в жизненном цикле ServletContext.

  • websocket: bean определен для повторного использования одноэлементного объекта в жизненном цикле websocket.

Большинство объектов, управляемых Spring, на самом деле являются объектами без состояния.Этот тип объектов, которые не будут уничтожены многопоточностью, очень подходит для области действия Spring по умолчанию.Каждый объект синглтона без состояния потокобезопасен (можно также сказать, что, пока это объект без состояния, независимо от одиночки и нескольких экземпляров, он потокобезопасен, но, в конце концов, синглтон экономит накладные расходы на постоянное создание объекты и ГК).

Объект без состояния — это объект без собственного состояния, и, естественно, он не уничтожит свое собственное состояние из-за альтернативного планирования нескольких потоков, что приводит к проблемам с безопасностью потоков.. К объектам без состояния относятся DO, DTO и VO, которые мы часто используем в качестве моделей сущностей только для данных, а также Service, DAO и Controller.Эти объекты не имеют собственного состояния, а используются только для выполнения определенных операций. Например, функции, предоставляемые каждым DAO, являются только CRUD для базы данных, и каждое соединение с базой данных используется как локальная переменная функции (локальная переменная находится в пользовательском стеке, а сам пользовательский стек представляет собой частную память потока). область, поэтому в ней не существует проблем с безопасностью потоков), закройте, когда она закончится (или верните ее в пул соединений).

Можно подумать, что я могу избежать проблем с безопасностью между каждым запросом, используя область запроса? Это совершенно неправильно, потому что Controller по умолчанию является синглтоном, а HTTP-запрос будет выполняться несколькими потоками, что возвращает к проблеме безопасности потоков. Конечно, вы также можете изменить область действия контроллера на прототип, что на самом деле и делает Struts2, но следует отметить, что гранулярность перехвата запросов Spring MVC основана на каждом методе, а Struts2 — на каждом классе. установка контроллера на несколько экземпляров будет часто создавать и повторно использовать объекты, что серьезно повлияет на производительность.

Прочитав вышесказанное, было сказано очень ясно,Spring вообще не дает никаких гарантий и мер по многопоточной безопасности бинов. Основной причиной проблемы безопасности потоков каждого компонента является конструкция каждого компонента.Не объявляйте какие-либо переменные экземпляра с состоянием или переменные класса в bean-компоненте, если необходимо, используйте ThreadLocal, чтобы сделать переменные потоко-частными, если переменные экземпляра bean-компонента или переменные класса должны совместно использоваться несколькими потоками, тогда используйте синхронизированный, блокирующий, CAS и другие методы для достижения синхронизации потоков.

Далее будет проанализирована его реализация и функции путем анализа исходного кода ThreadLocal.ThreadLocal — очень полезный инструментальный класс, который в некоторых случаях решает проблемы безопасности потоков (когда переменные не должны совместно использоваться несколькими потоками).

Автор этой статьиSylvanasSun(sylvanas.sun@gmail.com), впервые опубликовано вБлог SylvanasSun.
Оригинальная ссылка:sylva, то есть sun.GitHub.IO/2017/11/06/…
(Для перепечатки просьба сохранить заявление в этом абзаце и сохранить гиперссылку.)

ThreadLocal


ThreadLocal — это служебный класс, предоставляющий локальные переменные для потоков. Его идея также очень проста, т.Предоставляет потоку частную копию переменной, так что несколько потоков могут свободно изменять свои локальные переменные, не затрагивая другие потоки. Однако следует отметить, чтоThreadLocal предоставляет только поверхностную копию.Если переменная ссылочного типа, то необходимо учитывать, будет ли изменено ее внутреннее состояние.Чтобы решить эту проблему, вы можете реализовать глубокую копию, переписав функцию initialValue() класса ThreadLocal., рекомендуется переопределить эту функцию с самого начала при использовании ThreadLocal.

ThreadLocal отличается от механизмов блокировки, таких как synchronized. Во-первых, отличаются сценарии их применения и идеи реализации.Блокировки подчеркивают, как синхронизировать несколько потоков для правильного совместного использования переменной, а ThreadLocal — решить, как одна и та же переменная не используется несколькими потоками.. С точки зрения снижения производительности, если механизм блокировки должен обменивать время на пространство, то ThreadLocal должен обменивать пространство на время.

ThreadLocal содержит внутренний класс ThreadLocalMap, представляющий собой HashMap, реализованный путем линейного обнаружения. Его ключом является объект ThreadLocal, который также использует WeakReference, ThreadLocalMap используется для хранения копий переменных.

    /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        ....
    }

В ThreadLocal есть только три переменных-члена, и эти три переменные связаны со стратегией хеширования ThreadLocalMap.

    /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

Единственная переменная экземпляра threadLocalHashCode — это хэш-код, используемый для адресации, который генерируется функцией nextHashCode(), которая просто увеличивает HASH_INCREMENT для генерации хэш-кода. Что касается того, почему это приращение равно 0x61c88647, основная причина в том, что начальный размер ThreadLocalMap равен 16, и каждое расширение будет вдвое больше исходного размера, так что его емкость всегда будет равна 2 в n-й степени.Равномерно распределено для уменьшения конфликтов коллизий .

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;    

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

Чтобы получить копию переменной, которая является частной для текущего потока, вызовите функцию get(). Во-первых, она вызовет функцию getMap() для получения ThreadLocalMap текущего потока Эта функция должна получить экземпляр текущего потока в качестве параметра. Если полученная карта ThreadLocalMap имеет значение null, вызовите функцию setInitialValue() для ее инициализации. Если значение не равно null, используйте карту, чтобы получить копию переменной и вернуть ее.

Функция setInitialValue() сначала вызовет функцию initialValue() для создания начального значения. Эта функция по умолчанию возвращает null. Мы можем переопределить эту функцию, чтобы возвращать переменные, которые мы хотим поддерживать в ThreadLocal. После этого вызовите функцию getMap(), чтобы получить ThreadLocalMap. Если карта уже существует, используйте вновь полученное значение, чтобы перезаписать старое значение. В противном случае вызовите функцию createMap(), чтобы создать новую карту.

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    protected T initialValue() {
        return null;
    }

Функции set() и remove() в ThreadLocal проще, чем реализация get(), они просто получают ThreadLocalMap через getMap() и затем работают с ним.

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

Реализация функции getMap() и функции createMap() также очень проста, но секрет можно обнаружить, наблюдая за этими двумя функциями:ThreadLocalMap хранится в Thread.

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    // Thread中的源码

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Если хорошенько подумать, то на самом деле можно понять идею этого дизайна. Существует распространенный метод хранения переменных копий каждого потока через глобальную поточно-безопасную карту, но этот подход полностью нарушает исходное намерение ThreadLocal.Первоначальное намерение разработки ThreadLocal состоит в том, чтобы избежать одновременного доступа нескольких потоков к одному и тому же объекту. хотя он потокобезопасен. Хранение связанного с ним ThreadLocalMap в каждом Thread полностью соответствует идее ThreadLocal.Когда вы хотите оперировать локальными переменными потока, вам нужно использовать только Thread в качестве ключа для получения ThreadLocalMap в Thread. По сравнению с методом использования глобальной Карты этот дизайн займет много места в памяти, но также сэкономит время без дополнительных методов синхронизации потоков, таких как блокировки.

Утечка памяти в ThreadLocal


Мы должны рассмотреть ситуацию, когда произойдет утечка памяти: если ThreadLocal имеет значение null, и нет строгой ссылки, указывающей на него, ThreadLocal будет переработан в соответствии с алгоритмом анализа достижимости сборки мусора. Таким образом, ThreadLocalMap будет содержать Entry с нулевым ключом, а ThreadLocalMap находится в Thread.Пока поток не завершится, эти недоступные значения вызовут утечку памяти. Чтобы решить эту проблему, функции getEntry(), set() и remove() в ThreadLocalMap будут очищать запись с нулевым ключом.В качестве примера возьмем следующий исходный код функции getEntry().

        /**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            // 清理key为null的Entry
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

Выше мы обнаружили, что ключ ThreadLocalMap является слабой ссылкой, так зачем использовать слабую ссылку? Разница между использованием сильного эталонного ключа и слабого эталонного ключа заключается в следующем:

  • Ключ сильной ссылки: ThreadLocal имеет значение null. Поскольку ThreadLocalMap содержит сильную ссылку на ThreadLocal, если ее не удалить вручную, ThreadLocal не будет переработан, что приведет к утечке памяти.

  • Ключ слабой ссылки: ThreadLocal имеет значение null. Поскольку ThreadLocalMap содержит слабую ссылку на ThreadLocal, даже если она не будет удалена вручную, ThreadLocal все равно будет переработан. ThreadLocalMap очистит все функции при вызове set(), getEntry() и удалит () позже Запись, ключ которой равен нулю.

Но следует отметить, что ThreadLocalMap содержит только эти пассивные меры для решения проблемы утечки памяти. Если после этого вы не вызовете функции set(), getEntry() и remove() ThreadLocalMap, утечки памяти все равно будут.

В случае использования пула потоков, если его вовремя не очистить, проблема с утечкой памяти незначительна, и даже возникнет проблема с логикой программы. так,Чтобы безопасно использовать ThreadLocal, вы должны вызывать remove() после каждого использования ThreadLocal, чтобы очистить бесполезную запись, например разблокировать блокировку каждый раз, когда вы ее используете.

использованная литература