Статьи

Основы параллелизма: тупики и мониторы объектов

Эта статья является частью нашего курса Академии под названием Основы параллелизма Java .

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

1. Живость

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

1.1. Тупик

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

1
2
3
Thread 1: locks resource A, waits for resource B
 
Thread 2: locks resource B, waits for resource A

Чтобы лучше понять проблему, давайте взглянем на следующий исходный код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Deadlock implements Runnable {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();
    private final Random random = new Random(System.currentTimeMillis());
 
    public static void main(String[] args) {
        Thread myThread1 = new Thread(new Deadlock(), "thread-1");
        Thread myThread2 = new Thread(new Deadlock(), "thread-2");
        myThread1.start();
        myThread2.start();
    }
 
    public void run() {
        for (int i = 0; i < 10000; i++) {
            boolean b = random.nextBoolean();
            if (b) {
                System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 1.");
                synchronized (resource1) {
                    System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 1.");
                    System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 2.");
                    synchronized (resource2) {
                        System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 2.");
                    }
                }
            } else {
                System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 2.");
                synchronized (resource2) {
                    System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 2.");
                    System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 1.");
                    synchronized (resource1) {
                        System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 1.");
                    }
                }
            }
        }
    }
}

Как видно из приведенного выше кода, два потока запускаются и пытаются заблокировать два статических ресурса. Но для взаимоблокировки нам нужна различная последовательность для обоих потоков, поэтому мы используем экземпляр Random, чтобы выбрать, какой ресурс поток хочет заблокировать первым. Если булева переменная b имеет значение true, ресурс1 сначала блокируется, а затем потоки пытаются получить блокировку для ресурса 2. Если b равен false, поток сначала блокирует ресурс2, а затем пытается заблокировать ресурс1. Эта программа не должна запускаться долго, пока мы не достигнем первой тупиковой ситуации, то есть программа зависнет навсегда, если мы не остановим ее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
[thread-1] Trying to lock resource 1.
 
[thread-1] Locked resource 1.
 
[thread-1] Trying to lock resource 2.
 
[thread-1] Locked resource 2.
 
[thread-2] Trying to lock resource 1.
 
[thread-2] Locked resource 1.
 
[thread-1] Trying to lock resource 2.
 
[thread-1] Locked resource 2.
 
[thread-2] Trying to lock resource 2.
 
[thread-1] Trying to lock resource 1.

В этом исполнении поток-1 удерживает блокировку для resource2 и ожидает блокировки для resource1, тогда как поток-2 удерживает блокировку для resource1 и ожидает resource2.

Если бы мы установили для булевой переменной b в приведенном выше примере кода значение true, мы бы не столкнулись с тупиковой ситуацией, поскольку последовательность, в которой поток-1 и поток-2 запрашивают блокировки, всегда одинакова. Следовательно, один из обоих потоков сначала получает блокировку, а затем запрашивает вторую блокировку, которая все еще доступна, потому что другие потоки ожидают первую блокировку.

В целом можно определить следующие требования для тупика:

  • Взаимное исключение. Существует ресурс, к которому может обращаться только один поток в любой момент времени.
  • Удержание ресурса: заблокировав один ресурс, поток пытается получить другую блокировку для другого исключительного ресурса.
  • Нет выгрузки: нет механизма, который освобождает ресурс, если один поток удерживает блокировку в течение определенного периода времени.
  • Круговое ожидание: во время выполнения возникает созвездие, в котором два (или более) потока каждый ожидают в другом потоке, чтобы освободить заблокированный ресурс.

Хотя список требований выглядит длинным, нередки случаи, когда более продвинутые многопоточные приложения сталкиваются с проблемами взаимоблокировки. Но вы можете попытаться избежать взаимоблокировок, если вы можете ослабить одно из перечисленных выше требований:

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

1.2. голодание

Планировщик решает, какой из потоков в состоянии RUNNABLE должен выполняться следующим. Решение основано на приоритете потока; следовательно, потоки с более низким приоритетом получают меньше процессорного времени, чем потоки с более высоким приоритетом. То, что звучит как разумная особенность, также может вызвать проблемы при злоупотреблении. Если большую часть времени выполняются потоки с высоким приоритетом, потоки с более низким приоритетом кажутся «голодными», поскольку у них не хватает времени для правильного выполнения своей работы. Поэтому рекомендуется устанавливать приоритет потока, только если для этого есть веские причины.

Сложным примером для голодания потока является, например, метод finalize (). Эта особенность языка Java может использоваться для выполнения кода до того, как объект будет подвергнут сборке мусора. Но когда вы посмотрите на приоритет потока финализатора, вы можете увидеть, что он работает не с наивысшим приоритетом. Следовательно, существует вероятность истощения потока, если методы finalize () вашего объекта тратят слишком много времени по сравнению с остальной частью кода.

Другая проблема, связанная со временем выполнения, заключается в том, что не определено, в каком порядке потоки проходят синхронизированный блок. Когда многим параллельным потокам необходимо передать некоторый код, который инкапсулирован в синхронизированный блок, может случиться так, что некоторым потокам придется ждать дольше, чем другим потокам, чтобы они могли войти в блок. Теоретически они могут никогда не войти в блок.

Решением последней проблемы является так называемая «справедливая» блокировка. Честные блокировки учитывают время ожидания потоков при выборе следующего потока для прохождения. Пример реализации справедливой блокировки представлен Java SDK: java.util.concurrent.locks.ReentrantLock. Если используется конструктор с булевым флагом, установленным в true, ReentrantLock предоставляет доступ к самому длинному ожидающему потоку. Это гарантирует отсутствие голодания, но в то же время создает проблему, состоящую в том, что приоритет потока не учитывается, и поэтому потоки с более низким приоритетом, которые часто ожидают на этом барьере, могут выполняться чаще. Наконец, что не менее важно, класс ReentrantLock может, конечно, учитывать только потоки, ожидающие блокировки, то есть потоки, которые выполнялись достаточно часто, чтобы достичь блокировки. Если приоритет потока слишком низок, это может случаться не часто, и поэтому потоки с более высоким приоритетом по-прежнему чаще проходят блокировку

2. Объектные мониторы с wait () и notify ()

Обычная задача в многопоточных вычислениях состоит в том, чтобы иметь несколько рабочих потоков, ожидающих, пока их производитель создаст для них какую-то работу. Но, как мы узнали, занятое ожидание в цикле и проверка некоторого значения не является хорошим вариантом с точки зрения процессорного времени. В этом случае метод Thread.sleep () также не имеет большого значения, так как мы хотим начать нашу работу сразу после его отправки.

Поэтому язык программирования Java имеет другую конструкцию, которую можно использовать в этом сценарии: wait () и notify (). Метод wait (), который каждый объект наследует от класса java.lang.Object, можно использовать для приостановки выполнения текущего потока и ожидания, пока другие потоки не разбудят нас с помощью метода notify (). Для правильной работы поток, который вызывает метод wait (), должен удерживать полученную блокировку перед использованием синхронизированного ключевого слова. При вызове wait () блокировка снимается, и потоки ожидают, пока другой поток, которому теперь принадлежит блокировка, не вызывает notify () для того же экземпляра объекта.

Конечно, в многопоточном приложении может быть несколько потоков, ожидающих уведомления какого-либо объекта. Следовательно, есть два разных метода для пробуждения потоков: notify () и notifyAll (). Тогда как первый метод пробуждает только один из ожидающих потоков, методы notifyAll () пробуждают их всех. Но имейте в виду, что, как и в случае с синхронизированным ключевым словом, нет правила, определяющего, какой поток будет активироваться при вызове notify (). В простом примере производителя и потребителя это не имеет значения, поскольку нас не интересует тот факт, какая нить точно просыпается.

Следующий код демонстрирует, как можно использовать механизмы wait () и notify (), чтобы позволить потокам потребителя ждать новой работы, которая помещается в очередь из какого-либо потока производителя:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package a2;
 
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
 
public class ConsumerProducer {
    private static final Queue queue = new ConcurrentLinkedQueue();
    private static final long startMillis = System.currentTimeMillis();
 
    public static class Consumer implements Runnable {
 
        public void run() {
            while (System.currentTimeMillis() < (startMillis + 10000)) {
                synchronized (queue) {
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (!queue.isEmpty()) {
                    Integer integer = queue.poll();
                    System.out.println("[" + Thread.currentThread().getName() + "]: " + integer);
                }
            }
        }
    }
 
    public static class Producer implements Runnable {
 
        public void run() {
            int i = 0;
            while (System.currentTimeMillis() < (startMillis + 10000)) {
                queue.add(i++);
                synchronized (queue) {
                    queue.notify();
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (queue) {
                queue.notifyAll();
            }
        }
 
    }
 
    public static void main(String[] args) throws InterruptedException {
        Thread[] consumerThreads = new Thread[5];
        for (int i = 0; i < consumerThreads.length; i++) {
            consumerThreads[i] = new Thread(new Consumer(), "consumer-" + i);
            consumerThreads[i].start();
        }
        Thread producerThread = new Thread(new Producer(), "producer");
        producerThread.start();
        for (int i = 0; i < consumerThreads.length; i++) {
            consumerThreads[i].join();
        }
        producerThread.join();
    }
}

Метод main () запускает пять потоков потребителей и один поток производителей, а затем ожидает их завершения. Затем поток производителя вставляет новое значение в очередь и затем уведомляет все ожидающие потоки о том, что что-то произошло. Потребительские потоки получают блокировку очереди и затем засыпают, чтобы позже проснуться, когда очередь снова заполняется. Когда поток производителя завершил свою работу, он уведомляет все потребительские потоки о пробуждении. Если мы не сделаем последний шаг, потребительские потоки будут ждать следующего уведомления, поскольку мы не указали тайм-аут ожидания. Вместо этого мы могли бы использовать метод wait (long timeout) для пробуждения, по крайней мере, через некоторое время.

2.1. Вложенные синхронизированные блоки с wait () и notify ()

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class SynchronizedAndWait {
    private static final Queue queue = new ConcurrentLinkedQueue();
 
    public synchronized Integer getNextInt() {
        Integer retVal = null;
        while (retVal == null) {
            synchronized (queue) {
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                retVal = queue.poll();
            }
        }
        return retVal;
    }
 
    public synchronized void putInt(Integer value) {
        synchronized (queue) {
            queue.add(value);
            queue.notify();
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        final SynchronizedAndWait queue = new SynchronizedAndWait();
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    queue.putInt(i);
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Integer nextInt = queue.getNextInt();
                    System.out.println("Next int: " + nextInt);
                }
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
}

Как мы узнали ранее , добавление синхронизированного к сигнатуре метода равнозначно созданию синхронизированного (этого) {} блока. В приведенном выше примере мы случайно добавили ключевое слово synchronized в метод и затем синхронизировались в очереди монитора объектов, чтобы перевести текущий поток в спящий режим в ожидании следующего значения из очереди. Затем текущий поток освобождает блокировку блокировки для очереди, но не блокировку блокировки для этого. Метод putInt () уведомляет спящий поток о добавлении нового значения. Но случайно мы также добавили ключевое слово, синхронизированное с этим методом. Когда второй поток засыпает, он все еще удерживает блокировку. Тогда первый поток не может войти в метод putInt (), поскольку блокировка this удерживается первым потоком. Следовательно, у нас тупиковая ситуация, и программа зависает. Если вы выполните приведенный выше код, это произойдет сразу после начала программы.

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

2.2. Условия в синхронизированных блоках

Часто вам нужно будет проверить, что какое-то условие выполнено, прежде чем выполнять какое-либо действие с синхронизированным объектом. Когда у вас есть, например, очередь, вы хотите подождать, пока эта очередь не будет заполнена. Следовательно, вы можете написать метод, который проверяет, заполнена ли очередь. Если нет, вы кладете текущий поток в спящий режим, пока он не проснется:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public Integer getNextInt() {
    Integer retVal = null;
    synchronized (queue) {
        try {
            while (queue.isEmpty()) {
                queue.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    synchronized (queue) {
        retVal = queue.poll();
        if (retVal == null) {
            System.err.println("retVal is null");
            throw new IllegalStateException();
        }
    }
    return retVal;
}

Приведенный выше код синхронизируется в очереди перед вызовом wait (), а затем ожидает в цикле while, пока в очереди не появится хотя бы одна запись. Второй синхронизированный блок снова использует очередь в качестве монитора объекта. Опрашивает () очередь для значения внутри. Для демонстрационных целей IllegalStateException генерируется, когда poll () возвращает ноль. Это тот случай, когда в очереди нет значений для опроса.

Запустив этот пример, вы увидите, что IllegalStateException выдается очень скоро. Хотя мы правильно синхронизировались на мониторе очереди, исключение выдается. Причина в том, что у нас есть два отдельных синхронизированных блока. Представьте, что у нас есть два потока, которые прибыли в первый синхронизированный блок. Первый поток входит в блок и засыпает, потому что очередь пуста. То же самое верно для второго потока. Теперь, когда оба потока просыпаются (другим потоком, вызывающим notifyAll () на мониторе), они оба видят значение в очереди (добавленное производителем. Затем оба потока достигают второго барьера. Здесь первый поток входит и опрашивает значение из очереди. При входе во второй поток очередь уже пуста, поэтому она получает нулевое значение в качестве возвращаемого значения из вызова poll () и выдает исключение.

Чтобы избежать ситуаций, подобных описанной выше, вам придется выполнять все операции, которые зависят от состояния монитора, в одном синхронизированном блоке:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public Integer getNextInt() {
    Integer retVal = null;
    synchronized (queue) {
        try {
            while (queue.isEmpty()) {
                queue.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        retVal = queue.poll();
    }
    return retVal;
}

Здесь мы выполняем метод poll () в том же синхронизированном блоке, что и метод isEmpty (). Через синхронизированный блок мы уверены, что только один поток выполняет методы на этом мониторе в данный момент времени. Следовательно, никакой другой поток не может удалить элементы из очереди между вызовами isEmpty () и poll ().

3. Проектирование для многопоточности

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

3.1. Неизменный объект

Одним из правил проектирования, которое считается очень важным в этом контексте, является неизменность. Если вы разделяете экземпляры объекта между разными потоками, вы должны обратить внимание, что два потока не изменяют один и тот же объект одновременно. Но объекты, которые нельзя изменить, легко обрабатывать в таких ситуациях, поскольку вы не можете их изменить. Вы всегда должны создавать новый экземпляр, когда хотите изменить данные. Базовый класс java.lang.String является примером неизменяемого класса. Каждый раз, когда вы хотите изменить строку, вы получаете новый экземпляр:

1
2
3
String str = "abc";
 
String substr = str.substring(1);

Хотя создание объекта — это операция, которая не обходится без затрат, эти затраты часто переоценивают. Но вы всегда должны взвешивать, если простой дизайн с неизменяемыми объектами перевешивает не использовать неизменяемые объекты с риском ошибок параллелизма, которые наблюдаются, возможно, очень поздно в проекте.

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

  • Все поля должны быть окончательными и приватными.
  • Должны быть не сеттерские методы.
  • Сам класс должен быть объявлен как final, чтобы подклассы не нарушали принцип неизменности.
  • Если поля имеют не примитивный тип, а ссылку на другой объект:
    • Не должно быть метода получения, который предоставляет ссылку непосредственно вызывающей стороне.
    • Не изменяйте ссылочные объекты (или, по крайней мере, изменение этих ссылок невидимо для клиентов объекта).

Экземпляры следующего класса представляют сообщение с темой, телом сообщения и несколькими парами ключ / значение:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class ImmutableMessage {
    private final String subject;
    private final String message;
    private final Map<String,String> header;
 
    public ImmutableMessage(Map<String,String> header, String subject, String message) {
        this.header = new HashMap<String,String>(header);
        this.subject = subject;
        this.message = message;
    }
 
    public String getSubject() {
        return subject;
    }
 
    public String getMessage() {
        return message;
    }
 
    public String getHeader(String key) {
        return this.header.get(key);
    }
 
    public Map<String,String> getHeaders() {
        return Collections.unmodifiableMap(this.header);
    }
}

Класс является неизменным, так как все его поля являются окончательными и частными. Нет методов, которые могли бы изменить состояние экземпляра после его создания. Возвращать ссылки на тему и сообщение безопасно, так как сам String является неизменным классом. Вызывающая сторона, которая получает ссылку, например, на сообщение, не может изменить его напрямую. С картой заголовков мы должны уделять больше внимания. Простой возврат ссылки на карту позволит вызывающему абоненту изменить ее содержимое. Следовательно, мы должны вернуть неизменяемую карту, полученную с помощью вызова Collections.unmodifiableMap (). Это возвращает представление на карте, которое позволяет вызывающим абонентам читать значения (которые снова являются строками), но не допускает изменений. UnsupportedOperationException будет выброшено при попытке изменить экземпляр Map. В этом примере также безопасно возвращать значение для определенного ключа, как это делается в getHeader (ключ String), так как возвращаемая строка снова неизменна. Если карта содержит объекты, которые не являются неизменяемыми сами по себе, эта операция не будет поточно-ориентированной.

3.2. API дизайн

При разработке открытых методов класса, т. Е. API этого класса, вы также можете попытаться спроектировать его для многопоточного использования. У вас могут быть методы, которые не должны выполняться, когда объект находится в определенном состоянии. Одним простым решением для преодоления этой ситуации было бы иметь закрытый флаг, который указывает, в каком состоянии мы находимся, и генерирует, например, IllegalStateException, когда конкретный метод не должен вызываться:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Job {
    private boolean running = false;
    private final String filename;
 
    public Job(String filename) {
        this.filename = filename;
    }
 
    public synchronized void start() {
        if(running) {
            throw new IllegalStateException("...");
        }
        ...
    }
 
    public synchronized List getResults() {
        if(!running) {
            throw new IllegalStateException("...");
        }
        ...
    }
}

Приведенный выше шаблон также часто называют «шаблоном балансировки», поскольку метод блокируется, когда он выполняется в неправильном состоянии. Но вы могли бы спроектировать ту же функциональность, используя статический фабричный метод, не проверяя в каждом методе состояние объекта:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class Job {
    private final String filename;
 
    private Job(String filename) {
        this.filename = filename;
    }
 
    public static Job createAndStart(String filename) {
        Job job = new Job(filename);
        job.start();
        return job;
    }
 
    private void start() {
        ...
    }
 
    public synchronized List getResults() {
        ...
    }
}

Статический метод фабрики создает новый экземпляр Job, используя закрытый конструктор, и уже вызывает start () для экземпляра. Возвращенная ссылка на Job уже находится в правильном состоянии для работы, поэтому метод getResults () нужно только синхронизировать, но не проверять состояние объекта.

3.3. Поток локального хранилища

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

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

1
private static final ThreadLocal myThreadLocalInteger = new ThreadLocal();

Тип данных, которые должны храниться в ThreadLocal, задается универсальным параметром шаблона T. В приведенном выше примере мы использовали только Integer, но мы могли бы также использовать любой другой тип данных здесь. Следующий код демонстрирует использование ThreadLocal:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ThreadLocalExample implements Runnable {
    private static final ThreadLocal threadLocal = new ThreadLocal();
    private final int value;
 
    public ThreadLocalExample(int value) {
        this.value = value;
    }
 
    @Override
    public void run() {
        threadLocal.set(value);
        Integer integer = threadLocal.get();
        System.out.println("[" + Thread.currentThread().getName() + "]: " + integer);
    }
 
    public static void main(String[] args) throws InterruptedException {
        Thread threads[] = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new ThreadLocalExample(i), "thread-" + i);
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
    }
}

Вы можете удивиться, что каждый поток выводит именно то значение, которое он получает через конструктор, хотя переменная threadLocal объявлена ​​статической. Внутренняя реализация ThreadLocal гарантирует, что каждый раз, когда вы вызываете set (), данное значение сохраняется в области памяти, к которой имеет доступ только текущий поток. Следовательно, когда вы вызываете впоследствии get (), вы получаете значение, которое вы установили ранее, несмотря на тот факт, что в то же время другие потоки могли вызывать set ().

Серверы приложений в мире Java EE активно используют функцию ThreadLocal, поскольку у вас много параллельных потоков, но каждый поток имеет, например, свою собственную транзакцию или контекст безопасности. Поскольку вы не хотите передавать эти объекты в каждом вызове метода, вы просто сохраняете его в собственной памяти потока и обращаетесь к нему позже, когда вам это нужно.