- 线程不安全的例子
- 对象引用的逸出
- 隐藏的逸出
- 多个线程操作同一非线程安全对象
- 使用不同的锁锁定同一对象
- 同时操作多个关联的线程安全对象
- 总结
线程不安全的例子
1.对象引用的逸出
即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的
public void Calculator{
private ImmutableValue currentValue = null;
public ImmutableValue getValue(){
return currentValue;
}
public void setValue(ImmutableValue newValue){
this.currentValue = newValue;
}
public void add(int newValue){
this.currentValue = this.currentValue.add(newValue);
}
}
复制代码
Calculator类持有一个指向ImmutableValue实例的引用。通过setValue()方法和add()方法可能会改变这个引用。因此,即使Calculator类内部使用了一个不可变对象,但Calculator类本身还是可变的,因此Calculator类不是线程安全的。换句话说:ImmutableValue类是线程安全的,但使用它的类不是。
2.隐藏的逸出
public class ThisEscape {
public ThisEscape(EventSource source) {
//隐式的使this引用逸出
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
void doSomething(Event e) {
//do sth.
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
复制代码
当线程 1 和线程 2 同时访问 ThisEscape 构造方法,这时线程 A 初始化还未完成,此时由于 this 逸出,导致 this 在 1 和 2 中都具有可见性,线程 2 就可以通过 this 访问 doSomething(e) 方法,导致修改 ThisEscape 的属性。
3.多个线程操作同一非线程安全对象
public class NotThreadSafe{
StringBuilder builder = new StringBuilder();
public add(String text){
this.builder.append(text);
}
}
//调用方
NotThreadSafe sharedInstance = new NotThreadSafe();
new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();
public class MyRunnable implements Runnable{
NotThreadSafe instance = null;
public MyRunnable(NotThreadSafe instance){
this.instance = instance;
}
public void run(){
this.instance.add("some text");
}
}
复制代码
线程 1 和 线程 2 同时操作了同一个对象。并且这个对象并没有做线程安全的处理。
4.在同一对象上使用不同的锁
public class ListHelper<E> {
public List<E> list =
Collections.synchronizedList(new ArrayList<E>());
// 与 List 使用的不是同一把锁
public synchronized boolean putIfAbsent(E e) {
boolean absent = !list.contains(x);
if (absent) {
list.add(x);
}
return absent;
}
}
复制代码
虽然从代码上看像是线程安全代码,但是由于使用的锁与 list 的锁不是同一把锁,因此在多线程下仍然会出问题。
5.同时操作多个关联的同步对象
public class NumberRange{
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
//不安全的先检查,后执行
if (i > upper.get()) {
throw new Exception("");
}
lower.set(i);
}
public void setUpper(int i) {
if (i < lower.get()) {
throw new Exception("");
}
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
复制代码
虽然 lower 和 upper 都是线程安全的对象。但是它们之间并不是独立的。 存在当线程 1 调用 setLower(5), 线程 2 调用 setUpper(4),那么有可能产生的结果是两个线程均判断通过的「时序问题」,然后出现 lower 大于 upper 的情况。
总结
以上就是线程不安全的代码常见场景举例,在实际的开发中,很有可能是较为隐晦的,但是场景基本都是上面的一种或多种场景共同出现。因此实际排查线程安全问题的过程中,查看代码中有无上述的场景即可。下一篇讲结合实际开发讲解如何发现及处理此类问题。
[1]《Java 并发编程实战》