在使用日期类的时候,我们常常需要将日期转化为我们方便查看的格式,这时我们就会使用到SimpleDateFormat类。由于SimpleDateFormat是线程不安全的,本文通过一个使用SimpleDateFormat的demo不断迭代,展示SimpleDateFormat在高并发情况下出现的问题,通过解决这些问题,学习使用线程池和ThreadLocal。
一、两个线程打印SimpleDateFormat
使用两个线程打印SimpleDateFormat/**
* 两个线程打印日期并没有什么问题
*/
public class ThreadLocalNormalUsage00 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(10);
System.out.println(date);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(104707);
System.out.println(date);
}
}).start();
}
public String date(int seconds){
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
开启两个线程,各自在自己的线程内创建自己的SimpleDateFormat对象,打印出的结果没有任何问题。
1970-01-02 01:05:07
1970-01-01 08:00:10
Process finished with exit code 0
二、十个以上线程打印SimpleDateFormat
for循环开启线程当任务逐步增多以后,我们使用第一种创建线程的方式就变得十分麻烦了,所以我们使用for循环来创建线程。
package threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 十个线程打印日期
*/
public class ThreadLocalNormalUsage01 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 30; i++) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage01().date(finalI);
System.out.println(date);
}
}).start();
Thread.sleep(100);
}
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
运行结果如下:
.
.
.
1970-01-01 08:00:26
1970-01-01 08:00:27
1970-01-01 08:00:28
1970-01-01 08:00:29
Process finished with exit code 0
三、1000个线程打印SimpleDateFormat
使用线程池使用for循环的实现方法有点简陋,假如现在需求变成了1000个,需要如何实现呢?如果使用for循环,将会创建1000个线程,创建线程和销毁线程都是一笔不小的开销。实际上没有必要这样做,我们可以使用线程池将线程的生命周期和线程执行的内容解耦,所以我们继续升级代码。
/**
* 使用线程池打印日期
*/
public class ThreadLocalNormalUsage02 {
//使用固定数量的线程池
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage02().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
运行结果如下:
.
.
.
1970-01-01 08:00:26
1970-01-01 08:00:27
1970-01-01 08:00:28
1970-01-01 08:00:29
Process finished with exit code 0
四、所有的线程都公用一个SimpleDateFormat对象
上边的实现虽然可以使用线程池来复用线程减少开销,但是每创建一个线程就会new一个SimpleDateFormat对象,这样有几个线程就会创建几个对象,我们如果想让所有线程都使用同一个SimpleDateFormat对象,这时就出现了线程安全问题。
/**
* 使用线程池打印日期
*/
public class ThreadLocalNormalUsage03 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage03().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
}
由于SimpleDateFormat类本身是线程不安全的,当所有线程共用一个SimpleDateFormat对象时,就会出现问题。
.
.
.
1970-01-01 08:00:26
1970-01-01 08:00:30
1970-01-01 08:00:30
1970-01-01 08:00:29
Process finished with exit code 0
五、使用synchronized解决线程同步问题
/**
* 使用synchronized对format加锁,来保证线程安全
*/
public class ThreadLocalNormalUsage04 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage04().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
String s = null;
synchronized (ThreadLocalNormalUsage04.class) {
s = dateFormat.format(date);
}
return s;
}
}
使用synchronized确实可以解决时间重复的问题,然而synchronized属于重量级锁,它会无时无刻进行锁住对象,而不考虑到程序实际的竞争情况,大多数程序在都是进行交替执行,也就是说不存在资源的竞争。但是这样一个一个排队执行会严重影响效率,尤其是对于像SimpleDateFormat这样的工具类,很多地方都会使用,加锁效率很低。综上所述,我们继续优化代码。
五、最好的解决方案,使用ThreadLocal
ThreadLocal图示/**
* 利用ThreadLocal
*/
public class ThreadLocalNormalUsage05 {
//使用固定数量的线程池
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter{
public static ThreadLocal<SimpleDateFormat>dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
使用ThreadLocal是线程安全的,而且没有synchronized那样的性能问题,多个线程可以并行执行,每一个线程都有自己独享的对象。
网友评论