Java设计模式中有许多种,但是属单例模式的实现较为简单,所以在面试的时候面试官也经常让面试者手写单例模式,并且日常的开发工作,学会单例模式也是必须的,所以这篇文章好好记录一下单例模式的实现。
概念
单例模式需要确保某一个类只有一个实例,并且提供一个全局访问点
其实我们桌面上的微信只能打开一个账号,就有点类似单例模式的设计
Static关键字
在实现单例模式之前,了解static
关键字是非常必要的,因为这个关键字是单例模式中特别重要的组成部分
static修饰的变量,方法以及代码块,在加载类的时候就会被加载进内存,也就是说就算该类没被实例化,static修饰的变量,方法以及代码块都可以被调用
- static修饰的变量在该类中,所有对象都只维护一份值,并且只要有一个对象改变了该值,其他所有对象所对应的值也发生改变
public class StaticDemo {
static int i;
public static void main(String[] args) {
// 对象实例化之前static修饰的变量已经可以被调用了
StaticDemo.i = 3;
StaticDemo singletonDemo = new StaticDemo();
System.out.println(singletonDemo.i);
// 一个对象修改static变量的值,其他对象也一起跟着改变
singletonDemo.i = 2;
System.out.println(singletonDemo.i);
StaticDemo singletonDemo1 = new StaticDemo();
System.out.println(singletonDemo1.i);
}
}
// output
3
2
2
- static修饰成员方法
static修饰成员方法和变量是一样的,只是方法不像变量有固定的值,其他的基本一致
1.一方面static方法在加载类的时候就已经被加载了,所以就算类没被实例化也可以调用static方法
2.因为static方法在类没实例化的时候就已经可以被调用了,所以在static方法中不可以对类中的非static变量和方法进行调用与操作,毕竟在类没实例化之前这些非static的变量和方法都还没被加载
- static修饰代码块
static修饰的代码块,带有static关键字的特征,在类加载的时候就会被执行一次,并且不管实例化多少对象,都会只执行一次。
public class StaticDemo {
static{
System.out.println("Part of static code");
}
public static void main(String[] args) {
}
}
在上面代码中main函数中我啥也不做,他也会有以下输出,因为编译的时候回加载StaticDemo这个类,所以这部分代码一定会被执行,并且只执行一次,之后就算实例化对象这段代码也不会被执行了。
看到这里,static跟单例对象确实有很多相似之处,其实怎么设计单例模式在脑子里大概也有个雏形了吧
单例模式实现(Java)
首先,实现单例模式的目标是我们要有一个类,并且让这个类只能被实例化一次。
那么联系到Static关键字的特点,最简单的单例模式实现就出现了
饿汉模式
- 饿汉
public class SingletonDemo {
public static SingletonDemo instance = new SingletonDemo();
private SingletonDemo(){}
public static SingletonDemo getSingletonDemo(){
return instance;
}
}
我们来看看单例模式的类是什么样子。
首先,构造函数直接给他的权限是private
这样一来,别人都不能用过new
函数来实例化这个类了,这样就保证了,大家想要得到单例类只能统一通过我规定的途径,而这个途径就是用public static
修饰的getSingletonDemo
方法,他会返回一个instance
而这个instance
就是单例化类的唯一一个实例化对象。
- 饿汉(另外一种实现方法)
public class SingletonDemo {
public static SingletonDemo instance = null;
static {
instance = new SingletonDemo();
}
private SingletonDemo(){
}
public SingletonDemo getSingletonDemo(){
return instance;
}
}
看到这里如果上面的static
关键字看明白了其实就很好理解了
饿汉模式的有点就是不用管线程问题,反正不管你多少给线程来我这个对象就一个,而且已经创建好了,你们自己折腾吧
饿汉模式的缺点很明显,我不管你用不用这个单例,我反正先给他实例化了,所以会很占空间。
懒汉模式
- 懒汉非线程安全模式
public class SingletonDemo {
public static SingletonDemo instance = null;
private SingletonDemo(){
}
public SingletonDemo getSingletonDemo(){
if (instance == null){
instance = new SingletonDemo();
}
return instance;
}
}
跟饿汉不同的是懒汉在有人需要用到单例对象的时候才会去实例化这个全局唯一的对象,虽然在单线程程序中没啥问题,但是如果多线程的话,很明显会因为线程间的通信问题造成实例化多次,那么也就是说不通线程中的这个对象其实并不是同一个,那么也就违背了单例模式的初衷了。
- 懒汉线程安全
那有问题肯定就有解决问题了,多线程问题最简单的就是上锁了
public class SingletonDemo {
public static SingletonDemo instance = null;
private SingletonDemo(){
}
public synchronized SingletonDemo getSingletonDemo(){
if (instance == null){
instance = new SingletonDemo();
}
return instance;
}
}
双重检验锁
- 单锁检验
懒汉模式是肯定能保证该类的单例化的,但是就算上锁了,懒汉模式锁函数代价太大这个需要优化
public class SingletonDemo {
public static SingletonDemo instance = null;
private SingletonDemo(){
}
public SingletonDemo getSingletonDemo(){
if (instance == null){
synchronized (SingletonDemo.class){
instance = new SingletonDemo();
}
}
return instance;
}
}
到这一步虽然看上去好像没什么问题,但是因为锁的颗粒度太细了,多线程的安全也没那么靠谱,最简单的情况就是两个线程同时到达if
的位置,其中一个线程获得锁,另外一个等待,但是等前一个线程获取完了之后,if
判断已经得到结果是null
了,这个时候第二个线程会得到锁之后继续实例化。
- 双重检验锁
public class SingletonDemo {
public static volatile SingletonDemo instance = null;
private SingletonDemo(){
}
public SingletonDemo getSingletonDemo(){
if (instance == null){
synchronized (SingletonDemo.class){
instance = new SingletonDemo();
}
}
return instance;
}
}
我们都知道volatile
是为了保证线程间的可见性,如果instance
用volatile
了的话,在准备实例化的时候线程会去内存更新instance
的最新数据,这个时候可以保证不会重复实例化。