可变状态带来的线程安全问题
什么是可变状态
可变状态即一个对象是可变的,换句话说,就是数据是可变的,对于多线程来讲,线程间共享的数据是共享可变状态,对于不变的数据,多线程不用使用锁就可以安全地进行访问。可以这么说,数据的可变状态导致被多线程访问时候存在线程安全问题。
隐藏的可变状态
在我们的正常理解中,解决可变状态可以让数据成为一个常量,确实,很多时候我们也会这么去使用,比如下面:
这是我们很经常使用的日期工具类
/**
* @author linxu
* 日期解析类
*/
public class DateParser {
private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");
public static Date parse(String s) throws ParseException {
return FORMAT.parse(s);
}
}
以下是测试代码:
/**
* 可变状态引起的多线程安全问题
*/
public class DateFormatBug {
public static void main(String[] args) throws Exception {
//使用final修饰,表面上看起来是不变状态。
final String dateString = "2012-01-01";
final Date dateParsed = DateParser.parse(dateString);
class ParsingThread extends Thread {
@Override
public void run() {
try {
while (true) {
Date d = DateParser.parse(dateString);
//如果出现不对应的情况,则打印。
if (!d.equals(dateParsed)) {
System.out.println("Expected: " + dateParsed + ", got: " + d);
}
}
} catch (Exception e) {
System.out.println("Caught: " + e);
}
}
}
Thread t1 = new ParsingThread();
Thread t2 = new ParsingThread();
t1.start();
t2.start();
}
}
对于以上的代码:
-
涉及到的变量我们都设置成为final,即不可变
-
采用两个线程无线循环,找到不对应的情况则打印。
- Caught: java.lang.NumberFormatException: multiple points
- Caught: java.lang.NumberFormatException: For input string: “”
- Caught: java.lang.NumberFormatException: empty String
- Expected: Sun Jan 01 00:00:00 CST 2012, got: Mon Jan 01 00:00:00 CST 1201
- …
我们看到了出现了以上错误,这是为什么呢?
-
原因在于FORMAT.parse(s)这行代码中存在着可变状态数据,即在这种外星方法中存在着隐藏的可变状态,导致了线程安全问题。
-
可见隐藏状态的风险很大,但是JAVA提供的API中并不会跟你说隐藏着什么什么,我们也不能把它归结为BUG只能是尽力去避免这类问题。
如何解决隐藏的可变状态
以上面的日期解析工具为例子,可以这么解决:
public class DateParser {
private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");
private static final ReentrantLock LOCK =new ReentrantLock(false);
public static Date parse(String s) throws ParseException {
try {
LOCK .lock();
return FORMAT.parse(s);
} finally {
LOCK .unlock();
}
}
}
总结
- 对于能够使用final修饰的数据,尽量使用final修饰
- 表面上是final的数据,也可能是可变的,永远不要忘记java是一门高级语言
- 通过正确的测试能够检测出隐藏的可变状态从而进行解决