2023-06-15  阅读(2)
原文作者: 求和的小熊猫 原文地址:https://blog.csdn.net/qq_38219153/article/details/110894623

单例设计模式

单例模式的特点

  • 某个类只能有一个实例
  • 它必须自行创建这个类
  • 它不许向整个系统提供这个类的实例

饿汉模式/立即加载

饿汉式的特点: 在使用类时就已经将对象加载完毕

饿汉式的缺点:

  • 由于在使用前就加载完毕,会造成内存资源的浪费
  • 在获取实例时,若没有同步方法,容易产生非线程安全问题。

饿汉式的创建方式:

  • 静态常量饿汉式
  • 静态代码块饿汉式
  • 枚举类
静态常量饿汉式
    // 静态常量的饿汉式
    public class Singleton1 {
    	private static Singleton1 instance = new Singleton1();
    
    	private Singleton1() {
    	}
    
    	public static Singleton1 getInstance() {
    		return instance;
    	}
    }
静态代码块饿汉式
    // 静态代码块的饿汉式
    public class Singleton2 {
    	private static Singleton2 instance;
    	
    	private Singleton2() {
    	}
    	
    	static {
    		instance = new Singleton2();
    	}
    	
    	public static Singleton2 getInstance() {
    		return instance;
    	}
    }
饿汉式的非线程安全问题

如果在获取实力式没有同步,则会产生非线程安全问题
【示例】

使用静态常量饿汉式,创建一个 Article 方法, 包含标题内容两个属性,返回一个实例对象并重写 toString 方法

    public class Article {
    	private static Article object = new Article();
    	private static String title;
    	private static String content;
    	
    	private Article() {	
    	}
    	
    	public static Article getInstance() {
    		title=Thread.currentThread().getName();
    		Date a = new Date();
    		content = "The Article was Written at " + a ;
    		System.out.println(title + content);
    		try {
    			TimeUnit.SECONDS.sleep(8);
    		}catch(InterruptedException e) {
    			e.printStackTrace();
    		}
    		return object;
    	}
    
    	@Override
    	public String toString() {
    		return title + " : " + content;
    	}
    
    }

创建一个线程任务,并在线程中获取示例,并打印结果

    public class MyThread extends Thread {
    	
    	@Override
    	public void run() {	
    		Article instance = Article.getInstance();
    		System.out.println(instance.hashCode() + " : " + instance);
    	}
    
    }

Main 方法

    public class Main {
    
    	public static void main(String[] args) {
    		MyThread threads[] = new MyThread[3];
    		for(int i =0 ; i< 3; i++) {
    			threads[i] = new MyThread();
    			threads[i].setName("Thread-"+(i+1));
    			threads[i].start();
    			try {
    				TimeUnit.SECONDS.sleep(1);
    			}catch(InterruptedException e) {
    				e.printStackTrace();
    			}
    		}		
    	}
    
    }

运行结果

    Thread-1The Article was Written at Tue Dec 08 10:09:56 CST 2020
    Thread-2The Article was Written at Tue Dec 08 10:09:57 CST 2020
    Thread-3The Article was Written at Tue Dec 08 10:09:58 CST 2020
    953607256 : Thread-3 : The Article was Written at Tue Dec 08 10:09:58 CST 2020
    953607256 : Thread-3 : The Article was Written at Tue Dec 08 10:09:58 CST 2020
    953607256 : Thread-3 : The Article was Written at Tue Dec 08 10:09:58 CST 2020

由上述运行结果可知,最终变量被修改成相同的。但是三个线程都是获取的同一个实例。

懒汉模式/延时加载

懒汉式的创建方式

  • 线程不安全的懒汉式
  • 线程安全的懒汉式
  • 同步代码块懒汉式
  • DCL
线程不安全懒汉式
    public class Singleton1 {
    	private static Singleton1 instance;
    	
    	private Singleton1() {
    	}
    	
    	public static Singleton1 getInstance() {
    		if(instance == null) {
    			instance = new Singleton1();
    		}
    		return instance;
    	}
    
    }

那么为何说这个是线程不安全的创建方式呢
【示例】

一个单例类

    public class Instance {
    	private static Instance instance;
    	
    	private Instance() {
    		System.out.println("Create a Instance");
    	}
    	
    	public static Instance getInstance() {
    		if(instance == null) {
    			instance = new Instance();
    		}
    		return instance;
    	}
    
    }

一个任务类,用于获取单例实例

    public class Task implements Runnable {
    
    	@Override
    	public void run() {
    		System.out.println(Thread.currentThread().getName() + "Start");
    		Instance instance = Instance.getInstance();
    		System.out.println(Thread.currentThread().getName() + " : " + instance.hashCode());
    		try {
    			TimeUnit.SECONDS.sleep(3);
    		}catch(InterruptedException e) {
    			e.printStackTrace();
    		}
    		System.out.println(Thread.currentThread().getName() + "Finished 
    with : " + Instance.getInstance().hashCode());
    	}
    
    }

Main 方法

    public class Main {
    
    	public static void main(String[] args) {
    		Task task = new Task();
    		Thread thread1 = new Thread(task,"Thread-1");
    		Thread thread2 = new Thread(task,"Thread-2");
    		Thread thread3 = new Thread(task,"Thread-3");
    		thread1.start();
    		thread2.start();
    		thread3.start();
    	}
    
    }

运行结果

    Thread-2Start
    Thread-3Start
    Thread-1Start
    Create a Instance
    Create a Instance
    Create a Instance
    Thread-2 : 469684341
    Thread-3 : 55699698
    Thread-1 : 643939211
    Thread-1Finished with : 643939211
    Thread-2Finished with : 643939211
    Thread-3Finished with : 643939211

由运行结果可知,在此过程中创建了3个实例,这便是产生了线程安全问题,三个线程同时访问指针获取空值,从而创建了三个变量。

线程安全的懒汉式

为了保证线程安全,我们可以有两种方法,一种是为方法加锁,另一种方法便是使用同步代码块

假设为获取实例的方法声明为同步方法,那么方法里若有许多与创建实例无关的代码必然会影响代码运行的效率。

为了保证运行效率,我们可以将创建的关键步骤放入同步代码块中

    public class Singleton2 {
    	private static Singleton2 instance;
    	
    	private Singleton2() {
    	}
    	
    	public static Singleton2 getInstance() {
    		// 其他步骤
    		try {			
    			synchronized(Singleton2.class) {
    				if(instance == null) {
    					// 创建前的步骤
    					instance = new Singleton2();								
    				}
    			}			
    		}catch(Exception e) {
    			e.printStackTrace();
    		}
    		
    		return instance;
    	}
    }

当然,到这一步,那么以上代码还能否进一步提升运行效率呢

DCL 双检查机制
    public class Singleton3 {
    	private volatile static Singleton3 instance;
    	
    	private Singleton3() {
    	}
    	
    	public static Singleton3 getInstance() {
    		// 其他代码
    		try {
    			if(instance == null) {
    				// 创建前的准备代码
    				synchronized(Singleton3.class) {
    					if(instance==null) {
    						instance = new Singleton3();
    					}
    				}
    			}
    		}catch(Exception e) {
    			e.printStackTrace();
    		}
    		
    		return instance;
    	}
    
    }

双检查机制,将创建前的代码从创建代码分离出去,减少了锁占有的时间,减少了等待时间,既保证了效率,也避免了多次创建的线程安全问题。

PS: 对于需要返回的instance 实例需要加上 volatile 关键字,以保证其内存可见性。避免由于指令重排导致的错误。

内部静态类

    public class Singleton1 {
    	private static class SingletonHandler{
    		
    		private SingletonHandler() {
    			System.out.println("Create SingletonHandler");
    		}
    		
    		private static Singleton1 instance =  new Singleton1();
    	}
    	
    	private Singleton1() {
    		System.out.println("Create Singleton1");
    	}
    	
    	public static Singleton1 getInstance() {
    		return SingletonHandler.instance;
    	}
    	
    }

枚举类

枚举类与静态代码块的特征比较类似

    public enum Singleton1 {
    	instance("instance"),
    	check("check");
    	
    	private String instanceString;
    	
    	Singleton1(String str){
    		instanceString = str;
    		System.out.println("Create " + instanceString);
    	}
    	
    	public String getInstance() {
    		return instanceString;
    	}
    
    }

获取枚举类对象的调用方法

    public class Main {
    	public static void main(String[] args) {
    		System.out.println(Singleton1.instance.getInstance());
    	}
    }

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文