说起单例模式,大家应该都可以熟悉单例模式的三种不同情况吧:饿汉模式、懒汉模式以及DCL模式。以下首先分别对三种模式呈上相应的代码。
1.几种单例模式的形式
饿汉式:
public class Singleton1 {
private static final Singleton1 singleton1=new Singleton1();//饿汉形式,初始化时就进行初始化对象,并且声明不变性,可以保证线程安全。
private Singleton1() {}
public static Singleton1 getInstance() {
return singleton1;
}
}
饿汉式则在类加载过程中就对单例对象进行初始化创建对象,同时保证了线程安全。
懒汉式:
懒汉式有两种形式,一种是线程不安全的一种是线程安全的。
线程不安全:
public class Singleton2 {
private Singleton2() {}
private static Singleton2 singleton2=null;//为了延迟加载
public static Singleton2 getInstance() {
if(singleton2==null)
singleton2=new Singleton2();
return singleton2;
}
}
线程安全:
public class Singleton3 {
private Singleton3() {}
private static final class Holder{
static final Singleton3 singleton3=new Singleton3();
}
public static Singleton3 getInstance() {
return Holder.singleton3;
}
}
线程不安全形式只考虑了延迟加载,但是是线程不安全的,而后者则使用了静态内部类声明了一个不变的类对象,既实现了延迟加载又保证了线程安全。
双重加锁
public class Singleton4 {
private volatile static Singleton4 singleton4=null;
private Singleton4() {}
public static Singleton4 getInstance() {
if(singleton4==null)
{
synchronized (Singleton4.class) {
if(singleton4==null)
singleton4=new Singleton4();
}
}
return singleton4;
}
}
可以看出DCL版本的既要使用volatile保证可见性,还需要用synchronized保证线程安全,同时需要两次检测对象是否为空值,避免同一时间进入的两个线程引起的线程安全问题。
2.DCL到延迟初始化占位类模式
我们一直认为DCL是一种很安全并能实现延迟加载的模式,但是这其中使用了内置锁,使用锁需要一定的开销。并且DCL形式的单例模式在理解上不是一目了然的。结合以上,我们还是比较提倡延迟初始化占位类模式,能够实现DCL同样的功能,并且不需要考虑同步问题。该方法就是通过一个静态内部类通过在静态内部类中声明一个不变的静态类变量,并对其进行初始化,然后在外部类的静态方法中直接返回该内部类的静态变量即可。这样通过保证初始化过程中的安全性,从而保证访问共享变量的安全性。这里不需要使用同步,可以一定程度上减少同步带来的开销,并且代码易于理解。以下通过一个例子来通过共享数据初始化的安全性保证线程间访问该数据的安全性。
public class Num {
private static final class GetNum{
static final int ans=5;
}
public static int getNum() {
return GetNum.ans;
}
}
以上是一个Num类,类中有一个静态内部类GetNum类,里面有一个不变静态变量ans,并对其进行初始化,该初始化过程是安全的,类加载过程中,便对该静态变量进行初始化,并且是不可变的。所以不存在线程安全问题。接下来用代码进行测试:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class TestNum {
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
ExecutorService eService=Executors.newFixedThreadPool(5);
final AtomicInteger at=new AtomicInteger(0);
final CountDownLatch cdl=new CountDownLatch(20);
final Num num=new Num();
for(int i=0;i<20;i++)
{
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
cdl.countDown();
System.out.println(num.getNum()+at.getAndAdd(1));
}
});
thread.start();
//eService.execute(thread);
}
cdl.await();
System.out.println("最终结果是:"+(num.getNum()+at.get()));
}
}
以上有二十个线程,并且每个线程依次对该共享数据ans进行加一个递增数,为了避免使用同步,这里用了另一种方法保证递增的安全性,即通过原子类保证操作的原子性,从而保证线程的安全。运行结果如下:
6
10
9
7
8
5
11
13
14
12
15
16
17
18
21
19
20
22
23
24
最终结果是:25
一般保证线程安全简单方法就是通过加锁同步,但是会性能会有影响,所以我们应该从底层以及从其它方法避免加锁来保证线程安全,比如上面例子中的延迟初始化占位类模式加上原子类操作保证了线程安全,同时性能比较好。因此,在单例模式中,我们可以避免使用DCL模式,而是多使用延迟初始化占位类模式的单例模式。