单例模式
We all want to as a 有追求的程序猿,这不将早已尘封的《剑指Offer》給拿出来重新拜读一下。该书中面试题2就是单例模式,可见其重要性(Maybe just for Interview).同时也为了这次更加系统地阅读&总结。
PS :之前也是随便翻了几下就束之高阁啦。
前言
我们在面试中经常遇到单例模式(However you as 面试者or面试官),关于单例模式的优秀文章,网上也是俯首皆是。本文Just for me to 心得体会or笔记。如果有幸能帮助到其他人,那我将会更加高兴...
1-饿汉式单例
这个写法就类似于解决了单例模式中的“温饱问题”
public class Singleton {
// JVM加载该类时,单例对象就会自动创建
private static Singleton instance = new Singleton();
private Singleton() {
System.out.println("构造函数Running....");
}
public static Singleton getInstance(){
return instance;
}
/**
* 证明了没有对instance做延时加载...
*/
public static void doSomething(){
System.out.println("Just for fun...");
}
public static void main(String[] args) {
/*
* 这里没有用到该实例,But 照样给我创建了其实例
*/
Singleton.doSomething();
}
}
"Talk is cheap,show me the code"
JVM类的加载原理
- JVM在执行类的初始化期间,JVM会获得一把锁,该锁可以同步多个线程对同一个类的初始化
There is no doubt that 该种方案实现简单,且线程安全。但是其没有对instance做相应的延时加载,只要初始化该类就创建其实例,这样就造成了资源浪费。
2-懒汉式单例
“懒汉式”---顾名思义,就是你要我才給,按需分配
/**
* “懒汉式”,用到实例才加载,否则不加载
*
* 缺点:线程不安全...
*/
public class Singleton {
private static Singleton instance = null;
private Singleton() {
System.out.println("构造函数Running...");
}
public static Singleton getInstance(){
/**
* 避免重复创建...
*/
if (instance == null) {
instance = new Singleton();
}
return instance;
}
/**
* HashCode相等说明是同一个实例
* @return
*/
@Override
public int hashCode() {
return super.hashCode();
}
public static void main(String[] args) {
/**
* 模拟多线程环境,会发现不是同一个实例...
*/
for (int i = 0; i < 5; i++) {
new Thread(() -> System.out.println(getInstance().hashCode())).start();
}
}
}
Code 地址:该编辑器在多线程情况下测试单例不好使,有兴趣可以复制到本地去运行测试
(PS:原图片链接)
线程不安全,那我们只能去使用同步机制来保证线程安全
3-同步锁的懒汉式
/**
*
* 保证线程安全的“饿汉式”单例
*
* 即:加入synchronized 同步关键字...
*
* 下面的格式将会造成:每次来调用getInstance()都要进行线程同步(即调用synchronized锁)
*
* 而实际上, 只需要在第一次调用的时候才需要进行同步,只要单例存在,就没必要进行同步啦...
*
*/
public class Singleton {
private static Singleton instance = null;
private Singleton() {
System.out.println("构造函数running...");
}
public static synchronized Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
/**
* 也可以写成这种格式
* */
// public static Singleton getInstance(){
// synchronized (Singleton.class){
// if (instance != null) {
// instance = new Singleton();
// }
// }
// return instance;
// }
@Override
public int hashCode() {
return super.hashCode();
}
public static void main(String[] args) {
for (int i = 0; i <5 ; i++) {
new Thread(() -> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}
代码中的注释已经很清楚了,这里就闲话不扯...
4-双重校验锁(Double-Check)单例
这个是重点...
/**
* "Double-Check"
* "双重校验锁"的单例...
*
*
* 其实就是在"同步锁"的基础上,外层 + if 判断...
*
* 作用:若单例存在,就不需要进行同步加锁操作synchronized。直接返回实例。从而提高程序性能...
*
*
* PS: 到这里还没有完工,主要原因在于 instance = new Singleton2();
* 这并非是个原子操作,该句事实上在JVM中大概有三个过程:
* 1. 給instance 分配内存;
* 2. 调用Singleton2的构造函数来初始化成员变量,生成实例;
* 3. 将singleton对象指向分配的内存空间(此时,instance 才是非null的)
*
*
* 但是在JVM的即时编译器中存在指令重排的优化,so 上述的2,3顺序不能保证。
* 假如执行序列为1-3-2.,当3执行完毕,而2未执行之前,被其他线程抢占了,此时instance已经是非null(但是没有初始化)
* 线程直接返回了instance,然后使用就报错...
*
* 所以需要在instance声明为volatile 就可以啦...
*
*
* volatile关键字的两个功能:
* 1. 这个变量不会在多个线程中存在副本,直接从内存中读取...
* 2. 禁止指令重排序优化。
*
* 但是这个只在Java 1.5之后有效,因为之前的Java内存模型有缺陷...
*
* 总结:
* 该单例版本有点复杂...
*
*/
public class Singleton {
/**
* 注意这里...volatile关键字
*/
private volatile static Singleton instance = null;
private Singleton() {
System.out.println("running...");
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
@Override
public int hashCode() {
return super.hashCode();
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> System.out.println(getInstance().hashCode())).start();
}
}
}
双重校验锁是满足要求,But 有局限性... 别着急,还有更好的
5- 静态内部类实现单例
package offer;
/**
* @author king
* @date 2018/5/6
* <p>
* 静态内部类实现单例
*/
public class Singleton {
/**
* 创建静态内部类
*/
private static class InnerSingleton {
/**
* 在静态内部类里创建单例
*/
private static Singleton instance = new Singleton();
}
/**
* 私有化构造函数
*/
private Singleton() {
System.out.println("构造函数Running...");
}
public static Singleton getInstance() {
return InnerSingleton.instance;
}
@Override
public int hashCode() {
return super.hashCode();
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> System.out.println(getInstance().hashCode())).start();
}
}
}
关于单例的小插曲
曾经在面试过程中,问到面试者饿汉式&懒汉式的区别:
曾有面试者告诉我,饿汉式每次加载类都会 new 一次对象,将造成资源浪费。我当时没反应过来,只注意到它回答的“资源浪费”,后来我才明白过来,instance是static的,只会初始化一次,何来多次new 之谈???
- 有一次让面试者写个懒汉式单例(先不考虑多线程情况),代码大意如下:
public class Singleton {
public static Singleton instance = null;
private Singleton(){
}
// 注意...
public static Singleton getInstance(){
instance = new Singleton();
return instance;
}
}
少个判空,已非“单例”啊...
总结
无论是剑指offer这本书,还是我们面试中高级岗位时,考察点基本都会设在双重校验锁上,毕竟面试造核弹...
网友评论