参考文章
我的写法就类似于下面这种,将SimpleDateFormat作为了静态成员变量,大佬的建议就是提取一个方法,然后通过该方法获取SimpleDateFormat对象。
1.1 parse 先看个parse案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Test { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss" ); private static final String str = "2021-08-30 12:23:11" ; public static void main (String[] args) { Runnable runnable = new Runnable() { @Override public void run () { try { System.out.println(sdf.parse(str)); } catch (ParseException e) { e.printStackTrace(); } } }; for (int i = 0 ; i < 10 ; i++) { new Thread(runnable).start(); } } }
运行结果如图
通过查看源码来找出这个问题的原因。
左键点进去,发现是调用了parse(String source)
方法
再次左键点进去,发现是调用了自身的抽象方法,那么就找这个抽象方法的实现类,也就是SimpleDateFormat。
通过阅读文档,大概能了解到,如果将SimpleDateFormat作为静态变量,那么就相当于是一个共享变量,而其父类DateFormat中有一个成员变量Calendar。此时,多个线程进行操作时,因为SimpleDateFormat已经作为共享变量了,所以多线程中使用的Calendar其实是同一个,此时进行clear方法,之后再进行set时,会出现各种问题。比如:
a线程赋值之后,b线程给clear掉,此时a线程结果不对 a线程赋值之后,b线程也赋值,a线程结果不对 a线程执行到一半的时候,b线程也执行,a线程部分数值不对 看网上说,ParsePositioin也会存在线程安全问题,但是我发现源码中,每次执行时,ParsePosition都是重新new的,所以应该不存在该问题。
format就很好理解了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class Test { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss" ); public static void main (String[] args) { Runnable runnable = new Runnable() { @Override public void run () { Date date = randomDate("2010-09-20" , "2021-09-22" ); System.out.println(Thread.currentThread()+":" +date+"->" +sdf.format(date)); } }; for (int i = 0 ; i < 10 ; i++) { new Thread(runnable).start(); } } public static Date randomDate (String beginDate, String endDate) { try { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd" ); Date start = format.parse(beginDate); Date end = format.parse(endDate); if (start.getTime() >= end.getTime()) { return null ; } long date = random(start.getTime(), end.getTime()); return new Date(date); } catch (Exception e) { e.printStackTrace(); } return null ; } public static long random (long begin, long end) { long rtn = begin + (long ) (Math.random() * (end - begin)); if (rtn == begin || rtn == end) { return random(begin, end); } return rtn; } }
运行结果
查看源码
这个就很容易想了,多个线程执行时,a线程设置了时间,b线程又设置另一个时间,就会导致问题。
1.3 解决方案 三种种主流的解决方案
使用局部变量,每次调用,都有一个对象。也就是我现在的做法,将new对象的操作提取个方法出来。这个做法有个问题,频繁地创建对象,对象在方法内不被外部调用时,方法结束,就会被回收。频繁的进行创建对象、回收对象,会影响性能。 使用 ThreadLocal。使用 ThreadLocal 实现每个线程都可以得到单独的一个SimpleDateFormat的实例。与使用局部变量的不同的是,1个请求进来是1个线程,如果是使用局部变量来处理,那么每个方法里,都需要new一个SimpleDateFormat,如果三个方法用到,就有三个对象;而用ThreadLocal的话,只会产生1个对象,1个线程1个对象。 java8及其以上,可以使用DateTimeFormatter,线程安全的 使用ThreadLocal ,参考Java开发手册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Test { private static final ThreadLocal<SimpleDateFormat> sdfThreadLocal = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue () { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ); } }; public static void main (String[] args) { SimpleDateFormat sdf = sdfThreadLocal.get(); String strDate = sdf.format(new Date()); System.out.println(strDate); } }
使用DateTimeFormatter
1 2 3 4 5 6 7 8 9 10 11 public class Test { private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); public static void main (String[] args) { LocalDateTime now = LocalDateTime.now(); String strDate = formatter.format(now); System.out.println(strDate); } }
二、ThreadLocal 2.1 概念 ThreadLocal类用来提供线程内部的局部变量,让这些变量在多线程访问时,能保证各个线程里的变量相对独立于其他线程内的变量。
简单理解
在多线程并发的场景下 通过ThreadLocal在同一线程不同组件中传递公共变量 每个线程的变量都是独立的,不会互相影响 2.2 使用 常用方法
set():将变量绑定到当前线程中 get():获取当前线程绑定的变量 需求
在多线程并发的场景下,实现线程隔离,也就是每个线程中的变量都是相互独立的。设置变量、取出变量都是同一个。
线程A:操作的是变量1
线程B:操作的是变量2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Test { private String content; public String getContent () { return content; } public void setContent (String content) { this .content = content; } public static void main (String[] args) { Test test = new Test(); for (int i=0 ;i<5 ;i++){ Thread thread=new Thread(new Runnable() { @Override public void run () { test.setContent(Thread.currentThread().getName()+"的数据" ); System.out.println("==========" ); System.out.println(Thread.currentThread().getName()+"----->" +test.getContent()); } }); thread.setName("线程" +i); thread.start(); } } }
这就产生了线程安全问题。
解决办法就是将content绑定到ThreadLocal,这样就能保证每个线程拿到的变量都是相互独立的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class Test { ThreadLocal<String> tl=new ThreadLocal<>(); private String content; public String getContent () { return tl.get(); } public void setContent (String content) { tl.set(content); } public static void main (String[] args) { Test test = new Test(); for (int i=0 ;i<5 ;i++){ Thread thread=new Thread(new Runnable() { @Override public void run () { test.setContent(Thread.currentThread().getName()+"的数据" ); System.out.println("==========" ); System.out.println(Thread.currentThread().getName()+"----->" +test.getContent()); } }); thread.setName("线程" +i); thread.start(); } } }
2.3 ThreadLocal与synchronized 用synchronized同步代码块,也能达到同样的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Test { private String content; public String getContent () { return content; } public void setContent (String content) { this .content = content; } public static void main (String[] args) { Test test = new Test(); for (int i=0 ;i<5 ;i++){ Thread thread=new Thread(new Runnable() { @Override public void run () { synchronized (Test.class ) { test.setContent(Thread.currentThread().getName()+"的数据" ); System.out.println("==========" ); System.out.println(Thread.currentThread().getName()+"----->" +test.getContent()); } } }); thread.setName("线程" +i); thread.start(); } } }
synchronized加了锁,并发执行变成了只能排队进行访问,失去了并发性,效率就会降低。
synchronized ThreadLocal 原理 同步机制采用时间换空间 的方式,只提供了一份变量,让不同线程排队访问 ThreadLocal采用空间换时间 的方式,为每一个线程都提供了一份变量副本,从而实现同时访问而相互不干扰。 侧重点 多个线程间访问资源同步 多个线程间数据相互隔离
关于这个变量副本,就相当于每个线程中new了一个变量,并且仅有一个变量。
2.4 应用场景 1.将数据库连接通过ThreadLocal绑定到线程上,从而能保证成功回滚
2.每个线程内保存一个共享的对象,比如SimpleDateFormat,就不用每用一次就new一次了
2.5 实现原理 JDK1.8以前的ThreadLocal
每个ThreadLocal都创建一个Map,然后用当前线程作为Key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果
JDK1.8以后的ThreadLocal
每个Thread内部有一个ThreadLocalMap,ThreadLocal对象本身作为Key,要存储的局部变量作为value。具体的过程
每个Thread线程内部都有一个Map,即ThreadLocalMap。如果多个线程在操作同一个threadLocal里的值,那实际上就是每个Thread里面的都有一个key为threadLocal对象,value为变量副本的map。这边动手写源码会好理解不少,可以参照下方的源码理解。 Map里面存储ThreadLocal对象(key)和线程的变量副本(value) Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置线程的变量值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public class ThreadLocalTest { ThreadLocal<String> tl=new ThreadLocal<>(); private String content; public String getContent () { return tl.get(); } public void setContent (String content) { tl.set(content); } public static void main (String[] args) { ThreadLocalTest test = new ThreadLocalTest(); System.out.println(test.tl); for (int i=0 ;i<5 ;i++){ @SuppressWarnings ("all" ) Thread thread=new Thread(new Runnable() { @Override public void run () { String content=Thread.currentThread().getName()+ "的数据" +"======获取threadLocal====>" + test.tl; test.setContent(content); System.out.println(Thread.currentThread().getName()+ "----->" + test.getContent()); } }); thread.setName("线程" +i); thread.start(); } Thread[] threads=new Thread[5 ]; Thread.currentThread().getThreadGroup().enumerate(threads); System.out.println(Arrays.asList(threads)); } }
如此设计的好处
每个Map存储的Entry数量减少,Entry可以理解成键值对的对象。以前的方式是Map的key是存储的线程,线程越多,Entry键值对越多。现在将Map放到Thread里面,Entry数量减少。实际开发中,ThreadLocal数量往往少于Thread数量 Thread销毁的时候,ThreadLocalMap也会相应的进行销毁,减少了内存的使用。 单纯的文字描述看不太懂的话,可以参照ThreadLocal的get方法源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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(); }
三、TransmittableThreadLocal meethigher/transmittablethreadlocal-test: 由浅入深,简单使用transmittablethreadlocal
参考文章
3.1 ThreadLocal存在的问题 如果想要在父线程中,通过ThreadLocal赋值,子线程中取get的时候,是获取不到的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static void main (String[] args) { ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); threadLocal.set(1 ); new Thread(new Runnable() { @Override public void run () { Integer integer = threadLocal.get(); threadLocal.remove(); System.out.println(integer); } }).start();
3.2 InheritableThreadLocal InheritableThreadLocal可以解决在父子线程中,变量共享。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static void main (String[] args) { ThreadLocal<Integer> threadLocal=new InheritableThreadLocal<>(); threadLocal.set(1 ); new Thread(new Runnable() { @Override public void run () { Integer integer = threadLocal.get(); threadLocal.remove(); System.out.println(integer); } }).start(); }
但是,如果使用线程池的话,由于线程池会存在复用的情况,就会读取数据混乱的情况。
类似于下面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public static void main (String[] args) throws InterruptedException { ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>(); ThreadPoolExecutor executor = getExecutor(); CountDownLatch firstCountDownLatch = new CountDownLatch(1 ); CountDownLatch secondCountDownLatch = new CountDownLatch(1 ); threadLocal.set(1 ); executor.execute(() -> { System.out.println("子线程获取" + threadLocal.get()); threadLocal.remove(); System.out.println("异步任务1" ); firstCountDownLatch.countDown(); }); firstCountDownLatch.await(); System.out.println("父线程获取" + threadLocal.get()); threadLocal.set(2 ); executor.execute(() -> { System.out.println("子线程获取" + threadLocal.get()); threadLocal.remove(); System.out.println("异步任务2" ); secondCountDownLatch.countDown(); }); secondCountDownLatch.await(); System.out.println("父线程获取" + threadLocal.get()); executor.shutdown(); }
3.3 TransmittableThreadLocal 通过TransmittableThreadLocal就可以解决上面存在的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public static void main (String[] args) throws InterruptedException { ThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>(); ThreadPoolExecutor executor = getExecutor(); CountDownLatch firstCountDownLatch = new CountDownLatch(1 ); CountDownLatch secondCountDownLatch = new CountDownLatch(1 ); threadLocal.set(1 ); executor.execute(TtlRunnable.get(() -> { System.out.println("子线程获取" + threadLocal.get()); threadLocal.remove(); System.out.println("异步任务1" ); firstCountDownLatch.countDown(); })); firstCountDownLatch.await(); System.out.println("父线程获取" + threadLocal.get()); threadLocal.set(2 ); executor.execute(TtlRunnable.get(() -> { System.out.println("子线程获取" + threadLocal.get()); threadLocal.remove(); System.out.println("异步任务2" ); secondCountDownLatch.countDown(); })); secondCountDownLatch.await(); System.out.println("父线程获取" + threadLocal.get()); executor.shutdown(); }