并发编程有必要先了解 目前业务场景常见的问题:
问题1: 线程安全是核心问题
线程编程中的常见线程安全问题: SimpleDateFormat 线程安全问题复现:
内部类图结构:
每个SimpleDataFormat实例里面都有一个Calendar对象,通过后面源码分析,会发现SimpleDataFormat之所以线程不安全,就是因为Calender是线程不安全的,后者之所以是线程不安全的,是因为其中存放的日期数据的变量都是线程不安全的,比如fields、time等
代码分析:
点击进入parse:
@Override
public Date parse(String text, ParsePosition pos)
{
checkNegativeNumberExpression();
int start = pos.index;
int oldStart = start;
int textLength = text.length();
boolean[] ambiguousYear = {false};
CalendarBuilder calb = new CalendarBuilder();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
if (start >= textLength || text.charAt(start) != (char)count) {
pos.index = oldStart;
pos.errorIndex = start;
return null;
}
start++;
break;
case TAG_QUOTE_CHARS:
while (count-- > 0) {
if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
pos.index = oldStart;
pos.errorIndex = start;
return null;
}
start++;
}
break;
default:
// Peek the next pattern to determine if we need to
// obey the number of pattern letters for
// parsing. It's required when parsing contiguous
// digit text (e.g., "20010704") with a pattern which
// has no delimiters between fields, like "yyyyMMdd".
boolean obeyCount = false;
// In Arabic, a minus sign for a negative number is put after
// the number. Even in another locale, a minus sign can be
// put after a number using DateFormat.setNumberFormat().
// If both the minus sign and the field-delimiter are '-',
// subParse() needs to determine whether a '-' after a number
// in the given text is a delimiter or is a minus sign for the
// preceding number. We give subParse() a clue based on the
// information in compiledPattern.
boolean useFollowingMinusSignAsDelimiter = false;
if (i < compiledPattern.length) {
int nextTag = compiledPattern[i] >>> 8;
if (!(nextTag == TAG_QUOTE_ASCII_CHAR ||
nextTag == TAG_QUOTE_CHARS)) {
obeyCount = true;
}
if (hasFollowingMinusSign &&
(nextTag == TAG_QUOTE_ASCII_CHAR ||
nextTag == TAG_QUOTE_CHARS)) {
int c;
if (nextTag == TAG_QUOTE_ASCII_CHAR) {
c = compiledPattern[i] & 0xff;
} else {
c = compiledPattern[i+1];
}
if (c == minusSign) {
useFollowingMinusSignAsDelimiter = true;
}
}
}
start = subParse(text, start, tag, count, obeyCount,
ambiguousYear, pos,
useFollowingMinusSignAsDelimiter, calb);
if (start < 0) {
pos.index = oldStart;
return null;
}
}
}
// At this point the fields of Calendar have been set. Calendar
// will fill in default values for missing fields when the time
// is computed.
pos.index = start;
Date parsedDate;
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();
}
}
}
// An IllegalArgumentException will be thrown by Calendar.getTime()
// if any fields are out of range, e.g., MONTH == 17.
catch (IllegalArgumentException e) {
pos.errorIndex = start;
pos.index = oldStart;
return null;
}
return parsedDate;
}
解析日期字符串并把解析好的数据放入CalendarBuilder的实例calb中。CalendarBuilder是一个建造者模式,用来存放后面需要的日期数据。
使用calb中解析好的日期数据设置calendar
calb.establish的代码如下:
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;
}
// (1)重置日期对象cal的属性值
cal.clear();
// Set the fields from the min stamp to the max stamp so that
// (2) 使用calb中的属性设置calb
// 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);
}
// (3)返回设置好的cal对象。
return cal;
}
从以上代码可以看出 (1)(2)(3)并不是原子性操作,当多个线程调用parse方法时,比如线程A执行代码(1)和代码(2),也就是设置好了cal对象,但在执行代码(3)之前,线程B执行了代码(3),清空了cal对象,由于多个线程使用的是一个cal对象,所以线程A执行代码(3)返回的可能就是被线程B清空的对象,当然也有可能线程B执行了代码(2),设置了之前被线程A修改的cal对象,从而导致线程A程序出现错误。
解决方案:
出现线程安全的原因是因为多线程执行时代码(1)(2)(3)三个步骤不是一个原子性操作,那么容易想到的是对这个步骤进行同步处理,使这3步骤成为原子性操作,可以使用synchronized进行同步
/**
* SimpleDateFormat
* 如何解决线程安全问题
*/
public class SolutionSimpleDateFormat {
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
for (int i=0;i<10; i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (sdf){
try {
System.out.println(sdf.parse("2018-12-13 15:17:27"));
} catch (ParseException e) {
e.printStackTrace();
}
}
}
});
thread.start();
}
}
}
多线程同步意味着多个线程要进行竞争锁,在高并发场景下会导致系统响应性能下降。 所以我们可以再进一步优化,采用ThreadLocal
这样每个线程只需要使用一个SimpleDateFormat实例,消除了锁竞争的性能问题
参考代码如下:
package com.gary.javastack.concurrent.aqs.safe;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* SimpleDateFormat
* 如何解决线程安全问题
*/
public class SolutionSimpleDateFormat2 {
static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(sdf.get().parse("2018-12-13 15:17:27"));
} catch (ParseException e) {
e.printStackTrace();
} finally {
// 使用完毕记得清除,避免内存泄露
sdf.remove();
}
}
});
thread.start();
}
}
}