1单例介绍
定义:Ensure a class has only one instance, and provide a global point of access to it.
(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)
概念:一个对象只能创建一个实例
遵循规则:私有化构造器,通过其他方法获取该类的唯一实例
要点:
①一个类只能有一个实例构造器私有化
②必须自行创建这个实例含有一个该类的静态变量来保存这个唯一的实例
③必须自行向整个系统提供这个实例
对外提供获取该实例对象的方式:①直接暴露②用静态变量的get方法获取
单例模式的几种
1饿汉式(静态常量)
2 饿汉式(静态代码块)
3懒汉式(线程不安全)
4懒汉式(线程安全,同步方法,同步代码块,同步锁)
5 双重检查
6 静态内部类
7枚举
2示例代码
2.1饿汉式
2.1.1饿汉式-静态常量
饿汉式:根据jvm虚拟机中:静态修饰的只会加载一次来实现单例效果
class SingletonHungry1 {//饿汉式 静态常量
/*
/优势 简单 避免多线程的同步问题
劣势 没有达到懒加载的效果 内存的浪费
*/
private SingletonHungry1(){
System.out.println("SingletonHungry1静态常量构造方法");
}
private final static SingletonHungry1 INSTANCE = new SingletonHungry1();
public static SingletonHungry1 getInstance(){
return instance;
}
}
//测试是否满足单例
public static void main(String[] args) {
SingletonHungry2 singletonHungry1 = SingletonHungry2.getInstance();
SingletonHungry2 singleton2 = SingletonHungry2.getInstance();
System.out.println(singletonHungry1==singleton2);
}
2.1.2饿汉式-静态代码块
class SingletonHungry2 {//饿汉式 静态代码块
/*
/优势 简单 避免多线程的同步问题
劣势 没有达到懒加载的效果 内存的浪费
*/
static {
instance = new SingletonHungry2();
}
private static SingletonHungry2 instance =null;
private SingletonHungry2(){
System.out.println("SingletonHungry2静态常量构造方法");
}
public static SingletonHungry2 getInstance(){
return instance;
}
}
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到lazy loading的效果
结论:这种单例模式可用,可能造成内存浪费
2.1.3枚举(推荐)饿汉式
public enum EnumSingle {
INSTANCE; //属性
public void sayOK() {
System.out.println("ok~");
}
}
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
EnumSingle enumSingle1 = EnumSingle.INSTANCE;
EnumSingle enumSingle2 = EnumSingle.INSTANCE;
System.out.println(enumSingle1==enumSingle2);
},String.valueOf(i)).start();
}
}
}
这借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
这种方式是Effective Java作者Josh Bloch 提倡的方式
结论:推荐使用
2.2懒汉式
2.2.1懒汉式(线程不安全)
不推荐使用
class SingletonLazy1 {//懒汉式 线程不安全
/*
优势:起到了懒加载的效果 不会造成内存浪费
劣势:线程不安全 不推荐这种方式的
*/
private SingletonLazy1(){
System.out.println("SingletonLazy1懒汉式构造方法");
}
private static SingletonLazy1 instance = null;
public static SingletonLazy1 getInstance(){
if (instance==null){
instance = new SingletonLazy1();
}
return instance;
}
}
public class SingletonLazyTest {//在并发情况下,不满足单例效果
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonLazy1 singletonLazy1 = SingletonLazy1.getInstance();
System.out.println(singletonLazy1);
},String.valueOf(i)).start();
}
}
}
查看运行结果,在并发情况下,会创建多个对象,构造方法会执行多次
起到了Lazy Loading的效果,但是只能在单线程下使用。
如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
结论:在实际开发中,不要使用这种方式.
2.2.2懒汉式(线程安全,同步方法,同步代码块,同步锁)
不推荐使用
class SingletonLazy2 {//懒汉式 线程安全 同步锁 三种方式
/*
解决了线程安全问题,但是效率太低
*/
private static SingletonLazy2 instance = null;
private static ReentrantLock lock = new ReentrantLock();
private SingletonLazy2(){
System.out.println("SingletonLazy2懒汉式构造方法");
}
/* public static synchronized SingletonLazy2 getInstance(){
if (instance==null){
instance = new SingletonLazy2();
}
return instance;
}*/
/* public static SingletonLazy2 getInstance(){
synchronized(SingletonLazy2.class){
if (instance==null){
instance = new SingletonLazy2();
}
return instance;
}
}*/
public static SingletonLazy2 getInstance(){
try{
lock.lock();
if (instance==null){
instance = new SingletonLazy2();
}
return instance;
}finally {
lock.unlock();
}
}
}
解决了线程不安全问题
效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低
结论:在实际开发中,不推荐使用这种方式
2.3 双重检查(推荐)
class SingletonLazy4 {//双重检查DCL
/*
必须加volatile
可能发生指令重排问题 概率很小
*/
// private static SingletonLazy4 instance = null;
private static volatile SingletonLazy4 instance = null;
private SingletonLazy4(){
System.out.println("SingletonLazy3懒汉式构造方法");
}
public static SingletonLazy4 getInstance(){
if (instance==null){
synchronized (SingletonLazy4.class){
if (instance==null){
instance = new SingletonLazy4();
}
}
}
return instance;
}
}
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
new Thread(()->{
DoubleDCLSingle doubleDCLSingle = DoubleDCLSingle.getInstance();
System.out.println(doubleDCLSingle);
},String.valueOf(i)).start();
}
}
}
Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。
这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象,也避免的反复进行方法同步.
线程安全;延迟加载;效率较高
结论:在实际开发中,推荐使用这种单例设计模式
2.2.4 静态内部类(推荐)
package com.design.singleton;
/**
* @author nzy
* @create 2021-12-14 16:15
* 使用静态内部类 推荐使用
* 利用jvm帮助我们保证线程安全性
*/
public class StaticInnerSingle {
//构造器私有化
private StaticInnerSingle() {
System.out.println("StaticInnerSingle构造方法");
}
//写一个静态内部类,该类中有一个静态属性 Singleton
private static class SingletonInstance {
public static final StaticInnerSingle INSTANCE = new StaticInnerSingle();
}
// 提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
public static StaticInnerSingle getInstance() {
return SingletonInstance.INSTANCE;
}
}
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
StaticInnerSingle staticInnerSingle = StaticInnerSingle.getInstance();
System.out.println(staticInnerSingle);
},String.valueOf(i)).start();
}
}
}
这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
结论:推荐使用
3总结
按照实例对象被创建的时机,可以将单例模式分为两类。如果在应用开始时创建单例实例,就称作提前加载单例模式;如果在getInstance方法首次被调用时才调用单例构造器,则称作延迟加载单例模式。
单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new。
单例模式是23个模式中比较简单的模式,应用也非常广泛,
如在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理这些Bean的生命期,决定什么时候创建出来,什么时候销毁,销毁的时候要如何处理,等等。如果采用非单例模式(Prototype类型),则Bean初始化后的管理交由J2EE容器,Spring容器不再跟踪管理Bean的生命周期。
JDK中,java.lang.Runtime 就是经典的饿汉式单例模式