什么是单例模式?
通过单例模式的方法创建的类在当前进程中只有一个实例,或者说在当前软件系统中只有一个实例,而且该类只提供一个取得对象实例的方法,一般重量级的类都会用到单例模式
饿汉式(静态常量式)
package com.jc.singleton.type1;/**
* @program: 设计模式
*
* @description:
*
* @author: Mr.Wang
*
* @create: 2021-10-01 22:53
**/
public class XXXFactory {
private final static XXXFactory single = new XXXFactory();
//防止外部new
private XXXFactory(){
}
//只声明一个public方法进行获取单例
public static XXXFactory getInstance(){
return single;
}
public static void main(String[] args) {
XXXFactory instance = getInstance();
XXXFactory instance1 = getInstance();
System.out.println(instance == instance1? "一样的实例":"不是一样的实例");//true
}
}
好:实现起来比较简单,利用了类加载的特性,避免了线程安全问题。
不好:因为他在类进行加载的时候就实例化了,所以他没有实现懒加载(“懒加载”又称为延迟加载,就是在开发过程中,程序启动的时候不立刻使用的资源先不加载,当程序运行中需要使用的时候再去加载它。),但是导致类加载的情况有很多种(获取类的静态变量,静态方法,加载子类时发现父类并没有加载等等),因此不能确定有其他的方式对类进行加载,所以他没有达到懒加载的效果。如果在使用这个类之前就对此类进行加载了,或者从始至终就没有使用到这个类,那么就造成了内存的浪费。
结论:通过这种方式实现单例模式,可能造成内存的浪费。
饿汉式(静态代码块式)
package com.jc.singleton.type1;/**
* @program: 设计模式
*
* @description:
*
* @author: Mr.Wang
*
* @create: 2021-10-01 22:53
**/
public class XXXFactory {
private static XXXFactory single;
static {
single = new XXXFactory();
}
//防止外部new
private XXXFactory(){
}
//只声明一个public方法进行获取单例
public static XXXFactory getInstance(){
return single;
}
public static void main(String[] args) {
XXXFactory instance = getInstance();
XXXFactory instance1 = getInstance();
System.out.println(instance == instance1? "一样的实例":"不是一样的实例");//true
}
}
也可以实现单例模式,优缺点和静态常量式相同。
但是和静态常量式有一丢丢不同,虽然都是利用了jvm的类加载机制,但是静态常量式是在编译的时候就进行分配内存了,而静态代码块则是在初始化阶段才进行分配内存。
懒汉式
package com.jc.singleton.type1;/**
* @program: 设计模式
*
* @description:
*
* @author: Mr.Wang
*
* @create: 2021-10-02 12:42
**/
class XXXFactory2{
private static XXXFactory2 single;
//私有化 防止外部new
private XXXFactory2(){
}
//在获取他实例的时候才进行加载 分配内存
public static XXXFactory2 getInstance(){
if (single == null){
single = new XXXFactory2();
}
return single;
}
}
public class SingletonTest2 {
public static void main(String[] args) {
XXXFactory2 instance = XXXFactory2.getInstance();
XXXFactory2 instance2 = XXXFactory2.getInstance();
System.out.println(instance == instance2);//true
}
}
为了解决懒加载的问题,于是有了懒汉式,在获取他的实例的时候才进行加载为其分配内存,达到了懒加载的效果,也不会浪费内存。
不好:但是!如果是多线程模式的话,一个线程进入到if (single == null),还没来的及往下执行,另一个线程也进入到这个语句就会通过,这样的话,single就被多次实例化了,都保证不了单例了!
懒汉式(线程安全)
package com.jc.singleton.type1;/**
* @program: 设计模式
*
* @description:
*
* @author: Mr.Wang
*
* @create: 2021-10-02 12:42
**/
class XXXFactory2{
private static XXXFactory2 single;
//私有化 防止外部new
private XXXFactory2(){
}
//在获取他实例的时候才进行加载 分配内存
//改:加锁就ok了
public synchronized static XXXFactory2 getInstance(){
if (single == null){
single = new XXXFactory2();
}
return single;
}
}
public class SingletonTest2 {
public static void main(String[] args) {
XXXFactory2 instance = XXXFactory2.getInstance();
XXXFactory2 instance2 = XXXFactory2.getInstance();
System.out.println(instance == instance2);//true
}
}
确实解决了之前的懒汉式的线程安全的问题。
不好:但是!getInstance方法被上锁了,也就意味着如果多个进行想要获得实例,必须一个一个拿,所以它的效率太低了。
额。。。一个一个拿确实效率很低,那我们能不能在getInstance中给局部上锁,达到一种第一个线程进来后,不让别的线程进来,让第一个线程实例化single,其他线程在进来的时候,直接获取就好了呢?即大哥干完了,小弟们坐享其果呢?继续往下看。
双重检查
class XXXFactory2{
private static volatile XXXFactory2 single;
//私有化 防止外部new
private XXXFactory2(){
}
//在获取他实例的时候才进行加载 分配内存
//改:加锁就ok了
//再改:方法内部加锁
public static XXXFactory2 getInstance(){
//大哥进来处理完后,小弟们直接就return了
if (single == null){
synchronized (XXXFactory2.class) {
if (single == null)
single = new XXXFactory2();
}
}
return single;
}
}
volatile关键字:会强制将修改的值立即写入主存;
大哥线程(第一个要获取单例的线程)是怎么处理的呢?
线程分好多种:
①方法外的,即还没进来的线程
②方法内,但是在第一条if(single == null) 之前的
③第一条 if(single == null) 和 synchronized (XXXFactory2.class) {之间的
具体怎么处理的? 在同一时刻,有好多线程进入到了这个方法内部,并都通过了if(single==null)这条语句,突然一个线程对大家说,我先进去看看怎么回事,你们先在这里等一等,于是大哥线程就进去了,发现single为null,于是他就new了,但是跟他一起进来的线程,还是会一个一个的进去,因为他们回不了头(代码不能向上执行),所以只能继续往前走,所以跟大哥线程同时进来的这些线程还是会有效率问题,但是,他们出去之后,后来的所有线程获取实例都是直接return single. 形象点说,第一批通过if(single == null)的线程们,为后边的所有线程都作出了贡献,准确的说,**是大哥线程 为这一批后边的所有线程作出了贡献**。为哪些线程作出了贡献?
①还没进来的线程和还没想要获取单例的线程
②方法内,但是在第一条if(single == null)之前的。(因为有volatile,所以他们都会读取到single不null)好: 在方法内部锁住局部代码即可,这样既解决了线程安全的问题,又达到了懒加载的效果,还保证了效率。
静态内部类
package com.jc.singleton;/**
* @program: 设计模式
*
* @description:
*
* @author: Mr.Wang
*
* @create: 2021-10-02 13:47
**/
public class Singleton {
private Singleton(){
}
//静态内部类
private static class SingletonInstance{
private static final Singleton single = new Singleton();
}
//获取单例的时候直接拿静态内部类中的single即可
public static Singleton getInstance(){
return SingletonInstance.single;
}
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);//true
}
}
静态内部类的特点时就是它的外部类被加载的时候他并不会被加载,他不会被加载也就意味着单例也不会马上被实例化,而是等到想要获取单例的时候,用到了静态内部类的属性,单例才会被实例化,达到了懒加载的效果。
类的静态属性只会在第一次加载类的时候初始化,,所以这里是利用了jvm的特性(加载类的时是线程安全的,别的线程无法进入)来帮我们避免了线程安全问题。
好:懒加载的效果,线程安全,效率高。
枚举
package com.jc.singleton;/**
* @program: 设计模式
*
* @description:
*
* @author: Mr.Wang
*
* @create: 2021-10-02 13:58
**/
public class SingleTest4 {
public static void main(String[] args) {
Singleton1 instance = Singleton1.INSTANCE;
Singleton1 instance2 = Singleton1.INSTANCE;
System.out.println(instance == instance2);//true
}
}
enum Singleton1{
INSTANCE;
}
也可以通过枚举来实现单例。
好:可以保证线程安全,而且还能防止反序列化重新创建新的对象,效率高。怎么防止的?
Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。在序列化的时候Java仅仅是将枚举对象的INSTANCE属性输入到字节流中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。
在JDK源码中的应用
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
典型的饿汉式。
单例模式的应用场景
需要频繁创建和销毁的对象,重量级对象,经常用到的对象,包括工具类对象,频繁访问数据库或文件的对象(如各种数据源,各种Factory)。