我们常说,如果将对象放入到线程安全容器,例如Vector或synchronizedMap时,那么对象就可以安全地发布,后续的每个线程都可以安全地访问。
按照我们上一节的论述,这里的安全包含对象的内蕴状态吗,还是依旧仅仅只有对象的引用?
经过测试,我们可以发现,所有的线程安全容器依旧只能保证对象的引用更改是安全的,也就是说,只有当对象的引用地址发生了变化,其他线程才能感知到这种变化。
示例代码如下,依旧以计数为例:
// 类本身不是线程安全的,绝不会因为放入线程安全容器,就会变成线程安全的类了
static class Person {
private Long age = 0L;
// 总是递增
public void increment() {
// 如果是可见的,age的读取与写入应该可以保持一致
age ++;
}
public long getAge() {
return age;
}
}
测试代码如下:
final String KEY = "KEY";
final Person person = new Person();
// 放入线程安全容器
final Map<String, Person> values = Collections.synchronizedMap(Maps.newHashMap(KEY, person));
// 四个线程同时计数
for (int i = 0; i < 4; i++) {
new Thread() {
public void run() {
int count = 25;
while (count > 0) {
Person yiifaa = values.get(KEY);
yiifaa.increment();
count--;
}
}
}.start();
}
从结果来看,最后“person.getAge()”依旧不能达到100的计数,所以对象的内容依旧不是线程安全的。
下面说简单的解决办法,因为对象的获取是线程安全的,但递增的方法(yiifaa.increment())是不安全的,所以只需要最小的锁同步,如下:
Person yiifaa = values.get(KEY);
// 当然观察锁也不一定要是yiifaa,只要是共享对象都可以,如KEY
synchronized(yiifaa) {
yiifaa.increment();
}
count--;
从上面的代码可以看出,锁与共享变量并没有直接的关系,只需要保证所有的线程在同一个锁上同步即可。
结论
在使用synchronizedMap、synchronizedList、ConcurrentMap时要千万注意,线程安全容器仅仅只能保证对象的引用可见性,而不能内容的可见性。