Java中线程安全级别:
1.不可变的(immutable):
不可变的类的实例是不能被修改的,每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。不可变类不需要外部的同步,常见例子有String,long和BigInteger。
为了使类成为不可变,需要遵循下面五条规则:
(1).不提供任何会修改对象状态的方法(如setter方法等)。
(2).保证类不会被扩展,最常用的做法是将类声明为final类型。
(3).使所有的域都是final。
(4).使所有的域都成为私有的。
(5).确保对于任何可变组件的互斥访问。
如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些可变对象的引用,且永远不要用客户端提供的对象引用来初始化这样的域,不要从任何访问方法中返回该对象的引用,在构造器和访问方法中使用保护性拷贝。
下面使用一个复数类来演示不可变类:
public final class Complex{
private final double re;//复数的实部
private final double im;//复数的虚部
public Complex(double re, double im){
this.re = re;
this.im = im;
}
public double realPart(){
return re;
}
public double imaginaryPart(){
return im;
}
public Complex add(Complex c){
return new Complex(re + c.re, im + c.im);
}
public Complex subtract(Complex c){
return new Complex(re - c.re, im - c.im);
}
public Complex multiply(Complex c){
return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
}
public Complex divide(Complex c){
double tmp = c.re * c.re + c.im * c.im
return new Complex((re * c.re + im * c.im)/tmp, ( im * c.re - re * c.im)/tmp);
}
}
2.无条件的线程安全:
无条件的线程安全类的实例是可变的,但是这个类有着足够的内部同步,所以它的实例可以被并发使用,无需任何外部同步。JDK中的Random和ConcurrentHashMap就是无条件线程安全的类。
3.有条件的线程安全:
有条件的线程安全类的实例除了有些方法为了进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全相同。JDK中Collections.synchronized包装的同步集合,Vector,HashTable,StringBuffer等就是有条件的线程安全类。
Collections.synchronized使用装饰者模式来包装非线程安全的集合容器,在这些非线程安全集合容器的公共方法上添加synchronized同步关键字,将方法变成线程安全。
有条件的线程安全集合在多线程操作时一般不需要同步,但是在遍历(集合产生的迭代器也必须要求外部同步)的时候必须要求外部同步,否则如果遍历过程中涉及到添加或者删除等改变集合容器大小的操作时可能会产出例如ArrayIndexOutOfBoundException。
有条件线程安全的例子:
Map<k, v> m = Collections.synchronizedMap(new HashMap<k, v>());
……
Set<k> s = m.keySet();
……
synchronized(m){
for(k key : s){
key.f();
……
}
}
4.非线程安全:
非线程安全类的实例是可变的,为了并发地使用这些类,调用者必须使用外部同步包围每个调用序列。JDK中的ArrayList,HashMap,StringBuilder等都是非线程安全的例子。
只有在多线程共享的情况下才需要对非线程安全类进行外部同步,如果非线程安全的类不会被多个线程共享,则同样不需要考虑线程安全的同步问题。
5.线程对立:
即使所有的方法调用都被外部同步包围,线程对立类也不能安全地被多个线程并发地使用。
线程对立的根源在于没有同步地修改静态数据,JDK中的System.runFinalizersOnExit方法以及线程中的suspend,stop,resume方法就是线程对立的,这些方法现在已经被废弃。
私有所对象模式:
当一个类承诺了使用一个共有可访问的所对象时,就意味着允许客户端以原子方式执行一个方法调用序列,若方法中使用了类对象作为公有可访问锁,对于一般的应用已经足够了,但是并发集合使用的并发控制机制并不能与高性能的并发控制相兼容,如果客户端在超时还保持共有可访问锁不释放,就会引起拒绝服务攻击(DDos),为了避免拒绝服务攻击,需要使用一个私有锁对象:
private final Object lock = new Object();
public void test(){
synchronized(lock){
……
}
}
由于私有锁对象不能被外部调用者所访问,并且由于私有锁对象是final的,保证锁对象内容不会被修改,私有锁模式可以避免子类和基类中同步方法相互干扰的问题,因此私有锁对象模式特别适合于专门为集成而设计的类以及无条件线程安全的类。
延迟初始化:
延迟初始化是延迟到需要域的值时才将它初始化的行为,如果永远不需要这个值,则这个域就永远不会被初始化。延迟初始化既适用于静态域,也适用于实例域。
(1).正常初始化的实例域:
//静态域
private static final FieldType field1 = computeFieldValue();
//非静态域
private final FieldType field2 = computeFieldValue();
(2).使用同步方法延迟初始化:
private FieldType field;// 也可以对静态域延迟初始化private static FieldType field;
synchronized FieldType getField(){
if(field == null){
field = computeFieldValue();
}
return field;
}
同步方法通常比非同步同步方法速度慢,因此使用同步方法的延迟初始化其实并不见得能提高性能。
(3). 静态域的Lazy initialization holder class模式:
延迟初始化持有类模式又叫按需初始化持有类模式,其本质是使用一个静态内部类来持有要延迟初始化域引用,例子如下:
private static class FieldHolder{//静态内部类
static final FieldType field = computeFieldValue();
}
static FieldType getField(){
return FieldHolder.field;
}
只有当getField方法第一次被调用时,FieldHolder.field引起静态内部持有类FieldHolder初始化,在加载FieldHolder类的时候初始化其静态域,由于是静态域,因此只会被java虚拟机在加载时初始化一次,并由虚拟机保证线程安全。该模式的最大优势在于避免使用同步方法,在保持延迟初始化的同时没有增加额外的访问开销。
(4).实例域双重检查模式延迟初始化:
双重检查模式避免了在域被初始化之后访问该域时锁定开销,其双重检查发生在:
第一次检查:没有锁定,检查域是否被初始化。
第二次检查:锁定,只有当第二次检查时表明域没有被初始化,才会对域进行初始化。
例子如下:
private volatile FieldType field;
FieldType getField(){
FieldType result = field;
if(result == null){//第一次检查
synchronized(this){
result = field;
if(result == null){//第二次检查
field = result = computeFieldValue();
}
}
}
return result;
}
双重检查中域声明为volatile很重要,volatile关键字强制禁止java虚拟机对指令乱序执行,在JDK1.5之前由于不同的java虚拟机内存模型对volatile关键字实现支持不同,导致双重检查不能稳定正常运行,JDK1.5之后引入的内存模式解决了这个问题。
局部变量result确保实例域field只在已经被初始化的情况下读取一次,虽然不是严格要求,但是可以提升性能。双重检查模式将同步代码范围缩小,减少了实例域访问开销。
注意没有必须对静态域使用双重检查模式,延迟初始化持有类模式更加优雅强大。
(5).实例域单重检查模式:
如果延迟初始化的实例域可以接受重复初始化的实例域,则可以使用单重检查模式来代替双重检查模式,例子如下:
private volatile FieldType field;
FieldType getField(){
FieldType result = field;
if(result == null){//只检查一次
field = result = computeFieldValue();
}
return result;
}
注意单重检查模式中域实例变量依然要被声明为volatile,由于可以接受重复初始化,因此去掉了第二次同步检查,没有同步的方法大大减少了访问开销。
虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例初始化中的有害循环。和大多数的优化一样,对于延迟初始化,最好的建议是“除非绝对必要,否则就不要延迟初始化”。延迟初始化是一把双刃剑,它降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销,考虑到延迟初始化的域最终需要初始化的开销以及域的访问开销,延迟初始化实际上降低了性能。