经典并发编程描述满足不变性有以下条件:
1.对象创建后状态就不再变化。
2.对象的所有域都是final类型。
3.创建对象期间,this引用没有溢出。
实际对于第2点描述不完全准确:
1.只要成员变量是私有的,并且只提供只读操作,就可能做到线程安全,并不一定需要final修饰,注意这里说的是可能,原因见第2点。
2.如果成员变量是个对象,并且外部可写,那么也不能保证线程安全,例如:
public class Apple { public static void main(String[] args) { Dictionary dictionary = new Dictionary(); Map<String, String> map = dictionary.getMap(); //这个操作后,导致下一步的操作结果和预期不符,预期不符就不是线程安全 map.clear(); System.out.println(dictionary.translate("苹果")); } } class Dictionary { private final Map<String, String> map = new HashMap<String, String>(); public Dictionary() { map.put("苹果", "Apple"); map.put("橘子", "Orange"); } public String translate(String cn) { if (map.containsKey(cn)) { return map.get(cn); } return "UNKONWN"; } public Map<String, String> getMap() { return map; } } 复制代码
因此对不变对象的正确理解应该是:
1.对象创建后状态不再变化(所有成员变量不再变化)
2.只有只读操作。
3.任何时候对象的成员都不会溢出(成员不被其他外部对象进行写操作),而不仅仅只是在构建时。
另一些书籍和培训提到不变类应该用final修饰,以防止类被继承后子类不安全,个人觉得子类和父类本身就不是一个对象,我们说一个类是否线程安全说的是这个类本身,而不需要关心子类是否安全,唯一需要注意的是:父类的成员变量是protected修饰,子类继承后修改了它,然后子类和父类又被同一个线程调用,导致父类不安全。
如果对象只在单线程中使用,不在多个线程中共享,这就是线程封闭。
例如web应用中获取连接池中的数据库连接访问数据库,每个web请求是一个独立线程,当一个请求获取到一个数据库连接后,不会再被其他请求使用,直到数据库连接关闭(回到连接池中)才会被其他请求使用。
对象只在局部代码块中使用,就是栈封闭的,例如:
public void print(Vector v) { int size = v.size(); for (int i = 0; i < size; i++) { System.out.println(v.get(i)); } } 复制代码
变量size是局部变量(栈封闭),Vector又是线程安全的容器,因此对于这个方法而言是线程安全的。
通过ThreadLocal存储的对象只对当前线程可见,因此也是线程安全的。
有些容器线程安全指的是原子操作线程安全,并非所有操作都安全,非线程安全的操作如:IF-AND-SET,容器迭代,例如:
public class VectorDemo { public static void main(String[] args) { Vector<String> tasks = new Vector<String>(); for (int i = 0; i < 10; i++) { tasks.add("task" + i); } Thread worker1 = new Thread(new Worker(tasks)); Thread worker2 = new Thread(new Worker(tasks)); Thread worker3 = new Thread(new Worker(tasks)); worker1.start(); worker2.start(); worker3.start(); } } class Worker implements Runnable { private Vector<String> tasks; public Worker(Vector<String> tasks) { this.tasks = tasks; } public void run() { //如下操作非线程安全,多个线程同时执行,在判断时可能都满足条件,但实际处理时可能已经不再满足条件 while (tasks.size() > 0) { //模拟业务处理 sleep(100); //实际执行时,这里可能已经不满足tasks.size() > 0 System.out.println(Thread.currentThread().getName() + " " + tasks.remove(0)); } } private void sleep(long millis) { try { TimeUnit.MILLISECONDS.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } } 复制代码
输出日志:
Thread-0 task0 Thread-1 task2 Thread-2 task1 Thread-1 task3 Thread-2 task5 Thread-0 task4 Thread-0 task6 Thread-1 task8 Thread-2 task7 Thread-1 task9 Exception in thread "Thread-0" Exception in thread "Thread-2" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0 at java.util.Vector.remove(Vector.java:831) at com.javashizhan.concurrent.demo.safe.Worker.run(VectorDemo.java:46) at java.lang.Thread.run(Thread.java:745) java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0 at java.util.Vector.remove(Vector.java:831) at com.javashizhan.concurrent.demo.safe.Worker.run(VectorDemo.java:46) at java.lang.Thread.run(Thread.java:745) 复制代码
可以看到其中一个工作线程在tasks.remove(0)时,由于集合中已经没有数据而抛出异常。要做到线程安全则要对非原子操作加锁,修改后的代码如下:
public void run() { //对非原子操作加锁 synchronized (tasks) { while (tasks.size() > 0) { sleep(100); System.out.println(Thread.currentThread().getName() + " " + tasks.remove(0)); } } } 复制代码
在上述例子中,即使用final修饰Vector也非线程安全,final不代表被修饰对象是属于线程安全的不变对象。。
volatile关键字修饰的对象只能保证可见性,这类变量不缓存在CPU的缓存中,这样能保证如果A线程先修改了volatile变量的值,那么B线程后读取时就能看到最新值,而可见性不等于线程安全。
我们说Vector是线程安全的,但上面的例子已经说明并非所有场景下Vector的操作都是线程安全的,但明明Vector又被公认为是线程安全的,这怎么解释?
由此,我们就可以定义狭义线程安全和广义线程安全:
1.狭义:对象的单个操作线程安全
2.广义:对象的单个操作和组合操作都线程安全
对于上面例子中的Vector要修改为广义线程安全,就需要在remove操作中做二次判断,如果容器中已经没有对象,就返回null,方法签名可以修改为existsAndRemove。
end.