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

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

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

  • SimpleDateFormat诡异bug

  • 复现SimpleDateFormat诡异bug

    • 字符串日期转Date日期(parse)
  • Date日期转String类型(format)

  • SimpleDateFormat出现bug的原因

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

    • 局部变量
  • 使用SimpleDateFormat方法时加锁

  • 使用ThreadLocal

  • ThreadLocal介绍

  • ThreadLocal使用demo

  • ThreadLocal源码探索

  • ThreadLocal注意事项

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

  • 总结

又是风和日丽的一天,我正在快乐的写着bug,突然感觉到背后一阵凉风吹过,我感觉肯定有大事发生,我转头一看,果然,小明笑嘻嘻的站在我身后,边笑边说:哥,忙吗?不忙的话帮我看个问题呗!每次他的问题都十分诡异,不过身为同事,还是应该相互帮助的,我决定与他一起看看他的问题。

SimpleDateFormat诡异bug

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

SimpleDateFormat应该是我们开发中使用比较多的工具类了吧,小明也在项目中使用到了,但就是这个工具类让小明痛苦了好长一段时间,为什么呢?那是因为测试工程师的小哥哥们在辛劳的做着接口性能测试,但是发现有些接口返回的日期时间是错乱的,不符合实际结果,这个时候,禅道中就多出了一道美丽的风景线,那就是bug:在并发接口测试中,日期返回的数据偶尔错误。

就是这么一个bug,让小明郁闷了好长一段时间,实在找不到解决方案,这才找到了我,我来看了他的业务代码之后,发现他使用了SimpleDateFormat这个工具类来格式化时间, 并且还是静态的,这个时候我就知道为什么平时做功能测试的时候没有问题,在做性能测试的时候bug就出来了,小明也是刚毕业不久,对多线程这一块不是怎么的熟悉,所以这个也不能怪他,我们现在使用demo来复现一下SimpleDateFormat的诡异bug吧。

复现SimpleDateFormat诡异bug

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

我这里将使用demo的形式来复现一下SimpleDateFormat存在的bug。

字符串日期转Date日期(parse)


package com.ymy.test;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Date;

public class SimpleDateFormatBugTest {

private static SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);

private static Date parse(String date){

Date parse = null;

try {

return sdf.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(“2020-12-12 12:12:12”);

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(“线程执行完毕”);

}

}

执行结果

Exception in thread “Thread-2” Exception in thread “Thread-0” java.lang.NumberFormatException: For input string: “.1102E.1102E22”

at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)

at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)

at java.lang.Double.parseDouble(Double.java:538)

at java.text.DigitList.getDouble(DigitList.java:169)

at java.text.DecimalFormat.parse(DecimalFormat.java:2089)

at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)

at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)

at java.text.DateFormat.parse(DateFormat.java:364)

at com.ymy.test.SimpleDateFormatBugTest.parse(SimpleDateFormatBugTest.java:33)

at com.ymy.test.SimpleDateFormatBugTest.lambda$main$2(SimpleDateFormatBugTest.java:56)

at java.lang.Thread.run(Thread.java:748)

java.lang.NumberFormatException: For input string: “.1102E.1102E22”

at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)

at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)

at java.lang.Double.parseDouble(Double.java:538)

at java.text.DigitList.getDouble(DigitList.java:169)

at java.text.DecimalFormat.parse(DecimalFormat.java:2089)

at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)

at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)

at java.text.DateFormat.parse(DateFormat.java:364)

at com.ymy.test.SimpleDateFormatBugTest.parse(SimpleDateFormatBugTest.java:33)

at com.ymy.test.SimpleDateFormatBugTest.lambda$main$0(SimpleDateFormatBugTest.java:45)

at java.lang.Thread.run(Thread.java:748)

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

线程执行完毕

Process finished with exit code 0

Date日期转String类型(format)


package com.ymy.test;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Date;

public class SimpleDateFormatTest {

private static SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);

private static Date d1 = null;

private static Date d2 = null;

private static Date d3 = null;

static {

try {

d1 = sdf.parse(“2020-12-12 12:12:12”);

d2 =sdf.parse(“2019-11-11 11:11:11”);

d3 =sdf.parse(“2018-10-10 10:10:10”);

} catch (ParseException e) {

e.printStackTrace();

}

}

public static void main(String[] args) {

Thread t1 = new Thread(() -> {

String parse = sdf.format(d1);

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

});

Thread t2 = new Thread(() -> {

String parse = sdf.format(d2);

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

});

Thread t3 = new Thread(() -> {

String parse = sdf.format(d3);

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

});

t1.start();

t2.start();

t3.start();

try {

t1.join();

t2.join();

t3.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

使用三个线程分别对2020-12-12 12:12:12、2019-11-11 11:11:11、2018-10-10 10:10:10的Date格式转字符串格式的操作,我们在静态代码块中初始化了Date格式的数据,然后使用三个线程分别对他们进行格式转换。

第一次:

当前日期:2020-12-12 12:12:12

当前日期:2018-11-11 11:11:11

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

第二次:

当前日期: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(“执行完毕”);

}

}

如何自学黑客&网络安全

黑客零基础入门学习路线&规划

初级黑客
1、网络安全理论知识(2天)
①了解行业相关背景,前景,确定发展方向。
②学习网络安全相关法律法规。
③网络安全运营的概念。
④等保简介、等保规定、流程和规范。(非常重要)

2、渗透测试基础(一周)
①渗透测试的流程、分类、标准
②信息收集技术:主动/被动信息搜集、Nmap工具、Google Hacking
③漏洞扫描、漏洞利用、原理,利用方法、工具(MSF)、绕过IDS和反病毒侦察
④主机攻防演练:MS17-010、MS08-067、MS10-046、MS12-20等

3、操作系统基础(一周)
①Windows系统常见功能和命令
②Kali Linux系统常见功能和命令
③操作系统安全(系统入侵排查/系统加固基础)

4、计算机网络基础(一周)
①计算机网络基础、协议和架构
②网络通信原理、OSI模型、数据转发流程
③常见协议解析(HTTP、TCP/IP、ARP等)
④网络攻击技术与网络安全防御技术
⑤Web漏洞原理与防御:主动/被动攻击、DDOS攻击、CVE漏洞复现

5、数据库基础操作(2天)
①数据库基础
②SQL语言基础
③数据库安全加固

6、Web渗透(1周)
①HTML、CSS和JavaScript简介
②OWASP Top10
③Web漏洞扫描工具
④Web渗透工具:Nmap、BurpSuite、SQLMap、其他(菜刀、漏扫等)
恭喜你,如果学到这里,你基本可以从事一份网络安全相关的工作,比如渗透测试、Web 渗透、安全服务、安全分析等岗位;如果等保模块学的好,还可以从事等保工程师。薪资区间6k-15k

到此为止,大概1个月的时间。你已经成为了一名“脚本小子”。那么你还想往下探索吗?

如果你想要入坑黑客&网络安全,笔者给大家准备了一份:282G全网最全的网络安全资料包评论区留言即可领取!

7、脚本编程(初级/中级/高级)
在网络安全领域。是否具备编程能力是“脚本小子”和真正黑客的本质区别。在实际的渗透测试过程中,面对复杂多变的网络环境,当常用工具不能满足实际需求的时候,往往需要对现有工具进行扩展,或者编写符合我们要求的工具、自动化脚本,这个时候就需要具备一定的编程能力。在分秒必争的CTF竞赛中,想要高效地使用自制的脚本工具来实现各种目的,更是需要拥有编程能力.

如果你零基础入门,笔者建议选择脚本语言Python/PHP/Go/Java中的一种,对常用库进行编程学习;搭建开发环境和选择IDE,PHP环境推荐Wamp和XAMPP, IDE强烈推荐Sublime;·Python编程学习,学习内容包含:语法、正则、文件、 网络、多线程等常用库,推荐《Python核心编程》,不要看完;·用Python编写漏洞的exp,然后写一个简单的网络爬虫;·PHP基本语法学习并书写一个简单的博客系统;熟悉MVC架构,并试着学习一个PHP框架或者Python框架 (可选);·了解Bootstrap的布局或者CSS。

8、超级黑客
这部分内容对零基础的同学来说还比较遥远,就不展开细说了,附上学习路线。
img

网络安全工程师企业级学习路线

img
如图片过大被平台压缩导致看不清的话,评论区点赞和评论区留言获取吧。我都会回复的

视频配套资料&国内外网安书籍、文档&工具

当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料&工具,并且已经帮大家分好类了。

img
一些笔者自己买的、其他平台白嫖不到的视频教程。
img

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

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

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

Bootstrap的布局或者CSS。

8、超级黑客
这部分内容对零基础的同学来说还比较遥远,就不展开细说了,附上学习路线。
img

网络安全工程师企业级学习路线

img
如图片过大被平台压缩导致看不清的话,评论区点赞和评论区留言获取吧。我都会回复的

视频配套资料&国内外网安书籍、文档&工具

当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料&工具,并且已经帮大家分好类了。

img
一些笔者自己买的、其他平台白嫖不到的视频教程。
img

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

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

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

  • 17
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值