пятница, 22 апреля 2011 г.

О бедном ThreadLocal замолвите слово


Цитата от Антона Архипова:

Q: что такое ThreadLocal?
A: это Thread, который Local. Ну то есть локальный Thread... лёгковесный... JVM может его круто запускать и использовать меньше памяти...


На собеседованиях есть тенденция спрашивать про ThreadLocal. Как оказалось, далеко не все Java программисты знакомы с данным понятием. В заметке я попробую максимально коротко рассказать про ThreadLocal и поделиться примером его использования.

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

Первое решение, заведомо неправильное, может выглядеть следующим образом:

  1. package name.samolisov.threadlocal;

  2.  

  3.  

  4.  

  5. public class SomeBuilderDemo {

  6.  

  7.     public static class SomeBuilder {

  8.        

  9.         private int counter;

  10.        

  11.         public void build() {

  12.             System.out.println("Thread " + Thread.currentThread().getName() + " Build some structure...");

  13.                        

  14.             counter++;

  15.            

  16.             try {

  17.                 Thread.sleep(100);

  18.             }

  19.             catch (InterruptedException e) {

  20.                 e.printStackTrace();

  21.             }

  22.         }

  23.        

  24.         public int getCount() {

  25.             return counter;

  26.         }

  27.     }

  28.    

  29.     public static class SomeBuilderThread extends Thread {

  30.        

  31.         private SomeBuilder builder;

  32.        

  33.         public SomeBuilderThread(SomeBuilder builder) {    

  34.             this.builder = builder;

  35.         }

  36.        

  37.         @Override

  38.         public void run() {

  39.             for (int i = 0; i < Math.random() * 10; i++) {

  40.                 builder.build();               

  41.             }

  42.             System.out.println("My name is " + getName() + " and I built " + builder.getCount() + " things");

  43.         }

  44.     }

  45.    

  46.     public static void main(String[] args) {

  47.         SomeBuilder builder = new SomeBuilder();

  48.        

  49.         Thread thread1 = new SomeBuilderThread(builder);

  50.         Thread thread2 = new SomeBuilderThread(builder);

  51.         try {

  52.             thread1.start();

  53.             thread2.start();

  54.            

  55.             thread1.join();

  56.             thread2.join();

  57.         }

  58.         catch (InterruptedException e) {

  59.             e.printStackTrace();

  60.         }

  61.     }

  62. }

  63.  



Результат работы:

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 - с независимой от нее своей. Для изоляции переменных можно использовать таблицу, индексируемую, например, названием потока:

  1. package name.samolisov.threadlocal;

  2.  

  3. import java.util.Hashtable;

  4.  

  5.  

  6. public class SomeBuilderDemo {

  7.  

  8.     public static class SomeBuilder {

  9.        

  10.         private Hashtable<String, Integer> counters = new Hashtable<String, Integer>();

  11.        

  12.         public void build() {

  13.             System.out.println("Thread " + Thread.currentThread().getName() + " Build some structure...");

  14.            

  15.             if (!counters.containsKey(Thread.currentThread().getName()))

  16.                 counters.put(Thread.currentThread().getName(), 0);

  17.            

  18.             counters.put(Thread.currentThread().getName(), counters.get(Thread.currentThread().getName()) + 1);

  19.            

  20.             try {

  21.                 Thread.sleep(100);

  22.             }

  23.             catch (InterruptedException e) {

  24.                 e.printStackTrace();

  25.             }

  26.         }

  27.        

  28.         public int getCount() {

  29.             return counters.get(Thread.currentThread().getName());

  30.         }

  31.     }

  32.    

  33.     public static class SomeBuilderThread extends Thread {

  34.        

  35.         private SomeBuilder builder;

  36.        

  37.         public SomeBuilderThread(SomeBuilder builder) {    

  38.             this.builder = builder;

  39.         }

  40.        

  41.         @Override

  42.         public void run() {

  43.             for (int i = 0; i < Math.random() * 10; i++) {

  44.                 builder.build();               

  45.             }

  46.             System.out.println("My name is " + getName() + " and I built " + builder.getCount() + " things");

  47.         }

  48.     }

  49.    

  50.     public static void main(String[] args) {

  51.         SomeBuilder builder = new SomeBuilder();

  52.        

  53.         Thread thread1 = new SomeBuilderThread(builder);

  54.         Thread thread2 = new SomeBuilderThread(builder);

  55.         try {

  56.             thread1.start();

  57.             thread2.start();

  58.            

  59.             thread1.join();

  60.             thread2.join();

  61.         }

  62.         catch (InterruptedException e) {

  63.             e.printStackTrace();

  64.         }

  65.     }

  66. }

  67.  



Код примера тривиален. В таблице 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-переменную:



А затем, в потоке, сделаем

  1. locals.set(myObject)



то ключом таблицы будет ссылка на объект locals, а значением - ссылка на объект myObject. При этом для другого потока мы можем "положить" внутрь locals другое значение.

Следует обратить внимание, что ThreadLocal изолирует именно ссылки на объекты, а не сами объекты. Если изолированные внутри потоков ссылки ведут на один и тот же объект, то возможны коллизии.

Теперь рассмотрим пример:
  1. package name.samolisov.threadlocal;

  2.  

  3.  

  4.  

  5. public class SomeBuilderDemo {

  6.  

  7.     public static class SomeBuilder {

  8.        

  9.         private ThreadLocal<Integer> counter = new ThreadLocal<Integer>();

  10.        

  11.         public void build() {

  12.             System.out.println("Thread " + Thread.currentThread().getName() + " Build some structure...");

  13.            

  14.             if (counter.get() == null)

  15.                 counter.set(0);

  16.            

  17.             counter.set(counter.get() + 1);

  18.            

  19.             try {

  20.                 Thread.sleep(100);

  21.             }

  22.             catch (InterruptedException e) {

  23.                 e.printStackTrace();

  24.             }

  25.         }

  26.        

  27.         public int getCount() {

  28.             return counter.get();

  29.         }

  30.     }

  31.    

  32.     public static class SomeBuilderThread extends Thread {

  33.        

  34.         private SomeBuilder builder;

  35.        

  36.         public SomeBuilderThread(SomeBuilder builder) {    

  37.             this.builder = builder;

  38.         }

  39.        

  40.         @Override

  41.         public void run() {

  42.             for (int i = 0; i < Math.random() * 10; i++) {

  43.                 builder.build();               

  44.             }

  45.             System.out.println("My name is " + getName() + " and I built " + builder.getCount() + " things");

  46.         }

  47.     }

  48.    

  49.     public static void main(String[] args) {

  50.         SomeBuilder builder = new SomeBuilder();

  51.        

  52.         Thread thread1 = new SomeBuilderThread(builder);

  53.         Thread thread2 = new SomeBuilderThread(builder);

  54.         try {

  55.             thread1.start();

  56.             thread2.start();

  57.            

  58.             thread1.join();

  59.             thread2.join();

  60.         }

  61.         catch (InterruptedException e) {

  62.             e.printStackTrace();

  63.         }

  64.     }

  65. }

  66.  



Результат работы:

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-переменная, которая и "захватывает" счетчик. В строках

  1. if (counter.get() == null)

  2.     counter.set(0);



производится ее инициализация. Важно! т.к. ThreadLocal-переменные изолированы в потоках, то инициализация такой переменной должна происходить в том же потоке, в котором она будет использоваться. Ошибкой является инициализация такой переменной - вызов метода set() - в главном потоке приложения, т.к. в данном случае значение, переданное в методе set(), будет "захвачено" для главного потока, и при вызове метода get() в целевом потоке будет возвращен null.

Для облегчения процесса инициализации ThreadLocal-переменной служит protected-метод initialValue(). Данный метод вызывается внутри метода get() в случае, если внутри ThreadLocal-переменной не "захвачено" никакое значение. При перекрытии данного метода следует учитывать такую особенность - он явно предназначен для работы в многопоточной среде, т.е. при первом вызове метода get() из потока A будет вызван initialValue и при первом вызове метода get() из потока B будет вызван тот же самый initialValue. Эти вызовы могут совпасть по времени, соответственно, если у вас в методе initialValue выполняется сложная логика, связанная с изменением каких-либо объектов, то данный метод нужно синхронизировать.

В следующем примере:

  1. package name.samolisov.threadlocal;

  2.  

  3.  

  4.  

  5. public class SomeBuilderDemo {

  6.  

  7.     public static class SomeBuilder {

  8.        

  9.         private ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {

  10.             @Override

  11.             protected Integer initialValue() {

  12.                 return 0;

  13.             }          

  14.         };

  15.        

  16.         public void build() {

  17.             System.out.println("Thread " + Thread.currentThread().getName() + " Build some structure...");

  18.            

  19.             counter.set(counter.get() + 1);

  20.            

  21.             try {

  22.                 Thread.sleep(100);

  23.             }

  24.             catch (InterruptedException e) {

  25.                 e.printStackTrace();

  26.             }

  27.         }

  28.        

  29.         public int getCount() {

  30.             return counter.get();

  31.         }

  32.     }

  33.    

  34.     public static class SomeBuilderThread extends Thread {

  35.        

  36.         private SomeBuilder builder;

  37.        

  38.         public SomeBuilderThread(SomeBuilder builder) {    

  39.             this.builder = builder;

  40.         }

  41.        

  42.         @Override

  43.         public void run() {

  44.             for (int i = 0; i < Math.random() * 10; i++) {

  45.                 builder.build();               

  46.             }

  47.             System.out.println("My name is " + getName() + " and I built " + builder.getCount() + " things");

  48.         }

  49.     }

  50.    

  51.     public static void main(String[] args) {

  52.         SomeBuilder builder = new SomeBuilder();

  53.        

  54.         Thread thread1 = new SomeBuilderThread(builder);

  55.         Thread thread2 = new SomeBuilderThread(builder);

  56.         try {

  57.             thread1.start();

  58.             thread2.start();

  59.            

  60.             thread1.join();

  61.             thread2.join();

  62.         }

  63.         catch (InterruptedException e) {

  64.             e.printStackTrace();

  65.         }

  66.     }

  67. }

  68.  



инициализация 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, должны быть потокобезопасными, то разумно изолировать в каждом потоке свой экземпляр соединения.

Реализация данного решения может выглядеть следующим образом:

  1. public class ConnectionAccess {

  2.     private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {

  3.         public Connection initialValue() {

  4.             return DriverManager.getConnection(DB_URL);

  5.         }

  6.     };

  7.        

  8.     public static Connection getConnection() {

  9.         return connectionHolder.get();

  10.     }

  11. }



Понравилось сообщение - подпишитесь на блог

23 комментария:

Vladimir комментирует...

Почему-то очевидно неправильный вариант (Листинг 1) и правильный вариант (Листинг 2) очень похожи друг на друга (хотел diff сделать, но код копируется криво). И если все-таки первый пример нормально скопировался, то все равно непонятно почему он работает неправильно (я не Java программист, но из сочувствующих :). Я попробую сам разобраться, но если добавите в статью объяснение, то было бы неплохо :)
ЗЫ У меня первый пример работает как следует.

Pavel Samolisov комментирует...

Огромное спасибо Вам за комментарий! Моя невнимательность когда-нибудь меня погубит. Я просто запостил не тот код на место первого примера.

Исправил.

Roman комментирует...

Спасибо. Я не знал об этом. Достаточно полезная статья.
Я верно понял, что ThreadLocal в первозданном виде нельзя использовать для хранения Билдеров из первого примера? Ибо передаём в оба потока один и тот же билдер. Поэтому и переменная count снова будет общей

Pavel Samolisov комментирует...

Можно поступить следующим образом: создавать в методе initialValue ThreadLocal-переменной новый экземпляр билдела, у которого, соответственно, будет свой экземпляр переменной count.

Roman комментирует...

Спасибо, теперь совсем понятна суть)

ilyas комментирует...

гуд!

Pavel Samolisov комментирует...

Да вообще чОтко все расписал, сам радуюсь.

Vladimier Khrystianovskyy комментирует...

Да, сам столкнулся что про ТреадЛокал часто спрашивают, не знал про него, хотя пишу на яве уже 12 лет :) Статья неплохая, полезная, хотя лично мне хватило описания класса в джавадоках и 2х минут. Класс по-своему полезный, но Мап решит вопрос не хуже. Так что, большой разницы имхо нет - ну разве что автоочистка при завершении Треда.

Pavel Samolisov комментирует...

В последнее время помимо ThreadLocal начали спрашивать и про InheritableThreadLocal. Рекомендую ознакомиться и с этой возможностью JVM.

А про использование на практике. Опыт конечно у всех разный, но для хранения потокозащищенных контекстов, как некий аналог javax.transaction.TransactionSynchronizationRegistry.

zencd комментирует...

Спасибо. Уже второй раз в поиске наталкиваюсь на ваши "разжёвывания" не самой популярной темы по Джаве.

Pavel Samolisov комментирует...

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

Ruslan комментирует...

полезная статья, спасибо

Pavel Samolisov комментирует...

И Вам спасибо за отзыв

Ivan Dikanev комментирует...

Спасибо, освежил в памяти. Как пример использования вспоминаю пример из нашего проекта, когда содержимое сессии пользователя в JavaEE (в те времена J2EE) приложении хранилось в HTTP-сессии, но эти данные должны были быть использованы в Stateless Session EJB-слое. Для передачи такого рода "контекста" как раз и использовались InheritableThreadLocal переменные, поскольку согласно спецификации JavaEE исполнение EJB-метода должно осуществляться в том же потоке, что исполнение сервлета, его вызывающего. В общем отличная возможность неявной передачи данных между слоями приложения, особенно если инициализировать сам поток средствами AOP, тогда для прикладного программиста все становится совсем прозрачно, нужно только забрать из потока необходимые данные.

Pavel Samolisov комментирует...

Иван, спасибо большое за практический пример!

Artem Zadorozhniy комментирует...

Спасибо за очень интересную статью!

Иван Осипов комментирует...

Спасибо, было полезно

Ivan Pavlukhin комментирует...

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

Pavel Samolisov комментирует...

Вот код класса ThreadLocal. Напомню, что внутри Thread ThreadLocal-переменные хранятся в поле типа ThreadLocal.ThreadLocalMap. У описания вложенного статического класса ThreadLocal.ThreadLocalMap.Entry есть комментарий:

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-переменные, то рано или поздно будут собраны и эти ThreadLocal-переменные, а значит и освободятся ключи в таблице ThreadLocal.ThreadLocalMap. Поэтому, то, что написали в комментарии к приведенной вами статье, мол мы создаем наследника ThreadLocal в нашем приложении, значит в ключах ThreadLocal.ThreadLocalMap будут объекты класса нашего приложения и они будут держать класс в памяти неверно.

С ключами разобрались. Причиной утечки памяти могут быть значения в таблице ThreadLocal.ThreadLocalMap. Т.к. значение доступно по обычной ссылке:

static class Entry extends WeakReference {
Object value;

Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}

Однако в классе ThreadLocal.ThreadLocalMap определены методы, чистящие значения или заменяющие их при новом вызове метода set и даже get, не находящего ключа (см. expungeStaleEntry). Здесь надо разобраться, в коде есть что почитать.

Рекомендуется или не использовать ThreadLocal-переменные в управляемой среде, или делать remove() после завершения обработки каждого запроса. Сам я использовал как раз для хранения DateFormater'а на WebLogic, падения сервера по OOM PermGen не было, хотя это конечно ни о чем и не говорит.

Ivan Pavlukhin комментирует...

Спасибо за рекомендации по использованию в управляемой среде!
Я попытался погонять некоторые тесты, но примеры кода не прошли по длине комментария. Может я найду какую-нибудь подсветку покомпактней, и потом напишу, что у меня получилось.
А комментарий там кстати, как будто был поспешный
>> Inner classes have an implicit pointer to the surrounding class...
В том примере, анонимный класс создавался в статическом контексте, и этой ссылки быть вовсе не должно.

Pavel Samolisov комментирует...

Выполнить тесты и получить результат - наверное самое верное решение. Ибо практика - критерий истины.

Можете завести Gist на GitHub, а в комментарии скинуть ссылку. Было бы интересно.

Ivan Pavlukhin комментирует...

Пока, разобраться детально во всех тонкостях утечек в серверах приложений, к сожалению, мне не удалось. Но я попробовал запустить некий такой синтетический пример, который вызывает падение по утечке памяти.
Я проверял на Tomcat, редеплоил приложение несколько раз и нарывался на OOM exception. Ежели, раскомментировать вызов remove() в contextDestroyed(...), то падение все равно происходит через некоторое время, т.к. contextDestroyed(...) может вызываться не тем потоком, что и contextInitialized(...).
Если обратиться за советом к Tomcat на кнопочку "Find leaks" после редеплоя, то он выдает следующее:
The following web applications were stopped (reloaded, undeployed), but their
classes from previous runs are still loaded in memory, thus causing a memory
leak (use a profiler to confirm):
/leak

Unknown комментирует...

Очень доступно. Спасибо за хорошую статью.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!