1. Общ преглед
В тази статия ще разгледаме конструкцията ThreadLocal от пакета java.lang . Това ни дава възможността да съхраняваме данни поотделно за текущата нишка - и просто да ги обвиваме в специален тип обект.
2. API на ThreadLocal
Конструкцията TheadLocal ни позволява да съхраняваме данни, които ще бъдат достъпни само от определена нишка .
Да кажем, че искаме да имаме Integer стойност, която ще бъде свързана със специфичната нишка:
ThreadLocal threadLocalValue = new ThreadLocal();
След това, когато искаме да използваме тази стойност от нишка, трябва само да извикаме метод get () или set () . Най-просто казано, можем да мислим, че ThreadLocal съхранява данни вътре в карта - с нишката като ключ.
Поради този факт, когато извикаме метод get () на threadLocalValue , ще получим Integer стойност за искащата нишка:
threadLocalValue.set(1); Integer result = threadLocalValue.get();
Можем да изградим екземпляр на ThreadLocal, като използваме статичния метод withInitial () и му предадем доставчик:
ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 1);
За да премахнем стойността от ThreadLocal , можем да извикаме метода remove () :
threadLocal.remove();
За да видим как да използваме правилно ThreadLocal , първо ще разгледаме пример, който не използва ThreadLocal , след което ще пренапишем нашия пример, за да използваме тази конструкция.
3. Съхраняване на потребителски данни в карта
Нека разгледаме програма, която трябва да съхранява специфичните за потребителя данни за контекст за даден потребителски идентификатор:
public class Context { private String userName; public Context(String userName) { this.userName = userName; } }
Искаме да имаме по една нишка на потребителски идентификатор. Ще създадем клас SharedMapWithUserContext, който реализира интерфейса Runnable . Внедряването в метода run () извиква някаква база данни чрез класа UserRepository, който връща Context обект за даден userId .
След това съхраняваме този контекст в ConcurentHashMap, въведен от userId :
public class SharedMapWithUserContext implements Runnable { public static Map userContextPerUserId = new ConcurrentHashMap(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContextPerUserId.put(userId, new Context(userName)); } // standard constructor }
Можем лесно да тестваме кода си, като създадем и стартираме две нишки за две различни userIds и твърдим, че имаме два записа в картата userContextPerUserId :
SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1); SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start(); assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);
4. Съхраняване на потребителски данни в ThreadLocal
Можем да пренапишем нашия пример, за да съхраним потребителския екземпляр Context, използвайки ThreadLocal . Всяка нишка ще има свой собствен екземпляр ThreadLocal .
Когато използваме ThreadLocal , трябва да бъдем много внимателни, защото всеки екземпляр на ThreadLocal е свързан с определена нишка. В нашия пример имаме специална нишка за всеки конкретен userId и тази нишка е създадена от нас, така че имаме пълен контрол върху нея.
Методът run () ще извлече потребителския контекст и ще го съхрани в променливата ThreadLocal, използвайки метода set () :
public class ThreadLocalWithUserContext implements Runnable { private static ThreadLocal userContext = new ThreadLocal(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContext.set(new Context(userName)); System.out.println("thread context for given userId: " + userId + " is: " + userContext.get()); } // standard constructor }
Можем да го тестваме, като стартираме две нишки, които ще изпълнят действието за даден userId :
ThreadLocalWithUserContext firstUser = new ThreadLocalWithUserContext(1); ThreadLocalWithUserContext secondUser = new ThreadLocalWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start();
След стартирането на този код ще видим на стандартния изход, че ThreadLocal е зададен за дадена нишка:
thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'} thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}
Виждаме, че всеки от потребителите има свой собствен контекст .
5. ThreadLocal s и Thread Pools
ThreadLocal предоставя лесен за използване API за ограничаване на някои стойности във всяка нишка. Това е разумен начин за постигане на безопасност на нишките в Java. Трябва обаче да бъдем особено внимателни, когато използваме ThreadLocal s и пулове от нишки заедно.
За да разберем по-добре възможното предупреждение, нека разгледаме следния сценарий:
- Първо, приложението заема нишка от пула.
- След това съхранява някои стойности, ограничени от нишки, в ThreadLocal на текущата нишка .
- След като текущото изпълнение завърши, приложението връща заетата нишка в пула.
- След известно време приложението заема същата нишка, за да обработи друга заявка.
- Тъй като приложението не е извършило необходимите почиствания последния път, то може да използва същите данни на ThreadLocal за новата заявка.
Това може да доведе до изненадващи последици при едновременно прилагани приложения.
Един от начините за решаване на този проблем е ръчното премахване на всеки ThreadLocal, след като приключим с използването му. Тъй като този подход се нуждае от строги прегледи на кода, той може да бъде склонен към грешки.
5.1. Разширяване на ThreadPoolExecutor
Както се оказва, възможно е да се разшири класът ThreadPoolExecutor и да се осигури изпълнение на персонализирана кука за методите beforeExecute () и afterExecute () . Пулът от нишки ще извика метода beforeExecute () , преди да изпълни каквото и да е, използвайки заимстваната нишка. От друга страна, той ще извика метода afterExecute () след изпълнение на нашата логика.
Следователно можем да разширим класа ThreadPoolExecutor и да премахнем данните ThreadLocal в метода afterExecute () :
public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor { @Override protected void afterExecute(Runnable r, Throwable t) { // Call remove on each ThreadLocal } }
Ако подадем заявките си към това изпълнение на ExecutorService , тогава можем да сме сигурни, че използването на ThreadLocal и пулове от нишки няма да въведе опасности за безопасността на нашето приложение.
6. Заключение
В тази бърза статия разглеждахме конструкцията ThreadLocal . Внедрихме логиката, която използва ConcurrentHashMap , споделена между нишките, за да съхранява контекста, свързан с определен userId. След това пренаписахме нашия пример, за да използваме ThreadLocal за съхраняване на данни, които са свързани с определен userId и с конкретна нишка.
Прилагането на всички тези примери и кодови фрагменти може да бъде намерено в GitHub.