文章目录
优快云一位大佬关于单例模式的博文讲的很好:https://blog.youkuaiyun.com/jason0539/article/details/23297037/
参考视频:https://www.bilibili.com/video/BV1G4411c7N4?p=33&spm_id_from=pageDriver
1. 单例模式的特点
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他类提供这一实例。
2. 饿汉式单例
2.1 饿汉式(静态常量)
/**
* 饿汉式单例模式
* 饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。
*
* 1. 构造器私有化。避免外部通过new关键字被实例化。在同一个虚拟机内,Hungry的唯一实例只能通过getInstance()创建。
* (事实上,java的反射机制可以创建private修饰构造器的类,这样会使单例模式失效。)
* 2. 开始一上来,就直接定义一个private static final Hungry的实例instance,然后通风new在定义处初始化。
* 3. 通过公共静态方法向外部提供该对象的引用
*
* 存在的问题:
* 因为一上来就创建好所有的对象。如果这些对象很消耗内存空间,就会出问题。
*/
public class Hungry {
// 验证内存占用问题
/*
private static Byte[] data1 = new Byte[1024 * 1024];
private static Byte[] data2 = new Byte[1024 * 1024];
private static Byte[] data3 = new Byte[1024 * 1024];
private static Byte[] data4 = new Byte[1024 * 1024];
private static Byte[] data5 = new Byte[1024 * 1024];
private static Byte[] data6 = new Byte[1024 * 1024];
*/
private Hungry() { }
private static final Hungry instance = new Hungry();
//静态工厂方法
// 对外暴露一个public static 的方法,返回该实例独享的引用。
public static Hungry getInstance() {
return instance;
}
}
// 测试
import com.qww.supplement.singleton.hungry.Hungry;
public class Test06 {
public static void main(String[] args) {
Hungry instance = Hungry.getInstance();
System.out.println(instance == new A().f());
}
}
class A {
public Hungry f(){
return Hungry.getInstance();
}
}
// 运行结果
true
2.2 饿汉式(静态代码块)
public class Hungry {
private Hungry() { }
private static Hungry instance;
static {
instance = new Hungry();
}
//静态工厂方法
// 对外暴露一个public static 的方法,返回该实例独享的引用。
public static Hungry getInstance() {
return instance;
}
}
3. 懒汉式单例
3.1 懒汉式单例(非线程安全)
/**
* 懒汉式单例
*
* 1. 构造器私有化。
* 2. 开始一上来,不像饿汉式单例那样,就直接在类加载时创建对象,而是在调用getInstance*()方法且instanc为null时才创建对象。
* 3. 在向外部提供对象引用的静态方法体中,对对象引用做null判断,如果为null才创建对象对该私有字段进行初始化,最后将对象引用返回。
* 4. 同时考虑到要工作在多线程环境下,对多个线程会创建多个对象,所以需要使用synchronized进行同步。
*/
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName() + " ok!");
}
private static LazyMan instance;
// 静态工厂方法
public static LazyMan getInstance() {
/*
这在多线程环境下,是线程不安全的,会出现多个LazyMan实例
*/
if (instance == null) {
instance = new LazyMan();
}
return instance;
}
// 测试
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(new Runnable() {
@Override
public void run() {
LazyMan instance = LazyMan.getInstance();
}
}).start();
}
}
}
// 运行结果
Thread-0 ok!
Thread-3 ok!
Thread-2 ok!
Thread-1 ok!
3.2 多线程环境下的懒汉式单例
3.2.1 在getInstance()方法上加同步
/**
* 懒汉式单例
*
* 1. 构造器私有化。
* 2. 开始一上来,不像饿汉式单例那样,就直接创建对象,而是不对该私有字段初始化,
* 3. 在向外部提供对象引用的静态方法体中,对对象引用做null判断,如果为null才创建对象对该私有字段进行初始化,最后将对象引用返回。
* 4. 同时考虑到要工作在多线程环境下,对多个线程会创建多个对象,所以需要使用synchronized进行同步。
*
* 缺点:直接同步方法效率太低了。每个线程调用getInstance()方法获取实例时,都需要同步。
* 但是其实这个方法执行一次实例化代码就够了。后面的获取实例直接return就行了。
*
*/
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName() + " ok!");
}
private static LazyMan instance;
// 静态工厂方法
public static synchronized LazyMan getInstance() {
if (instance == null) {
instance = new LazyMan();
}
return instance;
}
}
3.2.2 同步代码块
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName() + " ok!");
}
private static LazyMan instance;
// 静态工厂方法
public static LazyMan getInstance() {
/*
这在多线程环境下,是线程不安全的,会出现多个LazyMan实例
如果一个线程进入if (instance == null)判断语句,刚刚通过。还没有来得及执行下面的语句
而另一个线程也通过if判断语句,那么就会出现两个线程同时执行instance = new LazyMan();语句
从而出现多个实例。
*/
/*
if (instance == null) {
synchronized (LazyMan.class) {
instance = new LazyMan();
}
}
*/
synchronized (LazyMan.class) {
if (instance == null) {
instance = new LazyMan();
}
}
return instance;
}
// 测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
LazyMan instance = LazyMan.getInstance();
}
}).start();
}
}
}
3.2.3 双重检查锁定
// 懒汉式单例 双重检查(Double Check)
public class Singleton {
private Singleton() { }
private static volatile Singleton instance;
public static Singleton getInstance() {
/*
在多线程环境下,a线程先进入到外层if (instance == null)判断语句
此时instance是nul,通过条件判断,界下来会拿到一个Singleton.class对象锁,然后进入同步代码块。
假如此时b线程也进入到了外层if (instance == null)判断语句,此时instance也是nul,所以也通风判断语句。
但是碰到了synchronized关键字,需要获得Singleton.class这把还有a线程占据的对象锁才能执行下面的代码。
等a线程通过内层if语句。并执行完同步代码块之后,释放了锁对象后。
b线程获得锁对象,进入到同步代码块,然后进行if判断,但此时instance已经不是null了,
所以不会通过判断,从而去创建新的对象。
后面的线程(x线程和d线程)如遇到getInstance()方法体,先进行外层的if语句判断,
但是前面的a线程已经创建过对象,对instance初始化过来看,所以也就不会进入通过if判断语句。已经return了。
这种双重检查方法,既保证了线程安全问题,又兼顾了性能。
*/
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
3.2.4 静态内部类
既保证了线程安全,又避免了同步带来的性能影响。
Singleton
的类加载不会导致它的静态内部类SingletonHolder
的类加载。只有当调用getInstance()
静态方法的时候,会导致静态内部类SingletonHolder
类加载。而jvm底层装载类的过程是线程安全的,不会有多个线程进入。
// 懒汉式单例 静态内部类
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
3.2.5 枚举
public enum Singleton {
INSTANCE;
public void f() {
System.out.println("ok !");
}
}
4. 饿汉式和懒汉式区别
5. jdk中典型单例模式的原码分析
java.lang.Runtime
单例模式使用的饿汉式。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
// ...
}