什么是单例
确保某个类在系统运行中只能产生一个唯一的实例。
来一个不太规矩的例子
新建Singleton类,并将其构造方法设为private,然后想办法获取这个类的实例。
public class Singleton {
private static Singleton uniqueInstance;
//私有的构造器,只有自己能访问,换言之只有自己能创建自己的实例
private Singleton(){
System.out.println("创建了一个实例");
}
public static Singleton getInstance(){
//如果当前实例为空,则创建一个,否则返回已经创建好的实例
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
System.out.println("得到了一个实例");
return uniqueInstance;
}
}
写一个测试类。
先试着传统的new一下,你会发现会报错的。因为Singleton的构造器是私有的,除了在他自己内部,谁也调用不了。
public class Test {
public static void main(String[] args){
//报'Singleton()' has private access in 'Singleton'错误
Singleton singleton = new Singleton();
}
}
所以我们只能拿过来已有的实例,而不是去创建。
public class Test {
public static void main(String[] args){
/*
* 这里写了五个,测试一下是不是只会得到一个实例
* */
Singleton singleton = Singleton.getInstance();
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
Singleton singleton3 = Singleton.getInstance();
Singleton singleton4 = Singleton.getInstance();
}
}
输出结果
创建了一个实例
得到了一个实例
得到了一个实例
得到了一个实例
得到了一个实例
得到了一个实例
通过结果发现,确实只是在第一次使用时创建了出一个实例,以后只是拿过来用了。
但是,仔细想一个问题,这是单线程的情况。如果有两个线程同时执行到if判断处,会怎样?
/*
* 加入两个线程同时到了判断处,发现还没有当前类的实例,所有都会执行到new
* 这样就会创建出两个实例,这就违背了我们的初衷
* */
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
System.out.println("得到了一个实例");
return uniqueInstance;
用多线程测试一下(需多次测试,会发现会有不同的结果)。
public class MyThread implements Runnable {
@Override
public void run() {
Singleton singleton = Singleton.getInstance();
}
}
public class Test {
public static void main(String[] args){
Thread thread = new Thread(new MyThread());
thread.start();
Thread thread1 = new Thread(new MyThread());
thread1.start();
Thread thread2 = new Thread(new MyThread());
thread2.start();
Thread thread3 = new Thread(new MyThread());
thread3.start();
Thread thread4 = new Thread(new MyThread());
thread4.start();
}
}
测试结果:
创建了一个实例
得到了一个实例
创建了一个实例
得到了一个实例
得到了一个实例
得到了一个实例
得到了一个实例
这里发现,确实违背了我们的初衷,在多个线程中,我们写的单例已经出了问题,现在系统中已经存在了两个实例。这种情况,我们也称之为非线程安全。所以刚刚这个例子称不上真正意义上的单例模式。
有点笨拙的单例模式
刚刚问题的根源是由于多个线程同时操作了一个资源,那么我们在访问时每次限定在一个线程就好了。
Java中有一个synchronized关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
所以我们给获取实例的方法,加上synchronized关键字。
public class Singleton {
private static Singleton uniqueInstance;
//私有的构造器,只有自己能访问,换言之只有自己能创建自己的实例
private Singleton(){
System.out.println("创建了一个实例");
}
//有了synchronized关键字时候,当多个线程同时访问时,一次只能接待一个线程,剩下的线程排队等候。
public static synchronized Singleton getInstance(){
//如果当前实例为空,则创建一个,否则返回已经创建好的实例
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
System.out.println("得到了一个实例");
return uniqueInstance;
}
}
测试结果
创建了一个实例
得到了一个实例
得到了一个实例
得到了一个实例
得到了一个实例
得到了一个实例
现在的结果是符合我们的定义的。
但是又有一个新问题出现,只有在第一次判断实例非空的时候才需要synchronized控制,避免第一次同时创建多个实例,第一次实例创建之后就不需要synchronized了,之后的每一获取实例,对于多线程来说将使得程序的性能大大降低。
懒汉与饿汉
这里引入一个懒汉与饿汉的概念。
- 懒汉:当使用到该实例时,才会去创建该类的实例。
- 饿汉:程序初始化时就会创建该实例。
所以,刚刚我们的代码属于“懒汉”模式,在运行时才会创建实例,所以会出现非线程安全的问题。那么如果设计成“饿汉”模式呢?让程序一开始就创建好该实例,提前就准备好,免去了判断的麻烦,以后用到的时候直接获取就可以了。
单例-饿汉
修改我们的代码
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
//私有的构造器,只有自己能访问,换言之只有自己能创建自己的实例
private Singleton(){
System.out.println("创建了一个实例");
}
public static synchronized Singleton getInstance(){
System.out.println("得到了一个实例");
//因为已经存在了实例,所以直接获取就可以了
return uniqueInstance;
}
}
测试运行结果
创建了一个实例
得到了一个实例
得到了一个实例
得到了一个实例
得到了一个实例
得到了一个实例
这样解决了非线程安全和性能慢的问题。但这种饿汉模式,也有一个缺点,因为在初始化时就创建了一个类的实例,所以会耗费一定的系统资源。