Цитата от Антона Архипова:
Q: что такое ThreadLocal?
A: это Thread, который Local. Ну то есть локальный Thread... лёгковесный... JVM может его круто запускать и использовать меньше памяти...
На собеседованиях есть тенденция спрашивать про ThreadLocal. Как оказалось, далеко не все Java программисты знакомы с данным понятием. В заметке я попробую максимально коротко рассказать про ThreadLocal и поделиться примером его использования.
Рассмотрим следующую задачу: нужно написать многопоточное приложение, в котором каждый поток участвует в построении некоторой структуры данных внутри некоторого класса-билдера, при этом хочется отследить сколько записей в структуре построил тот или иной поток.
Первое решение, заведомо неправильное, может выглядеть следующим образом:
- package name.samolisov.threadlocal;
- public class SomeBuilderDemo {
- public static class SomeBuilder {
- private int counter;
- public void build() {
- counter++;
- try {
- }
- e.printStackTrace();
- }
- }
- public int getCount() {
- return counter;
- }
- }
- private SomeBuilder builder;
- public SomeBuilderThread(SomeBuilder builder) {
- this.builder = builder;
- }
- public void run() {
- builder.build();
- }
- }
- }
- SomeBuilder builder = new SomeBuilder();
- try {
- thread1.start();
- thread2.start();
- thread1.join();
- thread2.join();
- }
- e.printStackTrace();
- }
- }
- }
Результат работы:
Thread Thread-1 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-0 Build some structure...
My name is Thread-1 and I built 2 things
Thread Thread-0 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-0 Build some structure...
My name is Thread-0 and I built 8 things
Здесь мы имеем два потока, код которых реализован в классе SomeBuilderThread и между которыми распростределяется переменная builder типа SomeBuilder. Именно класс SomeBuilder реализует логику построения интересующей нас структуры в методе build (для простоты вся логика представлена одной строчкой - выводом сообщения в консоль). В данном же методе и происходит инкрементация счетчика counter. Счетчик является полем класса SomeBuilder и, т.к. объект данного класса распределен между потоками, то и данное поле распределено между потоками, т.е. является общим для всех потоков.
Понятно, что результат работы данной программы неверен: поток Thread-1 построил одну запись, а поток Thread-0 - 7 записей, в то время, как программа вывела 2 и 8 соответственно.
Для решения проблемы взаимного влияния потоков на переменную counter необходимо изолировать доступ к ней из разных потоков, т.е. сделать так, чтобы поток Thread-0 взаимодействовал со своей переменной counter, а поток Thread-1 - с независимой от нее своей. Для изоляции переменных можно использовать таблицу, индексируемую, например, названием потока:
- package name.samolisov.threadlocal;
- import java.util.Hashtable;
- public class SomeBuilderDemo {
- public static class SomeBuilder {
- public void build() {
- try {
- }
- e.printStackTrace();
- }
- }
- public int getCount() {
- }
- }
- private SomeBuilder builder;
- public SomeBuilderThread(SomeBuilder builder) {
- this.builder = builder;
- }
- public void run() {
- builder.build();
- }
- }
- }
- SomeBuilder builder = new SomeBuilder();
- try {
- thread1.start();
- thread2.start();
- thread1.join();
- thread2.join();
- }
- e.printStackTrace();
- }
- }
- }
Код примера тривиален. В таблице counters хранятся ссылки на переменные-счетчики для первого и второго потоков, соответственно каждый поток может изменять только поле таблицы, индексируемое его именем.
Результат работы программы не отличается от ожидаемого:
Thread Thread-1 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-1 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-1 Build some structure...
Thread Thread-0 Build some structure...
My name is Thread-1 and I built 3 things
My name is Thread-0 and I built 4 things
поток Thread-1 построил 3 записи, в то время, как поток Thread-0 - 4.
Однако, в стандартную поставку JDK входит класс, который избавляет нас от разработки подобного велосипеда. Данный класс, как вы догадались, называется ThreadLocal.
Как работает ThreadLocal? У каждого потока - т.е. экземпляра класса Thread - есть ассоциированная с ним таблица ThreadLocal-переменных. Ключами таблицы являются cсылки на объекты класса ThreadLocal, а значениями - ссылки на объекты, "захваченные" ThreadLocal-переменными.
Например, если мы объявим ThreadLocal-переменную:
А затем, в потоке, сделаем
- locals.set(myObject)
то ключом таблицы будет ссылка на объект locals, а значением - ссылка на объект myObject. При этом для другого потока мы можем "положить" внутрь locals другое значение.
Следует обратить внимание, что ThreadLocal изолирует именно ссылки на объекты, а не сами объекты. Если изолированные внутри потоков ссылки ведут на один и тот же объект, то возможны коллизии.
Теперь рассмотрим пример:
- package name.samolisov.threadlocal;
- public class SomeBuilderDemo {
- public static class SomeBuilder {
- public void build() {
- if (counter.get() == null)
- counter.set(0);
- counter.set(counter.get() + 1);
- try {
- }
- e.printStackTrace();
- }
- }
- public int getCount() {
- return counter.get();
- }
- }
- private SomeBuilder builder;
- public SomeBuilderThread(SomeBuilder builder) {
- this.builder = builder;
- }
- public void run() {
- builder.build();
- }
- }
- }
- SomeBuilder builder = new SomeBuilder();
- try {
- thread1.start();
- thread2.start();
- thread1.join();
- thread2.join();
- }
- e.printStackTrace();
- }
- }
- }
Результат работы:
Thread Thread-1 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-1 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-1 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-1 Build some structure...
Thread Thread-1 Build some structure...
My name is Thread-0 and I built 4 things
Thread Thread-1 Build some structure...
Thread Thread-1 Build some structure...
My name is Thread-1 and I built 7 things
Как видим поток Thread-1 построил 7 записей, в то время как Thread-0 - 4.
Вместо хэш-таблицы здесь используется ThreadLocal-переменная, которая и "захватывает" счетчик. В строках
- if (counter.get() == null)
- counter.set(0);
производится ее инициализация. Важно! т.к. ThreadLocal-переменные изолированы в потоках, то инициализация такой переменной должна происходить в том же потоке, в котором она будет использоваться. Ошибкой является инициализация такой переменной - вызов метода set() - в главном потоке приложения, т.к. в данном случае значение, переданное в методе set(), будет "захвачено" для главного потока, и при вызове метода get() в целевом потоке будет возвращен null.
Для облегчения процесса инициализации ThreadLocal-переменной служит protected-метод initialValue(). Данный метод вызывается внутри метода get() в случае, если внутри ThreadLocal-переменной не "захвачено" никакое значение. При перекрытии данного метода следует учитывать такую особенность - он явно предназначен для работы в многопоточной среде, т.е. при первом вызове метода get() из потока A будет вызван initialValue и при первом вызове метода get() из потока B будет вызван тот же самый initialValue. Эти вызовы могут совпасть по времени, соответственно, если у вас в методе initialValue выполняется сложная логика, связанная с изменением каких-либо объектов, то данный метод нужно синхронизировать.
В следующем примере:
- package name.samolisov.threadlocal;
- public class SomeBuilderDemo {
- public static class SomeBuilder {
- return 0;
- }
- };
- public void build() {
- counter.set(counter.get() + 1);
- try {
- }
- e.printStackTrace();
- }
- }
- public int getCount() {
- return counter.get();
- }
- }
- private SomeBuilder builder;
- public SomeBuilderThread(SomeBuilder builder) {
- this.builder = builder;
- }
- public void run() {
- builder.build();
- }
- }
- }
- SomeBuilder builder = new SomeBuilder();
- try {
- thread1.start();
- thread2.start();
- thread1.join();
- thread2.join();
- }
- e.printStackTrace();
- }
- }
- }
инициализация ThreadLocal переменной вынесена в метод initialValue(). Т.к. логика данного метода тривиальна - он просто возвращает константу, то нет необходимости в его синхронизации.
Корректность результата работы программы не изменилась:
Thread Thread-0 Build some structure...
Thread Thread-1 Build some structure...
Thread Thread-1 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-0 Build some structure...
Thread Thread-1 Build some structure...
My name is Thread-0 and I built 3 things
Thread Thread-1 Build some structure...
Thread Thread-1 Build some structure...
My name is Thread-1 and I built 5 things
поток Thread-1 построил 5 записей, в то время, как Thread-0 - 3.
В заключение приведу пример практически значимого использования ThreadLocal. Пример взят с сайта Санкт-Петербургской группы тестирования JVM. Это - JDBC. ThreadLocal удобно использовать для предоставления каждому потоку своего экземпляра соединения с БД. Т.к. в спецификации явно не сказано, что классы, реализующие интерфейс Connection, должны быть потокобезопасными, то разумно изолировать в каждом потоке свой экземпляр соединения.
Реализация данного решения может выглядеть следующим образом:
- public class ConnectionAccess {
- }
- };
- return connectionHolder.get();
- }
- }
Понравилось сообщение - подпишитесь на блог

7 комментариев:
Почему-то очевидно неправильный вариант (Листинг 1) и правильный вариант (Листинг 2) очень похожи друг на друга (хотел diff сделать, но код копируется криво). И если все-таки первый пример нормально скопировался, то все равно непонятно почему он работает неправильно (я не Java программист, но из сочувствующих :). Я попробую сам разобраться, но если добавите в статью объяснение, то было бы неплохо :)
ЗЫ У меня первый пример работает как следует.
Огромное спасибо Вам за комментарий! Моя невнимательность когда-нибудь меня погубит. Я просто запостил не тот код на место первого примера.
Исправил.
Спасибо. Я не знал об этом. Достаточно полезная статья.
Я верно понял, что ThreadLocal в первозданном виде нельзя использовать для хранения Билдеров из первого примера? Ибо передаём в оба потока один и тот же билдер. Поэтому и переменная count снова будет общей
Можно поступить следующим образом: создавать в методе initialValue ThreadLocal-переменной новый экземпляр билдела, у которого, соответственно, будет свой экземпляр переменной count.
Спасибо, теперь совсем понятна суть)
гуд!
Да вообще чОтко все расписал, сам радуюсь.
Отправить комментарий
Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!