目录
4、双重检查+volatile 加synchronized同步锁的懒汉式效率优化版
一、概述及可用方案
单例模式的意思是一个类只能产生一个实例,并提供全局访问。
使用场景一般有数据库连接池、线程池、配置项管理类等。
1、优点、缺点及重点
优点:
(1)在内存中只有一个实例,减少了内存开销。
(2)可以避免对资源的多重占用,比如写同一个日志文件。
缺点:
没有上层接口,扩展困难,只能直接修改该类的代码。比如Map m=new HashMap();这种语句单例模式中是不支持的,因为构造方法都是私有的或没有构造方法。
重点:
(1)私有构造器;
(2)线程安全;
(3)延迟加载(懒加载);
(4)序列化与反序列化安全;
(5)反射攻击。
前三个本篇都涉及到了,序列化和反射再单独开一篇文章学习。
2、分类
饿汉式、懒汉式、枚举
3、可用方案
优先推荐枚举单例模式,其次饿汉式,最后懒汉式。
(1)饿汉式
饿汉式缺点是程序部署加载的时候初始化会拉长程序加载时间、另外部分单例类实例化后可能用不到,导致资源浪费。
优点是提前加载好了,用到时访问不用再进行实例化。
构造方法设为private,防止外部访问。但是可以通过反射方式实现修改构造方法的访问权限。
/**
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用!
* 唯一缺点:不管用到与否,类装载时就完成实例化
* Class.forName("")
*/
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {};
public static Mgr01 getInstance() {
return INSTANCE;
}
}
(2)懒汉式
懒汉式核心是要实现懒加载,什么时候用什么时候实例化。
为了保证线程安全,懒汉式包含静态内部类的实现方式和双重检查的实现方式。
静态内部类的实现方式:加载外部类时不会加载内部类,只有访问调用的时候才会加载,于是实现了懒加载。另外JVM保证了执行加载同一个内部类只加载一次,于是就保证了线程安全性,不会出现多个实例的情况。
/**
* 静态内部类方式
* JVM保证单例
* 加载外部类时不会加载内部类,这样可以实现懒加载
*/
public class Mgr07 {
private Mgr07() {
}
private static class Mgr07Holder {
private final static Mgr07 INSTANCE = new Mgr07();
}
public static Mgr07 getInstance() {
return Mgr07Holder.INSTANCE;
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr07.getInstance().hashCode());
}).start();
}
}
}
双重检查:volatile这里是必须要加的,防止语句重排导致实例在没有初始化时就被返回。
/**
* 双重检查
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Mgr06 {
private static volatile Mgr06 INSTANCE; //JIT
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) {
//双重检查
synchronized (Mgr06.class) {
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}
(3)枚举
/**
* 不仅可以解决线程同步,还可以防止反序列化。
*/
public enum Mgr08 {
INSTANCE;
public void m() {}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr08.INSTANCE.hashCode());
}).start();
}
}
}
二、演进迭代与原理
1、基础的饿汉式
基础的饿汉式可以满足大部分需求,它在程序初始化的时候产生实例。优缺点明显:
缺点:唯一不足在于提前产生实例,不管后面是否需要,这样做的类多了容易造成资源占用浪费。
优点:简单易实现,没有线程安全问题。提前加载,后期用到的时候无需重新产生实例的时间,提前准备,效率较高。
/**
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用!
* 唯一缺点:不管用到与否,类装载时就完成实例化
* Class.forName("")
*/
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {};
public static Mgr01 getInstance() {
return INSTANCE;
}
}
2、有线程安全问题的第一版懒汉式
基于饿汉式的缺点,产生了懒加载的懒汉式。但第一版本的懒汉式单例模式存在线程安全问题。由于CPU分配时间的原因,可能导致一个线程执行到判断INSTANCE==null之后就被 挂起 了。下一个拿到时间片的线程又走了一次判断INSTANCE==null,也进入了if判断内部,由此new操作会重复执行。
public class Mgr03 {
private static Mgr03 INSTANCE;
private Mgr03() {
}
public static Mgr03 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03();
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->
System.out.println(Mgr03.getInstance().hashCode())
).start();
}
}
}
由此,需要进一步迭代,解决线程安全问题,搞出线程安全的懒汉式单例模式。
3、加synchronized同步锁的懒汉式效率不高
为了保证线程安全,我们进行了加synchronized同步锁的操作,由此同一时间点,只能有一个线程访问getInstance()方法,保证了线程安全,不会产生不同的实例对象。但这样的话,多线程的应用在这里会形成瓶颈,明明是并排的大马路,走着走着就成独木桥了,效率不高。
/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04() {
}
public static synchronized Mgr04 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr04.getInstance().hashCode());
}).start();
}
}
}
4、双重检查+volatile 加synchronized同步锁的懒汉式效率优化版
之前是不管你实例是否为空,调用getInstance()方法就会加锁。如果调整为实例为空的时候才进行加锁呢?
即调整加锁的位置缩小加锁范围会不会效率高一点呢?
public class Mgr05 {
private static Mgr05 INSTANCE;
private Mgr05() {
}
public static Mgr05 getInstance() {
if (INSTANCE == null) {
//妄图通过减小同步代码块的方式提高效率,然后不可行
synchronized (Mgr05.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr05.getInstance().hashCode());
}).start();
}
}
}
可这样又产生了新的线程安全问题。单重if不能保证线程安全,回到了懒汉加载初始版本的if判断线程安全问题。于是产生了双重检查式的懒汉单例模式,加锁后再判断一次,解决的是两个线程同时进了第一层if判断里面,但其中一个线程已经new操作过了的情况。但涉及到new对象,如果再分析new的详细步骤,则要在使用volatile关键字保证数据一致性,保证了线程安全。volatile这里是必须要加的,防止语句重排导致实例在没有初始化时就被返回。(这里的指令重排指的是JVM为了优化程序执行效率,在new这个操作的时候,做了指令重排。正常的顺序应该是1分配内存给这个对象、2初始化对象、3设置INSTANCE变量指向刚分配的内存地址。但如果2与3指令重排了,单线程没有问题,多线程可能会返回未经初始化的对象,造成程序问题。使用volatile禁止了指令重排)
那能不能拿掉第一层if,只保留下面代码中的第二层if呢?可以,不过这样就等价于上面的Mgr04中对 getInstance静态方法加锁的版本了(对静态方法加锁也是对Mgr.class加的锁,锁的也是当前的class对象)。
由此可以说保证懒加载的同时,进一步又优化了一下性能。不过缺点很明显,忒复杂了。
/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Mgr06 {
private static volatile Mgr06 INSTANCE; //JIT
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) {
//双重检查
synchronized (Mgr06.class) {
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}
5、用更简洁的 静态内部类实现懒汉式
进一步的,产生静态内部类的懒汉单例模式。利用的原理是一个类被加载时,其静态内部类不被加载,只有被访问调用到的时候才会被加载,由此实现懒加载。而如何保证静态内部类也只被加载一次呢?这个是由JVM实现保证的,由此就不会有线程不安全问题导致产生多个实例的问题了。(静态内部类只有被主动访问的时候才会进行初始化)
/**
* 静态内部类方式
* JVM保证单例
* 加载外部类时不会加载内部类,这样可以实现懒加载
*/
public class Mgr07 {
private Mgr07() {
}
private static class Mgr07Holder {
private final static Mgr07 INSTANCE = new Mgr07();
}
public static Mgr07 getInstance() {
return Mgr07Holder.INSTANCE;
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr07.getInstance().hashCode());
}).start();
}
}
}
6、枚举类型的单例模式
《Effective Java》中的建议写法。
不仅可以解决线程安全问题,还能防止反序列化问题。之所以能够防止反序列化,是因为枚举类没有构造方法。而其他实现方法都是一般类,即使构造方法写成private,后面仍然可以通过反射的方式,修改构造方法的访问权限,实现构造出新的实例。
/**
* 不仅可以解决线程同步,还可以防止反序列化。
*/
public enum Mgr08 {
INSTANCE;
public void m() {}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr08.INSTANCE.hashCode());
}).start();
}
}
}