面试~设计模式---单例模式


1、说说常见的设计模式有哪些?

单例模式、工厂模式、代理模式、观察者模式、装饰器模式、适配器模式等


2、什么是单例模式

简单来说,单列模式是为了保证某个对象在程序的生命周期内在内存中只存在一个实例。即一个类只有一个对象。

它提供了全局访问的方法。


3、为什么要用单例模式?

① 节省内存资源

② 节省时间(分配对象的时间)


4、单例模式的实现方式

饿汉、懒汉、双重检验锁、静态内部类、枚举


5、手写一个单例模式(结合你的面试时间,写双重检验锁可以延伸出更多问题的提问)

对并发熟悉,可以 写双重检验锁这样就有更多的面试提问点了

也可以写饿汉式

要求懒加载,也写静态内部类

/* 双重检验锁  */
public class Singleton{
    private Singleton(){}//构造器私有化,防止new,导致多个实例
    private static volatile Singleton singleton;
    public static Singleton getInstance(){//向外暴露一个静态的公共方法  getInstance
        //第一层检查
        if(singleton == null){
            //同步代码块
            synchronized (Singleton.class){
                 //第二层检查
                if(singleton == null) {
                    singleton = new Singleton();
                }
            }

        }
        return singleton;
    }
}


--------------------------------------------------------------------------------------------
/**
* 饿汉式
*/
public class Singleton{
    private static final Singleton singleton  = new Singleton();//饿汉式,初始化时就创建好了实例[使用了final常量-->饿汉式(静态常量)]
    
    private Singleton(){}//构造器私有化,防止new,导致多个实例
    
    public static Singleton getInstance(){//向外暴露一个静态的公共方法  getInstance
        return singleton;
    }
}
--------------------------------------------------------------------------------------------

/**
* 饿汉式
*/
public class Singleton{
    private Singleton(){}//构造器私有化,防止new,导致多个实例
    /**
    * 静态内部类,在其内部以静态常量的方式实例化对象
    */
    private static class SingletonInstance{
        private static final Singleton singleton = new Singleton();//常量静态属性,实例化对象[初始化]
    }
    public static Singleton getInstance(){//向外暴露一个静态的公共方法  getInstance
        return SingletonInstance.singleton;
    }
}

双重检验锁

☆ volatile 关键字,可深入到Java VM内存相关

☆ synchronized 关键字,可深入到Java锁机制,高并发相关

☆ new 关键字,可深入到Java VM类加载机制相关


★ 你写的这个程序是怎么保证线程安全的?

首先构造器私有化向外暴露一个静态的公共方法 getSingleton,来向系统提供对象实例

getSingleton 方法 首先先判断实例是否存在,存在直接返回;不存在加 synchronized 锁,为了进一步判断实例是否已经存在,需要再次进行非空判断,判断为null,不存在实例,再创建实例。



★ synchronized起到了什么作用?

锁定对象,限制当前对象只能被一个线程访问


★ synchronized里你传Singleton.class这个参数,起到什么作用,换成别的行不行?

对当前类加锁,使得这个代码块一次只能被一个线程访问

可以换成别的,这里Singleton.class可以换成一个常量字符串或者自己定义一个内部静态Object


★ 那传Singleton.class,常量字符串,自己定义一个内部静态Object有区别吗?

没啥区别,因为这是一个静态方法,相当于是类锁。类锁(全局锁)对应的就是可以锁在该类的class可以锁在classloader对象上。

类锁是所有线程共享的锁,所以同一时刻,只能有一个线程使用加了锁的方法或方法体,不管是不是同一个实例。



★ volatile 起到了什么作用?

加volatile,是为了 禁止指令重排

在执行new 创建单例对象[ singleton = new Singleton();],

指令1分配对象内存空间;指令2初始化对象;指令3设置singleton 引用指向分配好的对象内存空间;

由于指令重排的优化, 可能初始化的指令还没有执行,就先执行了设置引用指向分配好对象的内存空间的指令了。


  • 在单线程环境下是没有问题的,但在多线程环境下会出现问题:另外一个线程会看到一个还没有被初始化的对象



★ 为什么要进行两次非空检查?

保证线程安全,假设有两个线程同时到达 synchronized 语句块,那么实例化代码只会由其中先抢到锁的线程执行一次

而后抢到锁的线程会在第二个 if 判断中发现 singleton 不为 null,所以跳过创建实例的语句

再后面的其他线程再来调用 getInstance 方法时,只需判断第一次的 if (singleton == null) ,然后会跳过整个if块,直接return实例化后的对象。


双重检查:

  • 实现:线程安全,延迟加载、效率也更高



★ 那你知道synchronized关键字实现同步的原理吗?

synchronized在Java虚拟机中使用监视器锁来实现。每个对象都有一个监视器锁,当监视器锁被占用时就会处于锁定状态。

  线程执行一条叫monitorenter的指令来获取监视器锁的所有权。如果此监视器锁的进入数为0,则线程进入并将进入数设置为1,成为线程所有者。如果线程已经拥有该锁,因为是可重入锁,可以重新进入,则进入数加1.如果线程的监视器锁被其他线程占用,则阻塞直到此监视器锁的进入数为0时才能进入该锁。

  线程执行一条叫monitorexit的指令来释放所有权。执行monitorexit的必须是线程的所有者。每次执行此指令,线程进入数减1,直到进入数为0。监视器锁被释放。


★ 你刚才提到的可重入锁是什么概念,有不可重入锁吗?

我说的可重入锁是广义的可重入锁,当然jdk1.5引入了concurrent包,里面有Lock接口,它有一个实现叫ReentrantLock。广义的可重入锁也叫递归锁,是指同一线程外层函数获得锁之后,内层还可以再次获得此锁。可重入锁的设计是为了避免死锁。sun的corba里的mutex互斥锁是一种不可重入锁的实现。自旋锁也是一种不可重入锁,本质上是一种忙等锁,CPU一直循环执行"测试并设置"直到可用并取得该锁,在递归的调用该锁时必然会引起死锁。另外,如果锁占用时间较长,自旋锁会过多的占用CPU资源,这时使用基于睡眠原理来实现的锁更加合适。


★ 你刚才提到了concurrent包,它里面有哪些锁的实现?

常用的有ReentrantLock,它是一种独占锁。ReadWriteLock接口也是一个锁接口,和Lock接口是一种关联关系,它返回一个只读的Lock和只写的Lock。读写分离,在没有写锁的情况下,读锁是无阻塞的,提高了执行效率,它是一种共享锁。ReadWriteLock的实现类为ReentrantReadWriteLock。ReentrantLock和ReentrantReadWriteLock实现都依赖于AbstractQueuedSynchronizer这种抽象队列同步器。

posted @ 2022-08-23 15:18  一乐乐  阅读(55)  评论(0编辑  收藏  举报