【并发编程】ThreadLocal:如何优雅的解决SimpleDateFormat多线程安全问题(1)

第二次:

当前日期:2018-10-10 10:10:10

当前日期:2018-10-10 10:10:10

当前日期:2019-11-11 11:11:11

Process finished with exit code 0

第三次:

当前日期:2020-10-10 10:10:10

当前日期:2018-10-10 10:10:10

当前日期:2018-10-10 10:10:10

Process finished with exit code 0

我们发现String转Date的时候有两个线程直接报错,Date转String虽然不会报错,但是日期格式全部错乱,为什么平时使用的时候不会出现问题,一到性能测试的时候就会发生这种问题,小明表示快要崩溃了。

在这里插入图片描述

SimpleDateFormat出现bug的原因

=======================================================================================

其实了解过多线程的人都知道SimpleDateFormat是线程不安全的,但是为什么是线程不安全的,大家都知道吗?我们一起来看一下。

首先parse源码分析:

public Date parse(String source) throws ParseException

{

ParsePosition pos = new ParsePosition(0);

Date result = parse(source, pos);

if (pos.index == 0)

throw new ParseException(“Unparseable date: “” + source + “”” ,

pos.errorIndex);

return result;

}

进入:parse(source, pos);,然后注意到下面这段代码

try {

parsedDate = calb.establish(calendar).getTime();

// If the year value is ambiguous,

// then the two-digit year == the default start year

if (ambiguousYear[0]) {

if (parsedDate.before(defaultCenturyStart)) {

parsedDate = calb.addYear(100).establish(calendar).getTime();

}

}

}

Calendar establish(Calendar cal) {

boolean weekDate = isSet(WEEK_YEAR)

&& field[WEEK_YEAR] > field[YEAR];

if (weekDate && !cal.isWeekDateSupported()) {

// Use YEAR instead

if (!isSet(YEAR)) {

set(YEAR, field[MAX_FIELD + WEEK_YEAR]);

}

weekDate = false;

}

cal.clear();

// Set the fields from the min stamp to the max stamp so that

// the field resolution works in the Calendar.

for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {

for (int index = 0; index <= maxFieldIndex; index++) {

if (field[index] == stamp) {

cal.set(index, field[MAX_FIELD + index]);

break;

}

}

}

if (weekDate) {

int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;

int dayOfWeek = isSet(DAY_OF_WEEK) ?

field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();

if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {

if (dayOfWeek >= 8) {

dayOfWeek–;

weekOfYear += dayOfWeek / 7;

dayOfWeek = (dayOfWeek % 7) + 1;

} else {

while (dayOfWeek <= 0) {

dayOfWeek += 7;

weekOfYear–;

}

}

dayOfWeek = toCalendarDayOfWeek(dayOfWeek);

}

cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);

}

return cal;

}

我们深入到**calb.establish(calendar).getTime();**中,发现 establish() 中存在着一行比较秀的代码:

cal.clear();

public final void clear()

{

for (int i = 0; i < fields.length; ) {

stamp[i] = fields[i] = 0; // UNSET == 0

isSet[i++] = false;

}

areAllFieldsSet = areFieldsSet = false;

isTimeSet = false;

}

由于calendar这个参数是由调用方传递进来的,而调用方parse()拿的是当前类的成员变量。

protected Calendar calendar;

然而我们都知道,成员变量在多线程情况下如果没有锁的加持,是很容易出现线程安全问题的,他这里是先执行的clear,在重新获取时间

public final Date getTime() {

return new Date(getTimeInMillis());

}

public long getTimeInMillis() {

if (!isTimeSet) {

updateTime();

}

return time;

}

ressWarnings(“ProtectedField”)

protected long time;

getTime最终返回的是成员变量,这个是之前已经设置了值的属性,然而当开启多线程的时候,很有可能导致第一个线程执行了getTime之后第二个线程有执行了clear,由于这些变量都是成员变量,所以他们是共享的,bug就发生了。

format日期错乱的原因和这个类似,这里就不做过多说明了,感兴趣的小哥哥小姐姐可以翻开源码撸一下。

如何解决SimpleDateFormat多线程安全问题

==========================================================================================

局部变量


既然成员变量会发生线程安全问题,那将SimpleDateFormat设置成为局部变量那不就没问题了吗,却是是这样,但如果需要修改格式化的模式,改动量是非常大,因为你需要将所有涉及到的局部变量都修改一遍,而单例只需要修改一次,这个看情况而定

使用SimpleDateFormat方法时加锁


这也是一种解决的思路,比如:synchronized,大家都知道,加锁会降低程序的效率,除非必要情况,否者时不建议直接使用锁来解决的。

使用ThreadLocal


ThreadLocal不知道大家了解过没有,他是一种解决多线程并发问题简单有效的方式,通过线程和变量绑定的方式,让线程与线程之间不存在变量共享的问题,自然而然就解决了多线程并发的问题。

ThreadLocal介绍

============================================================================

JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量。

举个生活中的例子,人有三急,而厕所只有一坑位,所以公司的上百人都会争先恐后的争夺那一个坑位的使用权,并且在争夺过程中还有可能出现事故,公司老总有一次也想蹲坑,但是发现已经有很多人正在为了那一个坑位抢的死去活来,导致他不能及时排泄而。。。。。。

公司老总整理了情绪之后,于时吩咐秘书:你赶紧去安排一下,厕所的坑位增加到一百个,每人一个,看还有没有人抢。

这个例子就有点类似与我们程序中的多线程,很多线程都在抢同一个资源,在抢来抢去的时候难免发生意外情况,也就是程序中的线程安全问题,所以有没有一种给每个线程都分配对应的变量,让他们不用抢来抢去?这个时候ThreadLocal站了出来,我是土豪,我给你们每个线程都分配一个只属于你们自己的资源,省的你们抢来抢去,打扰我泡妞。

ThreadLocal使用demo

================================================================================

package com.ymy.test;

import java.util.Random;

public class MyLocalThread {

private static Random random = new Random();

private static ThreadLocal t = ThreadLocal.withInitial(

()->random.nextInt(10)+1

);

private static Integer get(){

return t.get();

}

public static void main(String[] args) {

Thread t1 = new Thread(() -> {

Integer num = get();

System.out.println(“线程名:”+Thread.currentThread().getName()+" "+ num);

num = get();

System.out.println(“线程名:”+Thread.currentThread().getName()+" 第二次获取 "+ num);

});

Thread t2 = new Thread(() -> {

Integer num = get();

System.out.println(“线程名:”+Thread.currentThread().getName()+" "+ num);

num = get();

System.out.println(“线程名:”+Thread.currentThread().getName()+" 第二次获取 "+ num);

});

Thread t3 = new Thread(() -> {

Integer num = get();

System.out.println(“线程名:”+Thread.currentThread().getName()+" "+ num);

num = get();

System.out.println(“线程名:”+Thread.currentThread().getName()+" 第二次获取 "+ num);

});

t1.start();

t2.start();

t3.start();

try {

t1.join();

t2.join();

t3.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“执行完毕”);

}

}

上面的代码很简单,ThreadLocal为三个线程生成三个随机数,然后三个线程分别去获取生成的随机数,看看会发生什么结果?

线程名:Thread-0 3

线程名:Thread-0 第二次获取 3

线程名:Thread-1 1

线程名:Thread-2 10

线程名:Thread-1 第二次获取 1

线程名:Thread-2 第二次获取 10

执行完毕

Process finished with exit code 0

我们来看一下三个线程分别生成的随机数:

第一个线程(Thread-0):3

第二个线程(Thread-1):1

第三个线程(Thread-2):10

我们看到三个线程生成的数据:3、1、10,都是随机的,但是这并不能说明他就是线程安全的,所以我这里还特意在这三个线程中重复获取了一次随机数,我们我们发现:

第一个线程第二次获取(Thread-0):3

第二个线程第二次获取(Thread-1):1

第三个线程第二次获取(Thread-2):10

你们是不是发现什么了?没错,那就是每个线程的第二次获取的数据和第一次是相同的,而且不会和其他线程发生任何错乱,这就是ThreadLocal的神奇之处。

ThreadLocal源码探索

==============================================================================

我们先看看ThreadLocal是如何被创建的

private static ThreadLocal t = ThreadLocal.withInitial(()->random.nextInt(10)+1);

这行代码是上面demo中ThreadLocal创建方式,通过ThreadLocal.withInitial来创建,我们一起来看一下withInitial方法。

/**

  • Creates a thread local variable. The initial value of the variable is

  • determined by invoking the {@code get} method on the {@code Supplier}.

  • @param the type of the thread local’s value

  • @param supplier the supplier to be used to determine the initial value

  • @return a new thread local variable

  • @throws NullPointerException if the specified supplier is null

  • @since 1.8

*/

public static ThreadLocal withInitial(Supplier<? extends S> supplier) {

return new SuppliedThreadLocal<>(supplier);

}

这是jdk1.8才支持的创建方式,以前的版本请不要这么使用,这个方法表示:创建线程局部变量。变量的初始值是通过调用get()方法确定的。

那我们在一起来看看get()方法

/**

  • Returns the value in the current thread’s copy of this

  • thread-local variable. If the variable has no value for the

  • current thread, it is first initialized to the value returned

  • by an invocation of the {@link #initialValue} method.

  • @return the current thread’s value of this thread-local

*/

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();

}

大致意思:返回当前线程中该线程局部变量的副本中的值。如果变量没有当前线程的值,则首先将其初始化为调用{@link #initialValue}方法返回的值。

/**

  • Variant of set() to establish initialValue. Used instead

  • of set() in case user has overridden the set() method.

  • @return the initial value

*/

private T setInitialValue() {

T value = initialValue();

Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

if (map != null)

map.set(this, value);

else

createMap(t, value);

return value;

}

大致意思:将此线程局部变量的当前线程副本设置为指定的值。大多数子类将不需要重写这个方法,仅仅依靠{@link #initialValue}方法来设置线程局部变量的值,其中看到ThreadLocalMap 了没有?之前是不是一直有一个疑惑,那就是ThreadLocal到底是怎么存储我们线程变量的,ThreadLocalMap 就是ThreadLocal给每一个线程都分配一个独立资源的王牌,我们一起看看ThreadLocalMap的内部结构

static class Entry extends WeakReference<ThreadLocal<?>> {

/** The value associated with this ThreadLocal. */

Object value;

Entry(ThreadLocal<?> k, Object v) {

super(k);

value = v;

}

}

/**

  • The table, resized as necessary.

  • table.length MUST always be a power of two.

*/

private Entry[] table;

里面也有很多的方法,这里就不全贴出来了,占用太多空间,Entry 存放的就是线程对应的存储对象,我们来梳理一下整个过程,当线程调用get()的时候,首先会判断Thread类中的ThreadLocal.ThreadLocalMap threadLocals变量,通过线程名获取

在这里插入图片描述

如果存在,接着获取当前线程的存储的对象T,如果没有找到,那么将会执行初始化过程,也就是setInitialValue()方法,在setInitialValue()方法方法中又会判断ThreadLocal.ThreadLocalMap是否为空,如果不为空,赋值,为空,创建ThreadLocal.ThreadLocalMap

void createMap(Thread t, T firstValue) {

t.threadLocals = new ThreadLocalMap(this, firstValue);

}

然后返回当前线程的变量对象,大致流程就是这样,Thread类中维护着一个ThreadLocalMap,它负责存储线程的与线程对应的资源对象,当线程调用get()的时候会判断ThreadLocalMap中是否存在当前线程,如果没有,创建在返回,否者直接返回。

ThreadLocal注意事项

==============================================================================

不知道你们注意到没有,存储线程变量的ThreadLocalMap属于Thread,并不属于ThreadLocal,你知道为什么吗?

1:我们知道,ThreadLocal只是一个简单的工具类,而ThreadLocalMap里面的数据都是和线程相关,所以存放在Thread中。

2:ThreadLocalMap存放在Thread中不容易发生内存泄漏,为什么这么说呢?那是因为ThreadLocalMap中对线程Thread有着引用关系,由于ThreadLocal的生命周期可能是和程序共存亡的,如果将ThreadLocalMap存放到ThreadLocal中,就算线程的生命周期结束了,ThreadLocalMap也不会被回收,因为ThreadLocal一直存在,如果存放在Thread中就不一样了,我们来看一下之前看过的源码

static class Entry extends WeakReference<ThreadLocal<?>> {

/** The value associated with this ThreadLocal. */

Object value;

Entry(ThreadLocal<?> k, Object v) {

super(k);

value = v;

}

}

看到WeakReference没有,它表示这ThreadLocalMap对ThreadLocal是一种弱引用,只要Thread生命周期结束,ThreadLocalMap也会跟着一起消失,所以考虑内存泄漏方面,存在Thread类中更合理。

那ThreadLocal会发生内存泄漏问题吗?答案是肯定的,刚刚我们说到,线程被回收,ThreadLocalMap也会一起被回收,但是有一种情况线程是同程序共生死的,那就是线程池,所这个时候就可能会发生内存泄露问题了,然而我们我们在项目开发中线程池是使用比较多的,那我们如何解决这个问题呢?

其实很简单,java的gc机制做不到,不代表我们自己做不到,我们在自己的逻辑处理中自己释放,想必很多人都想到了,try/finally,没错就是它,只需要在finally中假如这行代码即可。

t.remove();//t:ThreadLocal实例

ThreadLocal介绍的差不多了,下面我们来使用ThreadLocal解决一下SimpleDateFormat多线程的安全问题吧。

使用ThreadLocal解决SimpleDateFormat线程安全问题

====================================================================================================

上修改之后的代码

package com.ymy.test;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Date;

public class SimpleDateFormatBugTest {

private static ThreadLocal t = ThreadLocal.withInitial(() -> new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”));

private static SimpleDateFormat getSimpleDateFormat() {

return t.get();

}

private static Date parse(String date){

try {

return getSimpleDateFormat().parse(date);

} catch (ParseException e) {

e.printStackTrace();

}

return null;

}

public static void main(String[] args) {

Thread t1 = new Thread(() -> {

Date parse = parse(“2020-12-12 12:12:12”);

System.out.println(“当前日期:” + parse);

});

Thread t2 = new Thread(() -> {

Date parse = parse(“2019-11-11 11:11:11”);

System.out.println(“当前日期:” + parse);

});

Thread t3 = new Thread(() -> {

Date parse = parse(“2018-10-10 10:10:10”);

System.out.println(“当前日期:” + parse);

});

t1.start();

t2.start();

t3.start();

try {

t1.join();

t2.join();

t3.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“线程执行完毕”);

}

}

第一次运行

当前日期:Wed Oct 10 10:10:10 CST 2018

当前日期:Mon Nov 11 11:11:11 CST 2019

当前日期:Sat Dec 12 12:12:12 CST 2020

线程执行完毕

Process finished with exit code 0

第二次运行

当前日期:Wed Oct 10 10:10:10 CST 2018

当前日期:Sat Dec 12 12:12:12 CST 2020

当前日期:Mon Nov 11 11:11:11 CST 2019

线程执行完毕

Process finished with exit code 0

第三次运行

当前日期:Wed Oct 10 10:10:10 CST 2018

还有兄弟不知道网络安全面试可以提前刷题吗?费时一周整理的160+网络安全面试题,金九银十,做网络安全面试里的显眼包!

王岚嵚工程师面试题(附答案),只能帮兄弟们到这儿了!如果你能答对70%,找一个安全工作,问题不大。

对于有1-3年工作经验,想要跳槽的朋友来说,也是很好的温习资料!

【完整版领取方式在文末!!】

93道网络安全面试题

内容实在太多,不一一截图了

黑客学习资源推荐

最后给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

1️⃣零基础入门
① 学习路线

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

image

② 路线对应学习视频

同时每个成长路线对应的板块都有配套的视频提供:

image-20231025112050764

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以点击这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值