目录
1.单例设计模式
1.1 什么是单例设计模式
面试官: 介绍一下单例设计模式
- 什么是单例设计模式:
单例就是这个类只有一个实例对象. - 单例设计模式解决什么问题:
一个全局使用的类频繁地创建与销毁. 并且节省内存. - 什么时候使用单例设计模式:
当你想控制类的实例的数目, 节省资源的时候. - 单例设计模式的代码特点:
- 私有的构造方法;
- 私有的静态的成员变量;
- 共有的获取实例的静态方法;
1.2 代码实现&各种单例模式的优劣
面试官: 你日常开发中是怎么实现单例的? (面试官可能让你手写一个线程安全的单例)
我一般使用双重判定锁的方式来实现单例.
单例的实现方式有很多种, 比如 饿汉式
,懒汉式
,双重判定锁
等等方式实现的单例.
-
饿汉式单例介绍:
所谓"饿汉", 是指等不及要赶紧创建单例对象. 即在类加载的过程中就进行单例对象的创建. 饿汉就是在类初始化的时候创建单例对象. 具体实现方式有多种. -
饿汉式单例的实现代码:
class Singleton1{ private Singleton1(){} private final static Singleton1 SINGLE = new Singleton1(); public static Singleton1 getInstance(){ return SINGLE; } }
-
饿汉式变种:
class Singleton { private static Singleton instance = null; static { instance = new Singleton(); } private Singleton() { } public static Singleton getInstance() { return instance; } }
-
饿汉式单例优点:
代码简单; 使用时没有延迟, 在类装载时就完成实例化; 线程安全; -
饿汉式单例缺点:
没有懒加载, 启动比较慢; 无论是否使用这个实例, 这个实例都会创建, 浪费内存; -
懒汉式单例介绍:
所谓"懒汉", 指的是并不会事先初始化出单例对象, 而是在第一次使用的时候再进行初始化.
懒汉模式有两种写法, 分别是线程安全的和非线程安全的. -
懒汉式单例实现代码 – 非线程安全
class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
-
懒汉式单例实现代码 – 线程安全
// 线程安全的懒汉式只不过比线程不安全的懒汉式多了一个synchronized关键字 class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
-
懒汉式单例优点:
懒加载, 启动速度快; 只有在使用该类实例的时候才会初始化这个类的实例, 节约资源(内存); -
懒汉式单例缺点:
- 线程不安全的懒汉式缺点:
- 多线程环境下线程不安全. if (singleton == null) {…} 存在竞态条件, 可能会有多个线程同时进入 if 语句, 导致产生多个实例;
- 线程安全的懒汉式缺点:
- 第一次使用的时候需要
耗费时间进行对象的初始化
. - 假如有100个线程同时执行, 那么每次去执行getInstance方法时都要先获得锁再去执行方法体, 如果没有获取锁就要等待, 耗时长, 锁的粒度太大. (效率问题)
- 第一次使用的时候需要
- 线程不安全的懒汉式缺点:
-
双重判定锁实现单例介绍:
- 基于懒汉式. 使用同步代码块代替线程安全的懒汉式中的同步方法, 作用是减小锁的粒度, 减少阻塞.
但是为了保证线程安全需要进行两次非空判断, 所以叫双重判定锁. - 双重if判断: 为了避免了在首层判断就加上Synchorzied同步锁, 导致锁的粒度过大, 引起效率的低下 , 所以采用双重if判断, 在第二层判断才引入对性能开销较大的synchorzied锁, 双重if判断本质上是对效率的优化**(类似读写分离, 读不需要加锁, 写需要加锁)**
- volatile: 为了避免JVM的指令重排优化, 为了避免在new对象时出现指令重排序现象, 需要对引用对象用volatile修饰.
因为初始化对象分为三步:
①申请一块内存;
②在这块内存里实例化对象;
③instance的引用指向这块内存的地址;
假如先执行1申请一块内存, 再执行3将引用变量指向刚申请的内存, 那么当再执行2的时候, 判断instance是否为空(引用变量是否指向空)的时候就为false, 就不是null, 因此也就不会实例化对象了. 这就是所谓的指令重排序安全问题.
或者说用下面的说法:
因为编译器有可能进行指令重排优化, 使得singleton对象再未完成初始化之前就对其进行了赋值, 这样其他人拿到的对象就可能是个残缺的对象了. 使用volatile的目的是避免指令重排. 保证先进行初始化, 然后进行赋值.
- 基于懒汉式. 使用同步代码块代替线程安全的懒汉式中的同步方法, 作用是减小锁的粒度, 减少阻塞.
-
双重判定锁实现单例代码:
class Singleton { private Singleton() { } private static volatile Singleton instance = null; public static Singleton getInstance() { if (null == instance) { // 同步代码块内还有个if,是为了防止 同时有两个线程到达了这里的情况 synchronized (Singleton.class) { if (null == instance) { instance = new Singleton(); } } } return instance; } @Test // 测试单例设计模式. 创建50个线程并发调用getInstance()方法并打印返回值. public void test01() { ExecutorService threadPool = Executors.newFixedThreadPool(50); for (int i = 0; i < 50; i++) { threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + "\t" + Singleton.getInstance())); } threadPool.shutdown(); } }
-
其他实现单例的方式 (下面的使用枚举实现单例当做扩展, 面试加分)
-
使用枚举实现单例的代码:
public enum Singleton { INSTANCE; public void whateverMethod() { } }
-
使用枚举实现单例好处:
-
写法简单.
-
枚举其实底层是依赖Enum类实现的, 这个类的成员变量都是static类型的, 并且在静态代码块中实例化的, 和饿汉有点像, 所以他
天然是线程安全的
. -
枚举可以解决反序列化会破坏单例的问题
在枚举序列化的时候, Java仅仅是将枚举对象的name属性输出到结果中, 反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象. 同时, 编译器是不允许任何对这种序列化机制的定制的, 因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法.
普通的Java类的反序列化过程中, 会通过反射调用类的默认构造函数来初始化对象. 所以, 即使单例中构造函数是私有的, 也会被反射给破坏掉. 由于反序列化后的对象是重新new出来的, 所以这就破坏了单例.
但是, 枚举的反序列化并不是通过反射实现的. 所以, 也就不会发生由于反序列化导致的单例破坏问题.
-
参考自公众号: Hollis
1.3 实际应用
- Spring框架中单例的应用:
Spring中的Bean默认是单例的, Spring创建单例Bean使用的是getBean(), 这个方法底层调用的是getSingleton()方法.
https://www.cnblogs.com/chengxuyuanzhilu/p/6404991.html
https://blog.csdn.net/hbtj_1216/article/details/74896381 - 我们自己项目中单例的应用:
比如我们常见的工具类,连接池类等等就可以定义为单例类.
https://blog.csdn.net/NathanniuBee/article/details/91872152
1.4 面试思路
如果面试官问
什么是单例设计模式? 你平常怎么使用单例设计模式? 你了解单例设计模式吗…
就按照下面的思路说给他听.
- 什么是单例设计模式
- 单例设计模式解决的问题是什么
- 什么时候使用单例设计模式
- 常用的框架中哪里使用到了单例设计模式
- 平常的工作中哪里使用到了单例设计模式
- 单例设计模式的代码特点是什么
- 单例设计模式的实现方式有很多种, 比如饿汉式 懒汉式 双重判定锁实现单例等等.
然后介绍上面三种实现单例的方式的优缺点, 指明自己平常使用的是双重判定锁的单例设计模式,
面试官有可能会让你写一个线程安全的单例设计模式, 写双重判定锁的即可, 要求该博客中出现的单例设计模式的代码都要会用笔写.
强调使用双重判定锁的单例的时候成员变量需要加volatile修饰, 说明原因是JVM的指令重排.
扩展说一下使用枚举来实现单例设计模式的优点.
2. 常见算法(面试默写版)
2.1 冒泡排序
// 冒泡排序。从小到大排序
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
// 从大到小排序只需要将下面if中的>改为<即可.
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
// 测试冒泡排序
@Test
public void test01() throws Exception {
int arr[] = {3, 4, 2, 1, 5};
// [3, 4, 2, 1, 5]
System.out.println(Arrays.toString(arr));
bubbleSort(arr);
// [1, 2, 3, 4, 5]
System.out.println(Arrays.toString(arr));
}
2.2 二分查找
// 二分查找
public static int binarySearch(int[] arr, int value) {
int low = 0;
int high = arr.length - 1;
while (low < high) {
int mid = (low + high) / 2;
if (arr[mid] > value) {
high = mid - 1;
} else if (arr[mid] < value) {
low = mid + 1;
} else {
return mid;
}
}
return -1;
}
// 测试二分查找
@Test
public void test01() throws Exception {
int arr[] = {3, 4, 5, 2, 1, 5};
// 2
System.out.println(binarySearch(arr, 5));
// -1
System.out.println(binarySearch(arr, 10));
}
2.3 快速排序
// 快速排序
public static void quickSort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int key = arr[left];
int i = left;
int j = right;
while (i < j) {
while (arr[j] >= key && i < j) {
j--;
}
arr[i] = arr[j];
while (arr[i] <= key && i < j) {
i++;
}
arr[j] = arr[i];
}
arr[j] = key;
quickSort(arr, left, i - 1);
quickSort(arr, i + 1, right);
}
// 测试快速排序
@Test
public void test01() throws Exception {
int[] arr = {3, 1, 2, 5, 4};
quickSort(arr, 0, arr.length - 1);
// [1, 2, 3, 4, 5]
System.out.println(Arrays.toString(arr));
}
2.4 递归求阶乘
// 递归求阶乘
public static int multiply(int num) {
if (num < 0) {
System.out.println("请输入大于0的整数");
return -1;
} else if (num == 0 || num == 1) {
return 1;
} else {
return multiply(num - 1) * num;
}
}
// 测试递归求阶乘
@Test
public void test01() throws Exception{
// 6
System.out.println(multiply(3));
}
2.5 线程安全的单例模式
// 线程安全的单利设计模式。 双重判定锁
class Singleton {
private Singleton() {
}
private static volatile Singleton instance = null;
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
// 测试单例设计模式. 创建50个线程并发调用getInstance()方法并打印返回值.
@Test
public void test01() throws Exception {
final ExecutorService threadPool = Executors.newFixedThreadPool(50);
for (int i = 0; i < 50; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + Singleton.getInstance());
});
}
threadPool.shutdown();
}