什么是单例模式?
单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
八种单例模式:
**1.饿汉式(静态常量)**
public class SingletonTest01 {
public static void main(String[] args) {
Singleton instance1=Singleton.getInstance();
Singleton instance2=Singleton.getInstance();
System.out.println(instance1==instance2);
}
}
//饿汉式(静态变量)
class Singleton{
//构造函数定义成private,禁止外部创建Singleton实例(下面的例子省略定义私有构造方法)
private Singleton1(){}
//1.本类内部创建对象实例
public static final Singleton instance=new Singleton();
//2.提供一个公有的静态方法,返回实例对象
public static Singleton getInstance(){
return instance;
}
}
优点:这种写法比较简单,就是在类装载的时候就完成实例化,避免了线程同步问题;
缺点:在类装载的时候就完成实例化,没有达到懒加载的效果,如果从始至终从未使用过这个实例,则会造成内存的浪费
2.饿汉式(静态代码块)
public class SingletonTest02 {
public static void main(String[] args) {
Singleton instance1= Singleton.getInstance();
Singleton instance2= Singleton.getInstance();
System.out.println(instance1==instance2);
}
}
//饿汉式(静态代码块)
class Singleton{
.....
//1.本类内部创建对象实例
public static Singleton instance;
static {
instance=new Singleton();
}
//2.提供一个公有的静态方法,返回实例对象
public static Singleton getInstance(){
return instance;
}
}
这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码快中的代码,初始化类的实例
3.懒汉式(线程不安全)
public class SingletonTest03 {
public static void main(String[] args) {
Singleton instance1= Singleton.getInstance();
Singleton instance2= Singleton.getInstance();
System.out.println(instance1==instance2);
}
}
class Singleton{
private static Singleton instance;
...
//提供一个静态的公有方法,当使用到该方法时,才会创建instance
//懒汉式写法,线程不安全
public static Singleton getInstance(){
if (instance==null){
instance=new Singleton();
}
return instance;
}
}
起到了懒加载的效果,但是只能在单线程下使用
4.懒汉式(线程安全)
public class SingletonTest04 {
public static void main(String[] args) {
Singleton instance1= Singleton.getInstance();
Singleton instance2= Singleton.getInstance();
System.out.println(instance1==instance2);
}
}
class Singleton{
private static Singleton instance;
...
//懒汉式线程安全写法
public static synchronized Singleton getInstance(){
if (instance==null){
instance=new Singleton();
}
return instance;
}
}
解决了线程安全问题;
效率太低了,每个线程在想获得类的实例的时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低
接下来介绍推荐使用的方法
5.双重检查
public class SingletonTest05 {
public static void main(String[] args) {
Singleton instance1= Singleton.getInstance();
Singleton instance2= Singleton.getInstance();
System.out.println(instance1==instance2);
}
}
class Singleton{
private static Singleton singleton;
...
public static Singleton getInstance(){
//双重检查 适合多线程+有懒加载机制
if(singleton==null){
synchronized (Singleton.class){
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
当还没有实例 且 多线程的情况下 都进来第一层if判断—>通过, 然后第一个抢占到锁的线程经过第二层if判断—>通过,然后创建了实例 ;第二个线程经过第二次if判断—>发现有实例—->退出,以后任何线程都直接返回第一次创建的实例
6.静态内部类
class Singleton{
private static Singleton singleton;
...
public static class SingletonInstance{
//静态内部类实现 线程安全+懒加载
public static final Singleton INSTANCE =new Singleton();
}
//提供一个公有的方法 返回该类实例
public static Singleton getInstance(){
return SingletonInstance.INSTANCE;
}
}
为什么要使用内部类?
1.如果一个类A只为另一个类B服务,那把A嵌套在B中,就相当于B的一个服务类,代码可读性更高,方便维护。
2.如果一个类A要调用另一个类B,但B又不想被其他类所引用,B就要申明为private,这样A就调用不了B,于是把A嵌套在B中,就可以同时满足前面的两个要求了,代码的封装性更好了。
静态内部类和非静态内部类的区别:
其实我们可以把静态内部类看成和外部类平级的一个类,我们在调用静态内部类时甚至都不用初始化外部类。
静态内部类和非静态内部类一样,都不会因为外部类的加载而加载。
静态内部类的加载不同于非静态内部类,静态内部类使用时就会加载,但如果想实例化一个非静态内部类,则必须先实例化外部类,然后根据外部类的实例就可以创建非静态内部类实例
该方式的优点:
外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存—->懒加载。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
补充(类的加载在JVM篇):
7.枚举(最推荐)
public class SingletonTest07 {
public static void main(String[] args) {
Singleton instance = Singleton.INSTANCE;
}
}
enum Singleton{
INSTANCE;
Singleton(){
}
}
这借助了JDK1.5中添加的枚举来实现单例模式,不仅能避免多线程同步问题,代码简洁,而且还能防止反序列化重新创建新的对象
补充:什么是枚举?
我们学习过单例模式,即一个类只有一个实例。而枚举其实就是多例,一个类有多个实例,但实例的个数不是无穷的,是有限个数的。我们称呼枚举类中实例为枚举项。一般一个枚举类的枚举项的个数不应该太多,如果一个枚举类有30个枚举项就太多了!
2.定义枚举类型
定义枚举类型需要使用enum关键字,例如:
public enum Direction {
FRONT, BEHIND, LEFT, RIGHT;
}
Direction d = Direction.FRONT;
注意,定义枚举类的关键字是enum,而不是Enum,所有关键字都是小写的!
其中FRONT、BEHIND、LEFT、RIGHT都是枚举项,它们都是本类的实例,本类一共就只有四个实例对象。
在定义枚举项时,多个枚举项之间使用逗号分隔,最后一个枚举项后需要给出分号。不能使用new来创建枚举类的对象,因为枚举类中的实例就是类中的枚举项,所以在类外只能使用类名.枚举项。
3.枚举与switch
枚举类型可以在switch中使用
Direction d = Direction.FRONT;
switch(d) {
case FRONT: System.out.println(“前面”);break;
case BEHIND:System.out.println(“后面”);break;
case LEFT: System.out.println(“左面”);break;
case RIGHT: System.out.println(“右面”);break;
default:System.out.println(“错误的方向”);
}
Direction d1 = d;
System.out.println(d1);
注意,在switch中,不能使用枚举类名称,例如:“case Direction.FRONT:”这是错误的,因为编译器会根据switch中d的类型来判定每个枚举类型,在case中必须直接给出与d相同类型的枚举选项,而不能再有类型。
4.所有枚举类都是Enum的子类
所有枚举类都默认是Enum类的子类,无需我们使用extends来继承。这说明Enum中的方法所有枚举类都拥有。
5.枚举类的构造器
枚举类也可以有构造器,构造器默认都是private修饰,而且只能是private。因为枚举类的实例不能让外界来创建!
enum Direction {
FRONT, BEHIND, LEFT, RIGHT;
Direction()//枚举类的构造器不可以添加访问修饰符,枚举类的构造器默认是private的。但你自己不能添加private来修饰构造器
{
System.out.println("hello");
}
}
其实创建枚举项就等同于调用本类的无参构造器,所以FRONT、BEHIND、LEFT、RIGHT四个枚举项等同于调用了四次无参构造器,所以你会看到四个hello输出。
6.其实枚举类和正常的类一样,可以有实例变量,实例方法,静态方法等等
为什么说枚举可以解决线程安全问题?
public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
}
反编译后:
public final class T extends Enum
{
//省略部分内容
public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
线程安全原理同
那么枚举类的防止反序列化创建实例是为什么?
普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。