什么是Threadlocal及Threadlocal的应用

什么是Threadlocal

在Java中,ThreadLocal是一个容器,它为每个线程提供一个独立的变量副本。每个线程都可以独立地改变其副本,而不会影响其他线程的副本。

Threadlocal的使用

实例代码

public class ThreadLocalExample {
    private static ThreadLocal<Integer> counter = new ThreadLocal<>();
    private static ThreadLocal<Integer> counter2 = new ThreadLocal<>();

    public static void main(String[] args) {
        // 创建并启动多个线程
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                int value1 = (int) (Math.random() * 100);
                counter.set(value1);
                int value2 = (int) (Math.random() * 100);
                counter2.set(value2);
                System.out.println(Thread.currentThread().getName() + " set counter to " + value1);
                System.out.println(Thread.currentThread().getName() + " set counter2 to " + value2);
                // 模拟一些业务逻辑
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                System.out.println(Thread.currentThread().getName() + " get counter: " + counter.get());
                System.out.println(Thread.currentThread().getName() + " get counter2: " + counter2.get());
            }).start();
        }
    }
}

执行结果

Thread-1 set counter to 82
Thread-4 set counter to 36
Thread-3 set counter to 98
Thread-2 set counter to 83
Thread-0 set counter to 11
Thread-1 set counter2 to 95
Thread-2 set counter2 to 17
Thread-4 set counter2 to 49
Thread-3 set counter2 to 89
Thread-0 set counter2 to 88
Thread-0 get counter: 11
Thread-2 get counter: 83
Thread-0 get counter2: 88
Thread-4 get counter: 36
Thread-4 get counter2: 49
Thread-2 get counter2: 17
Thread-1 get counter: 82
Thread-1 get counter2: 95
Thread-3 get counter: 98
Thread-3 get counter2: 89

Threadlocal的原理

一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。

ThreadLocal -> get

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

ThreadLocal -> Set

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

ThreadLocal为什么可能产生内存泄漏,如何避免?

如果说一个线程执行完毕,线程Thread随之被释放,那么value便不存在内存泄漏的问题。然而,我们一般会通过线程池的方式来复用Thread对象来节省资源,这就会导致一个Thread对象的生命周期会非常长,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。

因此,我们在使用完ThreadLocal变量后,要手动调用remove()方法来清理ThreadLocalMap

解决线程安全问题的途径

使用Threadlocal

  • 优点: 每一个线程绑定自己的值,自己用自己的,不跟别人争抢
  • 缺点: 每个线程绑定的值会额外占据内存空间

使用synchronized或者lock

  • 优点: 线程相互竞争同一份资源,不会额外占据内存空间
  • 缺点: 加锁会使程序执行由异步改为同步,降低执行效率

Threadlocal的应用

解决SimpleDateFormat线程不安全的问题

为什么线程不安全

一般来说,这种时间格式转换工具,我们都会做成一个公共工具类,如下所示:

public class DateUtil {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 
    public static  String formatDate(Date date)throws ParseException {
        return sdf.format(date);
    }
 
    public static Date parse(String strDate) throws ParseException{
 
        return sdf.parse(strDate);
    }
}

SimpleDateFormat线程不安全的原因是因为它的实现类java.text.SimpleDateFormat在内部使用了一个共享的Calendar对象,并在多线程环境下对它进行修改和访问,导致线程竞争和数据不一致的问题。

public class SimpleDateFormat extends DateFormat {
    // 省略其他代码...

    private final Calendar calendar;

    // 省略其他代码...

    public SimpleDateFormat(String pattern) {
        this(pattern, Locale.getDefault(Locale.Category.FORMAT));
    }

    public SimpleDateFormat(String pattern, Locale locale) {
        // 省略其他代码...
        this.calendar = new GregorianCalendar(locale);
        // 省略其他代码...
    }

    // 省略其他代码...

    @Override
    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
        // 省略其他代码...
        calendar.setTime(date);
        // 省略其他代码...
    }

    @Override
    public Date parse(String source, ParsePosition pos) {
        // 省略其他代码...
        calendar.clear();
        // 省略其他代码...
        // 解析 source 并设置到 calendar 对象中
        // 省略其他代码...
        return calendar.getTime();
    }

    // 省略其他代码...
}

从源码分析可以看出以下关键点:

SimpleDateFormat内部有一个共享的Calendar对象,用于解析和格式化日期。
Calendar对象在多个方法中被修改和访问,包括format和parse等方法。
在多线程环境中,多个线程同时调用format和parse等方法会导致对Calendar对象的并发读写操作,从而引发不一致的问题。
由于SimpleDateFormat的内部使用的Calendar对象是共享的,不同线程同时修改它会导致竞争条件。这可能导致解析和格式化的结果不正确,或者抛出异常。因此,SimpleDateFormat在多线程环境中是线程不安全的,需要采取线程安全的措施来保证正确的日期处理。例如,可以使用ThreadLocal来为每个线程创建独立的SimpleDateFormat对象,以避免线程安全问题。

使用ThreadLocal来保证SimpleDateFormat类在使用过程中的线程安全
public class DateUtil {
    private static ThreadLocal<DateFormat> dateFormat = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String format(Date date) {
        return dateFormat.get().format(date);
    }
}
通过拦截器和Threadlocal保存用户信息
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值