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
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是线程不安全的,但是为什么是线程不安全的,大家都知道吗?我们一起来看一下。
首先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设置成为局部变量那不就没问题了吗,却是是这样,但如果需要修改格式化的模式,改动量是非常大,因为你需要将所有涉及到的局部变量都修改一遍,而单例只需要修改一次,这个看情况而定
这也是一种解决的思路,比如:synchronized,大家都知道,加锁会降低程序的效率,除非必要情况,否者时不建议直接使用锁来解决的。
ThreadLocal不知道大家了解过没有,他是一种解决多线程并发问题简单有效的方式,通过线程和变量绑定的方式,让线程与线程之间不存在变量共享的问题,自然而然就解决了多线程并发的问题。
============================================================================
JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量。
举个生活中的例子,人有三急,而厕所只有一坑位,所以公司的上百人都会争先恐后的争夺那一个坑位的使用权,并且在争夺过程中还有可能出现事故,公司老总有一次也想蹲坑,但是发现已经有很多人正在为了那一个坑位抢的死去活来,导致他不能及时排泄而。。。。。。
公司老总整理了情绪之后,于时吩咐秘书:你赶紧去安排一下,厕所的坑位增加到一百个,每人一个,看还有没有人抢。
这个例子就有点类似与我们程序中的多线程,很多线程都在抢同一个资源,在抢来抢去的时候难免发生意外情况,也就是程序中的线程安全问题,所以有没有一种给每个线程都分配对应的变量,让他们不用抢来抢去?这个时候ThreadLocal站了出来,我是土豪,我给你们每个线程都分配一个只属于你们自己的资源,省的你们抢来抢去,打扰我泡妞。
================================================================================
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是如何被创建的
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
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
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
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-EIYB1fke-1715160114959)]
[外链图片转存中…(img-R5yx5e6L-1715160114959)]
[外链图片转存中…(img-JA3cWSGi-1715160114959)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!