昨天看博客看到这样一段代码,说是看似线程安全,但实际上并不是,参考大佬一个看似线程安全的示例:
class ListHelper <E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
方法等价于:
public boolean putIfAbsent(E x) {
synchronized(this) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
大佬的解析是:putIfAbsent方法的锁其实是ListHelper对象, 而可以肯定的是Collections.synchronizedList返回的线程安全的List内部使用的锁绝对不是ListHelper的对象,所以ListHelper中的putIfAbsent方法和线程安全的List使用的不是同一个锁,因此上面的这个加了synchronized关键字的方法依然不能实现线程安全性。
嗯.....有道理,我的理解是:当A线程判断了list.contains(x)不存在,刚准备往list插入的时候,另一个线程B如果直接调用当前线程A使用的ListHelper对象——BadListHelper.list.add()方法时,就会重复添加元素。即由于putIfAbsen(E x)方法不是原子性的,所以线程不安全。自己写个代码试一下:
MyRunnable2:
public class MyRunnable2 implements Runnable {
ListHelper list;
public MyRunnable2(ListHelper listHelper) {
list = listHelper;
}
@Override
public void run() {
synchronized (list) {
boolean absent = !list.list.contains("1");
System.out.println(absent);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (absent)
list.list.add("1");
System.out.println("MyRunnable2:" + new Gson().toJson(list));
}
}
}
我把putIfAbsent()方法直接写到了MyRunnable2里面,当判断到当前的listHelper对象不包含字符串"1"的时候,休眠5s,再做插入动作。
MyRunnable1:
public class MyRunnable1 implements Runnable {
ListHelper listHelper;
public MyRunnable1(ListHelper listHelper) {
this.listHelper = listHelper;
}
@Override
public void run() {
listHelper.list.add("1");
System.out.println("MyRunnable1:" + new Gson().toJson(list));
}
}
MyRunnable1则是直接调用listHelper对象的list属性中,list的add()方法——listHelper.list.add("1");
Main方法:
public class TestMain {
public static void main(String[] args) {
ListHelper listHelper = new ListHelper<String>();
MyRunnable1 r1 = new MyRunnable1(listHelper);
MyRunnable2 r2 = new MyRunnable2(listHelper);
new Thread(r2).start();
new Thread(r1).start();
System.out.println("程序结束...");
}
}
线程不安全,list添加了两个相同的字符串"1"。
但这也引出了一个问题:
synchronized锁住对象了,List<E>属于listHelper对象里面的属性,既然锁了对象,那么为什么不会锁对象里面的属性呢?换句话说,对象锁不会锁对象里面的属性,那么锁对象的时候锁住了什么呢?
往BadListHelper类里面加了一个测试的方法,加了synchronized锁,MyRunnable1改为执行这个test1方法:
public synchronized void test1() {
System.out.println("run test1");
}
结果是:MyRunnable1得等MyRunnable2执行完成之后才会执行,否则会一直阻塞。
诶,这么看对象锁会锁住当前对象中同样加了synchronized的,我把list变为synchronized,那么不就线程安全了吗?
public synchronized List<E> list = Collections.synchronizedList(new ArrayList<E>());
额...这行代码是错误的,变量不能被synchronized修饰。。。
那就这样,把List<E>变量设置为private私有,不提供getter和setter方法,并且禁止反射,这样不就线程安全了吗?后面想想,不太合理,项目里面或多或少都要用到反射,不能禁用。。。算了吧,还是好好百度对象锁吧。。。
百度百科:对象锁用于程序片段或者method上,此时将获得对象的锁,所有想要进入该对象的synchronized的方法或者代码段的线程都必须获取对象的锁,如果没有,则必须等其他线程释放该锁。
结论: