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这种抽象队列同步器。