什么是单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
使用场景
当您想控制实例数目,节省系统资源的时候(即内存中只有一份实例对象)。
实现方式
-
构造方法私有化(不允许其他程序new对象)
我们知道创建对象的时候一般是通过new关键字,使用类的构造器生成实例对象,例:new Object(); public Object() { }; 我们之所以能够通过这种方式创建对象是因为构造器的修饰符是Public;所以第一步就要把构造器私有化,即private Object() { } 。
-
在类内部创建类的实例
因为构造器被私有化了,除了在类的内部能使用,其他地方都使用不了,所以类内部要创建一份实例对象。
3.让外部能获取类中的实例对象。
实现代码
这里分为饥饿加载和懒加载,先看饥饿加载的方式
可用
public class Test{
//内部提供实例
private static Test test = new Test();
//所有的构造方法私有化,
private Test(){}
//提供对外的静态方法
public static Test getInstance(){
return test;
}
}
可用
public class Test{
//上面的变换写法,这里是通过静态代码块做初始化
private static Test test = null;
private Test(){}
static{
test = new Test();
}
public static Test getInstance(){
return test;
}
}
提问1:外部类可以通过类名.静态属性,和类名.静态方法的方式进行访问,为什么不把实例对象test 定义为public,变成下面这种方式?(答案见最后)
public class Test{
//将 private 变为 public
public static Test test = new Test();
private Test(){}
//public static Test getInstance(){
// return test;
//}
}
懒加载
不可用,线程不安全
public class Test{
private static Test instance = null;
private Test(){}
//只有在第一次调用getInstance()方法时才会生成实例对象
public static Test getInstance(){
//多个线程访问的时候,比如:线程a进入if(instance == null)时instance为null,
//进入if内部,但还没有执行instance = new Test();,
//线程b这时进入if(instance == null)这时因为线程a还没有执行instance = new Test();
//所以instance也是null,也进入了if内部,导致线程a,b各创建了实例一次,
// a创建instance = new Test();b创建instance = new Test();明显b创建的对象会将
//a创建的对象替换掉了
//这时a创建的对象也在内存中,b创建的对象也在内存中,这就有多个对象了。
if(instance == null){
instance = new Test();
}
return instance ;
}
}
线程安全,效率低
public class Test{
private static Test instance = null;
private Test(){}
//对getInstance()方法加锁,因为同时只能有一条线程执行这个方法,所以效率低
public static synchronized Test getInstance(){
if(instance == null){
instance = new Test();
}
return instance ;
}
}
线程不安全,不可用
public class Test{
private static Test instance = null;
private Test(){}
//对getInstance()方法加锁,因为同时只能有一条线程执行这个方法,所以效率低
public static synchronized Test getInstance(){
//这里不是加锁了吗,为什么说线程不安全
//上面说过多个线程可以同时进入if内,这里也是一样,线程a先获取锁,先执行instance = new Test();
//然后程b获取锁又执行了一次instance = new Test();
//这里你有没有要把if的判断放到synchronized{}内部的想法,下面这种就是你所想的。
if(instance == null){
synchronized(Test.class){
instance = new Test();
}
}
return instance ;
}
}
(双重校验锁)线程安全,推荐用
public class Test{
private static Test instance = null;
private Test(){}
//对getInstance()方法加锁,因为同时只能有一条线程执行这个方法,所以效率低
public static synchronized Test getInstance(){
//这里其实只要synchronized内部的if判断就够了,为啥外部还要再放一个?
//个人认为是:instance对象已经存在了的情况下,避免其他进程获取锁,因为这会造成线程的调度
//(也称上下文切换)记住这句话,多线程对锁资源的竞争会引起上下文切换,锁竞争导致的
//线程阻塞越多,上下文切换就越频繁,系统的性能开销就越大(不理解的话那就六个字:影响系统性能)
if(instance == null){
synchronized(Test.class){
if(instance == null){
instance = new Test();//这里会有指令重排序,所以还可以改进为用volatile修饰
//private static volatile Test instance = null;
}
}
}
return instance ;
}
}
(内部类)线程安全,推荐用
public class Test{
private Test(){}
//在Test类被装载时,内部类TestHolder不会被实例化,只有在调用getInstance()方法时才会。
//当多个线程调用getInstance()方法时,为什么这里不会线程安全的问题?
//类加载机制保证初始化线程时只有一个线程,本人能力有限,类加载机制自行百度
private static class TestHolder(){
private static Test instance = new Test();
}
public static Test getInstance(){
return TestHolder.instance ;
}
}
(枚举)线程安全,极推荐用
//为啥枚举可以实现线程安全和单例呢
public enum SingletonEnum {
instance;
private SingletonEnum(){}
public static void method(){//方法要是静态方法
System.out.println("被调用的方法体");
}
}
jdk1.5的时候引入了枚举类型,下面是jdk1.5之前的写法,枚举的实现原理和下面一致,只是使用了一些语法糖简化了书写
public class SingletonEnum {
public static final SingletonEnum instance = new SingletonEnum ();
//2、构造方法私有化
private SingletonEnum(){}
}
Java 单例模式案例(Runtime,)
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
答疑解惑
提问1:外部类可以通过类名.静态属性,和类名.静态方法的方式进行访问,为什么不把实例对象test 定义为public,变成下面这种方式?
public class Test{
//将 private 变为 public
public static Test test = new Test();
private Test(){}
//public static Test getInstance(){
// return test;
//}
}
答:咦,这个是不是跟我们刚刚看过的枚举在jdk1.5之前的写法类似啊,稍有不同的是少了个final关键词,这个有什么影响呢。final 修饰的变量是不能修改的,这就意味着我们的test对象是可以被修改的,构造方法都被私有化了,还怎么创建一个Test对象来修改呢,Java里创建对象的方式有四种,new关键字只是其中之一,另外三种是:反射,反序列化,克隆。我还想到一种就是Test的子类,可以把子类对象赋给父类的引用,利用多态的特性向上转型了,不过这种的话,不是导致Test对象有多个,而是获取的对象已经被更改了(对象地址不同)。