ThreadLocal详解

    科技2022-07-15  121

    第一种应用场景主要解决的是,工具类线程不安全,需要去解决工具类线程不安全的问题。 第二种应用场景主要解决的是,参数传递麻烦的问题。 下面进行的是SimpleDateFormat进行之路

    package threadlocalpratice; import java.text.SimpleDateFormat; import java.util.Date; public class ThreadLocalNormalUsage00 { public String data(int second){ //参数的单位是毫秒,从1970-01-01 00:00:00开始 Date date = new Date(1000 * second); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); return simpleDateFormat.format(date); } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { String data = new ThreadLocalNormalUsage00().data(100000); System.out.println(data); } }).start(); new Thread(new Runnable() { @Override public void run() { String data = new ThreadLocalNormalUsage00().data(100); System.out.println(data); } }).start(); } }

    这是两个线程用SimpleDateFormat

    package threadlocalpratice; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 1000个打印日期的任务,用线程池来执行 */ public class ThreadLocalNormalUsage01 { public static ExecutorService threadPool = Executors.newFixedThreadPool(10); public String data(int second){ //参数的单位是毫秒,从1970-01-01 00:00:00开始 Date date = new Date(1000 * second); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); return simpleDateFormat.format(date); } public static void main(String[] args) { for (int i = 0; i < 1000; i++) { int finalI = i; //将任务提交到线程池中 threadPool.submit(new Runnable() { @Override public void run() { String data = new ThreadLocalNormalUsage01().data(1000 + finalI); System.out.println(data); } }); } threadPool.shutdown(); } }

    这是1000个线程,使用线程池的方法来提高效率。这里的话发现SimpleDateFormat工具类,在每个线程中都被创建出来,浪费资源。

    package threadlocalpratice; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 1000个打印日期的任务,用线程池来执行 * 由于1000个线程共用一个simpleDateFormat对象,会浪费, * 所以把它写在类对象上。让1000个线程共用一个SimpleDateFormat对象 * 发现共用一个SimpleDateFormat会导致线程不安全 */ public class ThreadLocalNormalUsage02 { public static ExecutorService threadPool = Executors.newFixedThreadPool(10); static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); public String data(int second){ //参数的单位是毫秒,从1970-01-01 00:00:00开始 Date date = new Date(1000 * second); return simpleDateFormat.format(date); } public static void main(String[] args) { for (int i = 0; i < 1000; i++) { int finalI = i; //将任务提交到线程池中 threadPool.submit(new Runnable() { @Override public void run() { String data = new ThreadLocalNormalUsage02().data(1000 + finalI); System.out.println(data); } }); } threadPool.shutdown(); } }

    这里让1000个线程都共用一个SimpleDateFormat对象。

    package threadlocalpratice; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 1000个打印日期的任务,用线程池来执行 * 用加锁的方法来保证线程安全 */ public class ThreadLocalNormalUsage03 { public static ExecutorService threadPool = Executors.newFixedThreadPool(10); static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); public String data(int second) { //参数的单位是毫秒,从1970-01-01 00:00:00开始 Date date = new Date(1000 * second); String s = null; //选取simpleDateFormat来作为同步监控器 synchronized (simpleDateFormat) { s = simpleDateFormat.format(date); } return s; } public static void main(String[] args) { for (int i = 0; i < 1000; i++) { int finalI = i; //将任务提交到线程池中 threadPool.submit(new Runnable() { @Override public void run() { String data = new ThreadLocalNormalUsage03().data(1000 + finalI); System.out.println(data); } }); } threadPool.shutdown(); } }

    上面代码用加锁的方法来解决线程不安全的问题。但是,发现加锁的时候,效率比较低了。 给每一个线程都添加ThreadLocal 就是复制一份副本。

    package threadlocalpratice; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 1000个打印日期的任务,用线程池来执行 * 由于1000个线程共用一个simpleDateFormat对象,会浪费, * 使用ThreadLocal方法,由于只有10个线程,所以,其实只要同一个simpleDateFormat对象 * 复制9份就可以了 * */ public class ThreadLocalNormalUsage05 { public static ExecutorService threadPool = Executors.newFixedThreadPool(10); ThreadLocal<SimpleDateFormat> simpleDateFormat = new ThreadLocal(){ @Override protected SimpleDateFormat initialValue(){ return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); } }; public String data(int second){ //参数的单位是毫秒,从1970-01-01 00:00:00开始 Date date = new Date(1000 * second); return simpleDateFormat.get().format(date); } public static void main(String[] args) throws InterruptedException { HashSet set = new HashSet(); for (int i = 0; i < 1000; i++) { int finalI = i; //将任务提交到线程池中 threadPool.submit(new Runnable() { @Override public void run() { String data = new ThreadLocalNormalUsage05().data(1000 + finalI); System.out.println(data); set.add(data); } }); } threadPool.shutdown(); Thread.sleep(2000); System.out.println(threadPool.isTerminated()); System.out.println(set.size()); } }

    ThreadLocal 用途二: 传参的过程过于麻烦。不合适。 目标:每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦。 假设用UserMap 但是,这个方法也有问题: 当多线程同时工作时,我们需要保证线程安全,可以用synchronized,也可以用ConcurrentHashMap,但是,无论使用什么,都会对性能有所影响。 用ThreadLocal 不会影响性能

    package threadlocalpratice; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 演示ThreadLocal的应用场景2,进行数据的传输 */ public class ThreadLocalUsage06 { public static void main(String[] args) { ExecutorService threadPool = Executors.newFixedThreadPool(10); for (int i = 0; i < 100; i++) { int finalI = i; threadPool.submit(new Runnable() { @Override public void run() { new Service1( " " + finalI).process(); } }); } threadPool.shutdown(); } } class Service1 { User user; public Service1(String name){ user = new User(name); } public void process() { //对对象中的数据进行设置 UserContextHolder.holder.set(user); new Service2().process(); new Service3().process(); } } class Service2{ public void process(){ //这时候就可以得到User了 User user = UserContextHolder.holder.get(); System.out.println( user.name +" Service2拿到的user " + user.name); } } class Service3{ public void process(){ //这时候就可以得到User了 User user = UserContextHolder.holder.get(); System.out.println( user.name + " Service3拿到的user " + user.name); } } class UserContextHolder { //使用方式2时,不需要进行初始化 public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User { String name; public User(String name) { this.name = name; } }

    总结

    ThreadLocal的好处: 源码分析: 每一个Thread线程中,都会有一个ThreadLocalMap类, 里面会存储很多ThreadLocal 为什么需要map因为一个线程中可能会有多个ThreadLocal。 ThreadLocal:方法 下面在源码中进行查看,再没重写之前默认返回为null,重写后,调用get方法时就会执行该方法。 如果第一次调用get方法时,就会调用InitialValue。如果,之前调用了set方法,那么就不会去调用initialValue方法了。 原因如下: 由于map中不为null 因为你给它设置了值,所以,它就不会执行setInitialValue方法了。而拿到的result是set设置进行的。

    分析get方法: t代表当前线程: getMap返回的是当前线程的threadLocals,这个threadLocals代表的是每个线程都有的ThreadLocalMap 如果map为空,就是没有set过 那么就是执行setInitialValue方法,这个方法代表的就是初始化InitialValue。如果map不为空(已经初始化过或者set过)。注意,这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中。 如果不为null 就以当前的ThreadLocal作为key(this)去找value,然后来进行返回。 下面分析的是set方法 相比之下,set就简单得多了:如果map不为空,那么就直接加入。如果为空,就新建,新建如下: remove:方法 这里先获取ThreadLocal中的ThreadLocalMap 如果这个map不为空,就进行删除,但是,它不会把所有的都删除,它会把删除this,这个this代表的是调用它的ThreadLocal这个key所代表的key-value键值对。只能删除一个。 ThreadLocalMap类是Thread里面的一个成员变量。threadLocals ,类型是ThreadLocalMap 其有一个属性是: 这个可以认为是一个map,键值对: 键:ThreadLocal 值:实际需要的成员变量,比如uesr或者simpleDateFormat对象 但是,它和map有一点不同,hashmap碰到冲突会用链表的方法,或者红黑树,而它是进行找空的值,而进行填入。 总结一下 在每一个Thread中,都是会有一个ThreadLocalMap,里面会有一个个键值对。存入的key是ThreadLocal 类型,值的话,所有类型都可以。但调用get时,如果在ThreadLocalMap中没有,就会根据初始化函数进行创建。 ThreadLocal注意点 1.内存泄漏 这个key ThreadLocal代表的是弱引用。 而 value = v 代表的就是强引用; 但是, 因为value和Thread之间是强引用。所以,比如说我创建了十个对象,可以已经不用了,但是,还在内存中。 就是说,随着时间的增加,弱引用会被垃圾回收栈回收,但是,强引用会一直都在。这个value会一直在内存中。 在线程关闭之前要进行线程回收。不在使用的时候,主动调用remove()方法 ThreadLocal 碰到的空指针问题 package threadlocalpratice;

    public class ThreadLocalNPE { ThreadLocal longThreadLocal = new ThreadLocal<>(); public void set(){ longThreadLocal.set(Thread.currentThread().getId()); } public long get(){ return longThreadLocal.get(); }

    public static void main(String[] args) { ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE(); //这行语句会导致空指针异常,理论上,ThreadLocalNPE.longThreadLocal.get返回的是null。不会返回空指针异常的 System.out.println(threadLocalNPE.get()); Thread thread = new Thread( new Runnable() { @Override public void run() { threadLocalNPE.set(); long l = threadLocalNPE.get(); System.out.println(l); } } ); //子线程的id thread.start(); }

    } 这里会导致空指针异常: 原因如下: longThreadLocal.get()返回的是null,是Long类型的,但是想给他转化为long类型,所以,才导致了空指针类型报错。解决措施:只要把long改成Long即可,就不会有装箱拆箱问题了。 注意点:

    Processed: 0.010, SQL: 8