引言:
说到单例模式(Singleton Pattern)相信大家都不陌生,它是Java中最基础的设计模式之一。
这种模式属于创建型模式,提供了一种创建对象的最佳方式。
简单点就是一个类只有一个实例,即不能被new一个出来了嘛,要访问它的时候就调用它给你提供的方法即可,不需要再重新实例化。
所以我们要做的事就很简单:设计一个类,只能有一个实例,并提供一个访问该实例的全局访问方法。
主要解决的问题:一个类被频繁创建和销毁
关键点:构造函数私有
实现:
今天我们按照逻辑来讲讲Java的8种单例模式的写法,每种的优劣都有分析
一:饿汉式
类加载在内存之后,就实例化了一个单例,JVM保证线程安全
这是一种比较简单实用的方法,比较推荐使用
缺点:不管你用不用,类装载的时候都会实例化,(话说回来,你不用的时候装载它干嘛。。。)
public class Mgr01 {
//直接类装载的时候new一个
private static final Mgr01 INSTANCE = new Mgr01();
//这行代码是关键,保证构造方法是私有,别人不能new
private Mgr01(){}
//要就返回给你用
public static Mgr01 getInstance(){return INSTANCE;}
public void m(){
System.out.println("m");
}
//测试可用
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
}
二:饿汉式2
其实这种和第一种是一样的,看一看就好了
public class Mgr02 {
private static final Mgr02 INSTANCE;
static {
INSTANCE = new Mgr02();
}
public static Mgr02 getInstance(){return INSTANCE;}
public void m(){
System.out.println("m");
}
public static void main(String[] args) {
Mgr02 m1 = Mgr02.getInstance();
Mgr02 m2 = Mgr02.getInstance();
System.out.println(m1 == m2);
}
}
三:懒汉式1
既然饿汉式存在不管你用不用都会实例化的问题,那么自然就会有人想到懒汉式,
大概的思路就是我先不实例化,你要用了我再来实例化
存在的问题:不管你用不用都会实例化的小问题是解决了,却带来了更大的问题--线程不安全,具体可参考运行截图
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 void m(){
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mgr03.getInstance().hashCode());
}).start();
}
}
}
运行截图:
四:懒汉式2
既然懒汉式1存在线程安全的问题,那么我们加个锁不就好了?
说的没错,很有道理,说做就做,我们给getInstance()方法加锁
但是同时也带来了新的问题--效率下降了。。
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 void m(){
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mgr04.getInstance().hashCode());
}).start();
}
}
}
五:懒汉式3
既然给方法加锁效率低了,那么我们就只给实例化这个动作加锁呗,
想的没错,我们说做就做
事实证明,又变成线程不安全了。。
分析:见代码注释
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 void m(){
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mgr05.getInstance().hashCode());
}).start();
}
}
}
六:懒汉式4(也叫双重判断)
既然懒汉式3还是存在问题,那么再加个判断是不是为空呗
说的有道理,我们来动手
这里改动就是在锁里面还加了一层判断,确实修复了上一个的bug
至此,懒汉式的单例模式算是圆满告了一段落
public class Mgr06 {
private static Mgr06 INSTANCE;
private Mgr06(){}
public static Mgr06 getInstance(){
if (INSTANCE == null){
synchronized (Mgr05.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();
}
}
}
七:静态内部类(完美)
采用这种方式是JVM保证单例,也实现了懒加载,perfect
public class Mgr07 {
private Mgr07(){}
private static class Mgr07Holder{
private final static Mgr07 INSTANCE = new Mgr07();
}
public static Mgr07 getInstance(){
return Mgr07Holder.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(Mgr07.getInstance().hashCode());
}).start();
}
}
}
八:枚举(完美中的完美)
其实上述7种方式,都是有问题的(友军,别打我,一般情况够用了,真的)
因为虽然你的构造方法是私有的,但是我们知道,Java里面是有反射的,我们可以通过反射获取该类的构造方法,这是避免不了的。
“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。”
网上扣的代码验证这个不安全
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton s=Singleton.getInstance();
Singleton sUsual=Singleton.getInstance();
Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton sReflection=constructor.newInstance();
System.out.println(s+"\n"+sUsual+"\n"+sReflection);
System.out.println("正常情况下,实例化两个实例是否相同:"+(s==sUsual));
System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(s==sReflection));
}
“单元素的枚举类型已经成为实现Singleton的最佳方法”。
--霸气,这话可不是我说的,源自Java创始人之一的Joshua Bloch写的《effective java》这本书。(所以说没事多读书还是很重要的)
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();
}
}
}
what?这就写完了?
那妙在何处:首先枚举没有构造方法(当然也可以有,默认private,我们这里是没有),也避免了反射、反序列化的问题;
枚举类是JDK1.5才出现的,那之前的程序员面对反射攻击和序列化问题是怎么解决的呢?其实就是像Enum源码那样解决的,只是现在可以用enum可以使我们代码量变的极其简洁了。至此,相信同学们应该能明白了为什么Joshua Bloch说的“单元素的枚举类型已经成为实现Singleton的最佳方法”了吧,也算解决了我自己的困惑。
参考:
1、博客:为什么要用枚举实现单例模式(避免反射、序列化问题)
2、《Effective Java》(第2版):p14-15,p271-274