当访问共享变量时,往往需要加锁来保证数据同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程中访问数据,就不需要同步了。这种技术称为线程封闭。在Java语言中,提供了一些类库和机制来维护线程的封闭性,例如局部变量和ThreadLocal类
ThreadLocal类能使线程中的某个值与保存值的对象关联起来,它提供了get、set方法,这些方法为每个使用该变量的线程保存一份独立的副本,因此get总是set当前线程的set最新值。
ThreadLocal本地线程变量,线程自带的变量副本(实现了每一个线程副本都有一个专属的本地变量,主要解决的就是让每一个线程绑定自己的值,自己用自己的,不跟别人争抢。通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全的问题)
public class Test1 {
ThreadLocal longLocal = new ThreadLocal();
ThreadLocal stringLocal = new ThreadLocal();
public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final Test1 test = new Test1();
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread(() -> {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
});
thread1.start();
thread1.join();
System.out.println(test.getLong());
System.out.println(test.getString());
}
}
运行该程序,代码输出的结果为:
1
main
10
Thread-0
1
main
从这段代码可以看出在mian线程和thread1线程确实都保存着各自的副本,它们的副本各自不干扰。
来从源码的角度来解析ThreadLocal这个类,这个类存放在java.lang包,这个类有很多方法。
它内部又个ThreadLocalMap类,主要有set()、get()、setInitialValue 等方法。
首先来看下set方法,获取当前Thread的 map,如果不存在则新建一个并设置值,如果存在设置值,源码如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
跟踪createMap,可以发现它根据Thread创建来一个ThreadLocalMap。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
t.threadLocals为当前线程的一个变量,也就是ThreadLocal的数据都是存放在当前线程的threadLocals变量里面的,由此可见用ThreadLocal存放的数据是线程安全的。因为它对于不同的线程来,使用ThreadLocal的set方法都会根据线程判断该线程是否存在它的threadLocals成员变量,如果没有就建一个,有的话就存下数据。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap为ThreadLocal的一个内部类,源码如下:
static class ThreadLocalMap {
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。
在使用ThreadLocal的get方法之前一定要先set,要不然会报空指针异常。还有一种方式就是在初始化的时候调用initialValue()方法赋值。改造下之前的例子,代码如下:
public class Test2 {
ThreadLocal longLocal = new ThreadLocal(){
@Override
protected Long initialValue() {
return Thread.currentThread().getId();
}
};
ThreadLocal stringLocal = new ThreadLocal(){
@Override
protected String initialValue() {
return Thread.currentThread().getName();
}
};
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final Test2 test = new Test2();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread(() -> {
System.out.println(test.getLong());
System.out.println(test.getString());
});
thread1.start();
thread1.join();
System.out.println(test.getLong());
System.out.println(test.getString());
}
}
运行该程序,代码输出的结果为:
1
main
10
Thread-0
1
main
通常讲JDBC连接保存在ThreadLocal对象中,每个对象都有属于自己的连接,代码如下:
private static ThreadLocal connectionHolder
= new ThreadLocal() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
ThreadLocal维护的是一个map 这个map是线程的名称多为key 我们所有封闭的值作为value。
现在来使用ThreadLoad来存储一下用户信息。
public class RequestHolder {
private final static ThreadLocal requestHolder = new ThreadLocal<>();
public static void add(Long id) {
requestHolder.set(id);
}
public static Long getId() {
return requestHolder.get();
}
public static void remove() {
requestHolder.remove();
}
}
声明一个ThreadLoad对象用来存储ThreadLoad信息。
@Slf4j
public class HttpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
log.info("do filter, {}, {}", Thread.currentThread().getId(), request.getServletPath());
RequestHolder.add(Thread.currentThread().getId());
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
@Slf4j
public class HttpInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle");
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
RequestHolder.remove();
log.info("afterCompletion");
return;
}
}
定义filter的内容,我们在filter中将线程id加入到ThreadLoad中,然后在controller中获取,看看能不能获取的到。在线程执行完之后将ThreadLoad中的数据清空防止内存溢出。
@Controller
@RequestMapping("/threadLocal")
public class ThreadLocalController {
@RequestMapping("/test")
@ResponseBody
public Long test() {
return RequestHolder.getId();
}
}
最后我们用postman测试发现打印了线程id,ThreadLoad中变量值只要是一个线程中不管在哪个类中都是共享的。
/**
* 在对一些业务日志写入数据库的时候,日期调用了sdf的静态,导致了会报错或者日期乱了
* */
public class ThreadLocalDataUtils {
public static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
解决方案一:加入synchronized,用时间换空间,效率低
*/
/**
如果不加会导致线程安全问题,SimpleDateFormat类内部有一个Calendar对象引用,
SimpleDateFormat相关的日期信息,例如sdf.parse(dateStr),sdf.format(date)
诸如此类的方法参数传入的日期相关String,Date等等, 都是交由Calendar引用来储存的.
这样就会导致一个问题如果你的SimpleDateFormat是个static的,那么多个thread之间
就会共享这个SimpleDateFormat,同时也是共享这个Calendar引用(相当于买票案列)
*/
//public static synchronized Date parse(String stringDate) throws ParseException {
public static Date parse(String stringDate) throws ParseException {
System.out.println(sdf.parse(stringDate));
return sdf.parse(stringDate);
}
/***
* 解决方案二:使用ThreadLocal,用空间换时间,效率高
* ThreadLocal中变量副本会人手一份,每次使用完了threadLocal后都要将资源进行释放的处理
*/
public static final ThreadLocalsdfThreadLocal=
ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parseByThreadLocal(String stringDate) throws ParseException {
return sdfThreadLocal.get().parse(stringDate);
}
//3 DateTimeFormatter 代替 SimpleDateFormat
public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String formatForDateTime(LocalDateTime localDateTime) {
return DATE_TIME_FORMAT.format(localDateTime);
}
public static LocalDateTime parseForDateTime(String dateString) {
return LocalDateTime.parse(dateString,DATE_TIME_FORMAT);
}
public static void main(String[] args) throws Exception{
for (int i = 1; i <=3; i++) {
new Thread(()->{
try {
//ThreadLocalDataUtils.parse("2021-03-30 11:20:30");
//System.out.println(ThreadLocalDataUtils.parseByThreadLocal("2021-03-30 11:20:30"));
System.out.println(ThreadLocalDataUtils.parseForDateTime("2021-03-30 11:20:30"));
// System.out.println(ThreadLocalDataUtils.formatForDateTime(LocalDateTime.now()));
} catch (Exception e) {
e.printStackTrace();
}finally {
ThreadLocalDataUtils.sdfThreadLocal.remove();
}
},String.valueOf(i)).start();
}
}
}