ThreadLocal的应用场景

发布于:2024-10-16 ⋅ 阅读:(11) ⋅ 点赞:(0)

ThreadLocal介绍

        ThreadLocal为每个线程都提供了变量的副本,使得每个线程访问各自独立的对象,这样就隔离了多个线程对数据的共享,使得线程安全。ThreadLocal有如下方法:

方法声明  描述
public void set(T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public void remove() 移除当前线程绑定的局部变量
protected Object initialValue() 初始化值

ThreadLocal应用场景

场景一:每个线程需要一个独享的对象(典型的需要使用的类就是 SimpleDateFormat,因为它是线程不安全的)

package com.gingko.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//线程不安全
public class DateUtils1 {

    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String formatDate(long seconds) {
        Date date = new Date(seconds*1000);
        String format = dateFormat.format(date);
        return format;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for(int i=0;i<100;i++) {
            int finalI = i;
            /**
             * 10个线程共享1个SimpleDateFormat,会发生线程安全问题,运行结果出现相同的时间
             */
            executorService.submit(()-> {
                try {
                    String formatDate = formatDate(finalI);
                    System.out.println(formatDate);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

运行结果:

分析:线程池创建了10个线程处理100个任务,10个线程共享1个SimpleDateFormat,发生了线程安全问题,运行结果出现相同的时间。

改进版本:加锁机制

package com.gingko.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//使用锁机制,可以解决线程安全问题,多线程时等待,执行效率较低
public class DateUtils2 {

    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    //使用锁机制,多线程时等待,执行效率较低
    public static synchronized String formatDate(long seconds) {
        Date date = new Date(seconds*1000);
        String format = dateFormat.format(date);
        return format;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for(int i=0;i<100;i++) {
            int finalI = i;
            /**
             * 10个线程共享1个SimpleDateFormat
             */
            executorService.submit(()-> {
                try {
                    String formatDate = formatDate(finalI);
                    System.out.println(formatDate);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

运行结果:

分析:线程池创建了10个线程处理100个任务,10个线程共享1个SimpleDateFormat,在formatDate方法上加了锁,使得多个线程同时执行此方法时需要排队等待获取锁,执行结果没有问题,但是由于要等待获取锁,执行效率低。

改进版本:使用ThreadLocal

package com.gingko.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//使用threadlocal,使得每个线程有各个独立的SimpleDateFormat
public class DateUtils3 {

    //使用threadlocal,使得每个线程有各个独立的SimpleDateFormat
    private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(()-> {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    });

    public static String formatDate(long seconds) {
        Date date = new Date(seconds*1000);
        String format = dateFormatThreadLocal.get().format(date);
        return format;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for(int i=0;i<100;i++) {
            int finalI = i;
            /**
             * 10个线程有各自独立的SimpleDateFormat,不会发生线程安全问题
             */
            executorService.submit(()-> {
                try {
                    String formatDate = formatDate(finalI);
                    System.out.println(formatDate);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

 运行结果:

分析:线程池创建了10个线程处理100个任务,10个线程独占各自的SimpleDateFormat,执行结果没有问题,没有锁机制,执行效率高。

场景二:替代参数链传递

上图中通过前台获取到用户信息后,一路向下传递,假设方法A、B、C都需要用户信息,一种方式是A、B、C方法都接收用户信息作为入参(非常繁琐),一种方式是将用户信息放入ThreadLocal中,这样线程链上的所有方法都可以获取到用户信息,不用在方法A、B、C显式的指定User Info的入参,类似的应用场景还有:前台传递的分页参数等。

package com.gingko.threadlocal;

import com.gingko.entity.Student;

public class TransferParam {

    //线程中放入student信息,整个线程链路上都可以获取student信息
    private static ThreadLocal<Student> studentThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        TransferParam transferParam = new TransferParam();
        transferParam.setParam();
    }
    public void setParam() {
        Student student = new Student("1","张三",18,"001");
        studentThreadLocal.set(student);
        new ServiceA().getParam();
    }

    class ServiceA {
        public void getParam() {
            Student student = studentThreadLocal.get();
            System.out.println("ServiceA:" + student);
            new ServiceB().getParam();
        }
    }
    class ServiceB {
        public void getParam() {
            Student student = studentThreadLocal.get();
            System.out.println("ServiceB:" + student);
            //线程链路的最后删除threadlocal信息,防止发生内存泄露
            studentThreadLocal.remove();
        }
    }
}

运行结果:

 从结果上看出:在方法setParam设置了ThreadLocal的变量student,在其线程调用的链条上方法:ServiceA.getParam 和ServiceB.getParam 都可以获取到student

注意:在线程链最后的方法上记得调用ThreadLocal的remove方法,不然会出现内存泄漏的风险,这块内容后续的章节会介绍。