Skip to content

Commit

Permalink
增加部分知识内容
Browse files Browse the repository at this point in the history
  • Loading branch information
hollis.zhl committed Dec 1, 2019
1 parent 1549876 commit 1eeb3ab
Show file tree
Hide file tree
Showing 6 changed files with 1,384 additions and 5 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ finally和return的执行顺序

格林威治时间CET,UTC,GMT,CST几种常见时间的含义和关系

SimpleDateFormat的线程安全性问题
[SimpleDateFormat的线程安全性问题](/basics/java-basic/simpledateformat-thread-safe.md)

Java 8中的时间处理

Expand All @@ -253,9 +253,9 @@ URL编解码、Big Endian和Little Endian

#### 语法糖

Java中语法糖原理解语法糖
[Java中语法糖原理解语法糖](/basics/java-basic/syntactic-sugar.md)

语法糖switch 支持 String 与枚举泛型自动装箱与拆箱方法变长参数枚举内部类条件编译断言数值字面量for-eachtry-with-resourceLambda表达式
[语法糖switch 支持 String 与枚举泛型自动装箱与拆箱方法变长参数枚举内部类条件编译断言数值字面量for-eachtry-with-resourceLambda表达式](/basics/java-basic/syntactic-sugar.md)

### 阅读源代码

Expand Down Expand Up @@ -345,7 +345,7 @@ class文件格式、运行时数据区:堆、栈、方法区、直接内存、

堆和栈区别

Java中的对象一定在堆上分配吗
[Java中的对象一定在堆上分配吗](/basics/jvm/stack-alloc.md)

#### Java内存模型

Expand Down
2 changes: 1 addition & 1 deletion basics/java-basic/polymorphism.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
###什么是多态多态有什么好处多态的必要条件是什么Java中多态的实现方式
### 什么是多态多态有什么好处多态的必要条件是什么Java中多态的实现方式

多态的概念呢比较简单就是同一操作作用于不同的对象可以有不同的解释产生不同的执行结果

Expand Down
280 changes: 280 additions & 0 deletions basics/java-basic/simpledateformat-thread-safe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
在日常开发中我们经常会用到时间我们有很多办法在Java代码中获取时间但是不同的方法获取到的时间的格式都不尽相同这时候就需要一种格式化工具把时间显示成我们需要的格式

最常用的方法就是使用SimpleDateFormat类这是一个看上去功能比较简单的类但是一旦使用不当也有可能导致很大的问题

在阿里巴巴Java开发手册中有如下明确规定

<img src="https://www.hollischuang.com/wp-content/uploads/2018/11/规约1.png" alt="" width="1862" height="154" class="aligncenter size-full wp-image-3043" />

那么本文就围绕SimpleDateFormat的用法原理等来深入分析下如何以正确的姿势使用它

### SimpleDateFormat用法

SimpleDateFormat是Java提供的一个格式化和解析日期的工具类它允许进行格式化日期 -> 文本)、解析文本 -> 日期和规范化SimpleDateFormat 使得可以选择任何用户定义的日期-时间格式的模式

在Java中可以使用SimpleDateFormat的format方法将一个Date类型转化成String类型并且可以指定输出格式

// Date转String
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);


以上代码转换的结果是2018-11-25 13:00:00日期和时间格式由"日期和时间模式"字符串指定如果你想要转换成其他格式只要指定不同的时间模式就行了

在Java中可以使用SimpleDateFormat的parse方法将一个String类型转化成Date类型

// String转Data
System.out.println(sdf.parse(dataStr));


#### 日期和时间模式表达方法

在使用SimpleDateFormat的时候需要通过字母来描述时间元素并组装成想要的日期和时间模式常用的时间元素和字母的对应表如下

![-w717][1]

模式字母通常是重复的其数量确定其精确表示如下表是常用的输出格式的表示方法

![-w535][2]

#### 输出不同时区的时间

时区是地球上的区域使用同一个时间定义以前人们通过观察太阳的位置时角决定时间这就使得不同经度的地方的时间有所不同地方时)。1863首次使用时区的概念时区通过设立一个区域的标准时间部分地解决了这个问题

世界各个国家位于地球不同位置上因此不同国家特别是东西跨度大的国家日出日落时间必定有所偏差这些偏差就是所谓的时差

现今全球共分为24个时区由于实用上常常1个国家或1个省份同时跨着2个或更多时区为了照顾到行政上的方便常将1个国家或1个省份划在一起所以时区并不严格按南北直线来划分而是按自然条件来划分例如中国幅员宽广差不多跨5个时区但为了使用方便简单实际上在只用东八时区的标准时即北京时间为准

由于不同的时区的时间是不一样的甚至同一个国家的不同城市时间都可能不一样所以在Java中想要获取时间的时候要重点关注一下时区问题

默认情况下如果不指明在创建日期的时候会使用当前计算机所在的时区作为默认时区这也是为什么我们通过只要使用`new Date()`就可以获取中国的当前时间的原因

那么如何在Java代码中获取不同时区的时间呢SimpleDateFormat可以实现这个功能

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println(sdf.format(Calendar.getInstance().getTime()));


以上代码转换的结果是2018-11-24 21:00:00既中国的时间是11月25日的13点而美国洛杉矶时间比中国北京时间慢了16个小时这还和冬夏令时有关系就不详细展开了)。

> 如果你感兴趣你还可以尝试打印一下美国纽约时间America/New_York)。纽约时间是2018-11-25 00:00:00纽约时间比中国北京时间早了13个小时

当然这不是显示其他时区的唯一方法不过本文主要为了介绍SimpleDateFormat其他方法暂不介绍了

## SimpleDateFormat线程安全性

由于SimpleDateFormat比较常用而且在一般情况下一个应用中的时间显示模式都是一样的所以很多人愿意使用如下方式定义SimpleDateFormat

public class Main {

private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static void main(String[] args) {
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
System.out.println(simpleDateFormat.format(Calendar.getInstance().getTime()));
}
}


**这种定义方式存在很大的安全隐患。**

#### 问题重现

我们来看一段代码以下代码使用线程池来执行时间输出

/** * @author Hollis */
public class Main {

/**
* 定义一个全局的SimpleDateFormat
*/
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

/**
* 使用ThreadFactoryBuilder定义一个线程池
*/
private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("demo-pool-%d").build();

private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

/**
* 定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行
*/
private static CountDownLatch countDownLatch = new CountDownLatch(100);

public static void main(String[] args) {
//定义一个线程安全的HashSet
Set<String> dates = Collections.synchronizedSet(new HashSet<String>());
for (int i = 0; i < 100; i++) {
//获取当前时间
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
//时间增加
calendar.add(Calendar.DATE, finalI);
//通过simpleDateFormat把时间转换成字符串
String dateString = simpleDateFormat.format(calendar.getTime());
//把字符串放入Set中
dates.add(dateString);
//countDown
countDownLatch.countDown();
});
}
//阻塞,直到countDown数量为0
countDownLatch.await();
//输出去重后的时间个数
System.out.println(dates.size());
}
}


以上代码其实比较简单很容易理解就是循环一百次每次循环的时候都在当前时间基础上增加一个天数这个天数随着循环次数而变化),然后把所有日期放入一个**线程安全的**、**带有去重功能**的Set中然后输出Set中元素个数

> 上面的例子我特意写的稍微复杂了一些不过我几乎都加了注释这里面涉及到了[线程池的创建][3]、[CountDownLatch][4]、lambda表达式线程安全的HashSet等知识感兴趣的朋友可以逐一了解一下

正常情况下以上代码输出结果应该是100但是实际执行结果是一个小于100的数字

原因就是因为SimpleDateFormat作为一个非线程安全的类被当做了共享变量在多个线程中进行使用这就出现了线程安全问题

在阿里巴巴Java开发手册的第一章第六节——并发处理中关于这一点也有明确说明

<img src="https://www.hollischuang.com/wp-content/uploads/2018/11/guiyue2.png" alt="" width="1878" height="546" class="aligncenter size-full wp-image-3044" />

那么接下来我们就来看下到底是为什么以及该如何解决

#### 线程不安全原因

通过以上代码我们发现了在并发场景中使用SimpleDateFormat会有线程安全问题其实JDK文档中已经明确表明了SimpleDateFormat不应该用在多线程场景中

> Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

那么接下来分析下为什么会出现这种问题SimpleDateFormat底层到底是怎么实现的

我们跟一下SimpleDateFormat类中format方法的实现其实就能发现端倪

![][5]

SimpleDateFormat中的format方法在执行过程中会使用一个成员变量calendar来保存时间这其实就是问题的关键

由于我们在声明SimpleDateFormat的时候使用的是static定义的那么这个SimpleDateFormat就是一个共享变量随之SimpleDateFormat中的calendar也就可以被多个线程访问到

假设线程1刚刚执行完`calendar.setTime`把时间设置成2018-11-11还没等执行完线程2又执行了`calendar.setTime`把时间改成了2018-12-12这时候线程1继续往下执行拿到的`calendar.getTime`得到的时间就是线程2改过之后的

除了format方法以外SimpleDateFormat的parse方法也有同样的问题

所以不要把SimpleDateFormat作为一个共享变量使用

#### 如何解决

前面介绍过了SimpleDateFormat存在的问题以及问题存在的原因那么有什么办法解决这种问题呢

解决方法有很多这里介绍三个比较常用的方法

**使用局部变量**

for (int i = 0; i < 100; i++) {
//获取当前时间
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
// SimpleDateFormat声明成局部变量
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//时间增加
calendar.add(Calendar.DATE, finalI);
//通过simpleDateFormat把时间转换成字符串
String dateString = simpleDateFormat.format(calendar.getTime());
//把字符串放入Set中
dates.add(dateString);
//countDown
countDownLatch.countDown();
});
}


SimpleDateFormat变成了局部变量就不会被多个线程同时访问到了就避免了线程安全问题

**加同步锁**

除了改成局部变量以外还有一种方法大家可能比较熟悉的就是对于共享变量进行加锁

for (int i = 0; i < 100; i++) {
//获取当前时间
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
//加锁
synchronized (simpleDateFormat) {
//时间增加
calendar.add(Calendar.DATE, finalI);
//通过simpleDateFormat把时间转换成字符串
String dateString = simpleDateFormat.format(calendar.getTime());
//把字符串放入Set中
dates.add(dateString);
//countDown
countDownLatch.countDown();
}
});
}


通过加锁使多个线程排队顺序执行避免了并发导致的线程安全问题

其实以上代码还有可以改进的地方就是可以把锁的粒度再设置的小一点可以只对`simpleDateFormat.format`这一行加锁这样效率更高一些

**使用ThreadLocal**

第三种方式就是使用 ThreadLocalThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象那么自然也就不存在竞争问题了

/**
* 使用ThreadLocal定义一个全局的SimpleDateFormat
*/
private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};

//用法
String dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime());


ThreadLocal 来实现其实是有点类似于缓存的思路每个线程都有一个独享的对象避免了频繁创建对象也避免了多线程的竞争

当然以上代码也有改进空间就是其实SimpleDateFormat的创建过程可以改为延迟加载这里就不详细介绍了

**使用DateTimeFormatter**

如果是Java8应用可以使用DateTimeFormatter代替SimpleDateFormat这是一个线程安全的格式化工具类就像官方文档中说的这个类 simple beautiful strong immutable thread-safe

//解析日期
String dateStr= "2016年10月25日";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
LocalDate date= LocalDate.parse(dateStr, formatter);

//日期转换为字符串
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
String nowStr = now .format(format);
System.out.println(nowStr);


### 总结

本文介绍了SimpleDateFormat的用法SimpleDateFormat主要可以在String和Date之间做转换还可以将时间转换成不同时区输出同时提到在并发场景中SimpleDateFormat是不能保证线程安全的需要开发者自己来保证其安全性

主要的几个手段有改为局部变量使用synchronized加锁使用Threadlocal为每一个线程单独创建一个等

希望通过此文你可以在使用SimpleDateFormat的时候更加得心应手

[1]: https://www.hollischuang.com/wp-content/uploads/2018/11/15431240092595.jpg
[2]: https://www.hollischuang.com/wp-content/uploads/2018/11/15431240361504.jpg
[3]: https://www.hollischuang.com/archives/2888
[4]: https://www.hollischuang.com/archives/290
[5]: https://www.hollischuang.com/wp-content/uploads/2018/11/15431313894397.jpg
Loading

0 comments on commit 1eeb3ab

Please sign in to comment.