单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
注意:
1、单例类只能在一个jvm实例中有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
接下来我们介绍一下我们常见到的几种单例模式:
正如其名"饿汉" ,类加载的时候就实例化,并且创建单例对象。优点: 先天性线程安全 当类初始化的时候 该对象就会被创建
缺点 : 如果项目中使用过多的饿汉式就会发生问题,项目启动会很慢,存放在方法区中占用内存较大
public class demo1 {
/*
饿汉式 : 优点 先天性线程安全 当类初始化的时候 该对象就会被创建
缺点 : 如果项目中使用过多的饿汉式就会发生问题,项目启动会很慢,存放在方法区中占用内存较大
*/
private static demo1 singleTon = new demo1();
private demo1(){
}
public static demo1 getInstance(){
return singleTon;
}
}
只有在需要的时候才会加载 但是在多线程的情况下 会出现线程安全的问题. 实例对象可能被初始化多次
public class demo2 {
private static demo2 singleTon;
//懒汉式的单例模式
private demo2(){
}
public static demo2 getInstance(){
if(singleTon==null){
singleTon = new demo2();
}
return singleTon;
}
}
对于饿汉式它会存在,在多线程的情况下,线程不安全的问题,实例化的对象(单例对象)可能被实例化多次,这里我们做一个测试
public class test {
public static void main(String[] args) {
for(int i=0;i<300;i++){
new Thread(new Runnable() {
public void run() {
demo2 singleTon = demo2.getInstance();
System.out.println(Thread.currentThread().getName()+","+singleTon);
}
}).start();
}
}
}
public class demo2 {
private static demo2 singleTon;
//懒汉式的单例模式
private demo2(){
}
public static demo2 getInstance(){
if(singleTon==null){
try{
//这里线程睡眠更容易看到效果
Thread.sleep(2000);
}catch(Exception e){
}
singleTon = new demo2();
}
return singleTon;
}
}
结果:
Thread-14,单例模式.service.懒汉式.demo2@32b9f1f0
Thread-10,单例模式.service.懒汉式.demo2@7070e0ed
Thread-4,单例模式.service.懒汉式.demo2@1ea98862
Thread-7,单例模式.service.懒汉式.demo2@294eb59e
Thread-3,单例模式.service.懒汉式.demo2@26dc9ee3
Thread-1,单例模式.service.懒汉式.demo2@5e0d2717
Thread-27,单例模式.service.懒汉式.demo2@b20b63
Thread-6,单例模式.service.懒汉式.demo2@4b0d0bfe
Thread-16,单例模式.service.懒汉式.demo2@3e81761
Thread-0,单例模式.service.懒汉式.demo2@7b32301d
Thread-8,单例模式.service.懒汉式.demo2@7b32301d
Thread-9,单例模式.service.懒汉式.demo2@7b32301d
Thread-11,单例模式.service.懒汉式.demo2@21ed9a86
这里只截取了运行结果的一部分,剩余部分因为长度问题,本文没有全部截取,读者可以截取上述代码,自行运行,从结果我们可以看到singleTon对象,在多个线程中实例化了多次,这就违背了单例模式的设计原则.那在多线程情况下如何解决懒汉式存在线程安全的问题呢? 别急,接着往下看。
对于多线程的情况下,我们对于懒汉式问题可以给它加锁,来保证线程同步
public synchronized static demo2 getInstance(){
if(singleTon==null){
try{
Thread.sleep(2000);
}catch(Exception e){
}
singleTon = new demo2();
}
return singleTon;
}
这样加锁可以保证懒汉式在多线程情况下的线程安全问题,但是有一个致命的缺点就是加锁在高并发的情况下,会影响程序的执行效率.那我们如何保证线程安全的情况下,加快程序运行的效率呢 ,这里我们介绍一种双重检验锁。
什么是双重检验锁呢,双重检验锁是为了解决懒汉式 对于读和写都加锁的问题,在单例模式中,我们只有第一次访问该对象的时候需要加锁,对象才需要实例化,之后再次访问我们就没有必要再去加锁,只是读的操作。我们可以在对象创建的时候进行双重判断,来保证线程安全,并且提高运行效率.
public class demo3{
private static demo3 singleTon;
private demo3() throws Exception {
if(singleTon!=null){
throw new Exception("对象已经初始化");
}
System.out.println("初始化双重检验锁");
}
/*
只有在需要的时候才会加载 但是在多线程的情况下 会出现线程安全的问题
实例对象可能被初始化多次
*/
public synchronized static demo3 getInstance() throws Exception {
if(singleTon==null){
synchronized(demo3.class){
if(singleTon == null){
//当前线程已经获得锁对象 判断当前对象是否初始过
singleTon = new demo3();
}
}
}
return singleTon;
}
}
public class SingleTon{
private SingleTon(){}
private static class SingleTonHoler{
private static SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance(){
return SingleTonHoler.INSTANCE;
}
}
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。个人认为是懒汉式和饿汉式的升级版 ,这里推荐大家一篇博客,博主对于静态内部类的实现,讲的比较详细: 深入理解单例模式:静态内部类单例原理
public enum demo4 {
//枚举能够先天性破解序列化和反射
Instance;
public void add(){
System.out.println("add方法...");
}
}
测试类
public class Test {
public static void main(String[] args) {
demo4 instance = demo4.Instance;
instance.add();
demo4.Instance.add();
demo4 instance2 = demo4.Instance;
System.out.println(instance == instance2);
}
}
/*
add方法...
add方法...
true
*/
在枚举方式实现的单例模式中,在最终编译后的class文件上,是将enum转化为一个类,例如上述的例子里,这里我们使用jclasslib工具来查看demo4的字节码文件
这里我们可以看到在字节码文件中,demo4这个枚举类,最终是以class类的形式存在,继承了Enum类,其中的Instance最终是以实例的形式存在,这就是为什么可以使用
demo4.Instance.add();