单例模式是一种比较常用且相对简单的设计模式,首先我们来明确什么是单例模式,顾名思义“单例”就是单个实例,也就是说“在我们的系统中某个类只能存在一个对象实例,并且这个对象实例是私有的并且不能通过外部new()的方式创建这个对象,内部需要提供一个公共方法去取到这个对象实例”。
接下来我们看作用,单例到底有什么用它的存在意义是什么呢?我们知道单例只有一个实例,在某些业务中这样可以避免不同实例之间的冲突,以及减少内存的浪费,例如在一个资源监控组件中对同一个资源的多个监控器可能会造成显示差异以及内存浪费。
一个单例模式需要满足以下条件:
1、内部创建一个私有化对象
2、私有化该类的构造器防止外部创建对象
3、提供外部访问私有对象的公共方法
然后我们来看实现,单例模式的实现主要分为两种模式“懒汉式”以及“饿汉式”,他们的主要区别在于实例的创建时机下面我们来详细讨论。
1、饿汉式单例模式
饿汉式单例模式下对象实例是这个类加载时去创建这个对象实例,之后需要使用这个对象通过提供的公共方法去获取这个已经创建好的对象,我们来看下面一段代码
public class SingletonHungry {
private static final SingletonHungry singleton=new SingletonHungry();
private SingletonHungry(){} //私有化构造器防止外部创建对象
public static SingletonHungry getInstance(){
return singleton;
}
}
这就是一个饿汉式单例模式的简单实现,我们可以看到其中包含了一个私有的静态常量实例,一个将默认构造器重写为私有的构造器,以及一个获取实例的公共方法。下面我们来测试它。
class Main{
public static void main(String[] args) {
SingletonHungry instance0 = SingletonHungry.getInstance();
SingletonHungry instance1 = SingletonHungry.getInstance();
if (instance0==instance1){
System.out.println("我们指向同一个实例");
}
}
}
我们可以看到instance0;instance0两个引用确实指向了同一个实例
由于是在类加载阶段创建对象,所以这种方法的好处在于所以不存在线程同步问题,缺点在于有很多情况会造成类的加载,可能会造成内存的浪费。
2、懒汉式单例模式
懒汉式单例模式在于实例是在使用时去创建,实现了懒加载避免可能出现的内存浪费,我们来看下面一段代码。
2.1同步方法
public class SingletonLazy {
private static SingletonLazy singletonLazy;
private SingletonLazy(){}
public synchronized static SingletonLazy getInstance(){
if(singletonLazy==null){
singletonLazy=new SingletonLazy();
}
return singletonLazy;
}
}
这是懒汉式单例模式的一种实现,我们在getInstance方法中创建实例避免了可能出现的内存浪费,并且加入synchronized解决了多线程下的线程安全为题,但是这种方法每次调用getInstance方法都会执行同步逻辑,这样就大大降低了程序的效率所以有了下面的方法。
2.2双重检查(推荐使用)
对于上面的方法我们可以采用双重检查的方式来降低同步方法对效率的影响,我们来看下面代码。
public class SingletonLazy {
private static volatile SingletonLazy singletonLazy;
private SingletonLazy(){}
public static SingletonLazy getInstance(){
if(singletonLazy==null){
synchronized (SingletonLazy.class){
if (singletonLazy==null){
singletonLazy=new SingletonLazy();
}
}
}
return singletonLazy;
}
}
我们可以看到我们将同步加在了代码块中对这个类进行同步,这样就避免了每次调用方法都会执行同步逻辑。并且我们在创建对象前进行了两次对实例的判断,一次在同步前,一次在同步后。这样当还没有创建实例时,如果有两个线程同时执行这个方法的话,就会使两个线程排队执行,第一个线程执行完毕后第二个线程在执行同步代码块中的逻辑遇到判断直接返回,避免了线程不安全;当这个实例已经创建时,其他线程再来执行这个方法就会在第一个判断之后直接返回不进行同步代码块的执行,解决了效率问题,并且加入了轻量化的volatile修饰对象实例保证了线程间对该对象的可见性,每次对该对象进行修改时可立刻将更新刷入主存。
2.3补充
回到2.1如果我们去掉方法上的synchronized会怎样呢,我们看下面一段代码
public class SingletonLazy{
private static SingletonLazy singletonLazy;
private SingletonLazy(){}
public static SingletonLazy getInstance() throws InterruptedException {
if(singletonLazy==null){
System.out.println("啊我睡了");
Thread.sleep(200);
singletonLazy=new SingletonLazy();
}
return singletonLazy;
}
}
class Creater implements Runnable{
SingletonLazy instance;
@Override
public void run() {
try {
instance = SingletonLazy.getInstance();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public SingletonLazy getIt(){
return instance;
}
}
class Use{
public static void main(String[] args) throws InterruptedException {
Creater creater0=new Creater();
Creater creater1=new Creater();
Thread thread0=new Thread(creater0,"A");
Thread thread1=new Thread(creater1,"B");
thread0.start();
thread1.start();
Thread.sleep(300);
if (creater0.instance==creater1.instance){
System.out.println("我们指向同一实例");
}else {
System.out.println("我们不一样~");
}
}
}
结果显然是不对的,这样就不能保证线程安全了,这个类也就不是单例类了。