N久之前做的笔记,现在搬迁
和曾丹模拟的时候,讲到单例模式,她问我为什么叫懒汉?为什么叫饿汉?
懒汉式:就是非常懒,只有当你要用他的时候,他才会去行动,去产生实例类加载时不初始化
在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢
package Singleton;
public class LazySingleton {
//懒汉式单例模式
//比较懒,在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢
private static LazySingleton intance = null;//静态私用成员,没有初始化
private LazySingleton()
{
//私有构造函数
}
public static synchronized LazySingleton getInstance() //静态,同步,公开访问点
{
if(intance == null)
{
intance = new LazySingleton();
}
return intance;
}
}
//
饿汉式:饿汉就是我不能等待,创建类的时候就拥有实例类加载时就完成了初始化,所以类加载会比较慢,而获取对象的速度很快
package Singleton;
public class EagerSingleton {
//饿汉单例模式
//在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
private static EagerSingleton instance = new EagerSingleton();//静态私有成员,已初始化
private EagerSingleton()
{
//私有构造函数
}
public static EagerSingleton getInstance() //静态,不用同步(类加载时已初始化,不会有多线程的问题)
{
return instance;
}
}
单例模式特点:
1)私有构造函数
2)静态私有成员
3)公开访问点getInstance
饿汉懒汉的区别:
饿汉在类加载的时候就加载了,懒汉只有在使用的时候才会返回
在并发的情况下,懒汉式是不安全的。如果两个线程,我们称它们为线程A和线程B,在同一时间调用getInstance()方法,如果线程A先进入if块,此时instance是为null,然后线程B进入if块,并且创建实例,B退出,A再次进入,A再次实例化的值会覆盖B实例化的值
---
//
对懒汉式的同步有几种方式
synchronized代码块在AsyncTask的应用
private static Handler getHandler() {
synchronized (AsyncTask.class) {
if (sHandler == null) {
sHandler = new InternalHandler();
}
return sHandler;
}
}
synchronized代码块的双重检测
//用volatile防止指令重排,不是很懂,需要后面再看
private volatile static Handler sHandler;
public static Handler getHandler() {
if(sHandler == null)
{
synchronized (AsyncTask.class) {
if (sHandler == null) {
sHandler = new InternalHandler();
}
return sHandler;
}
}
}
同步方法
public static synchronized Singleton getInstance() //静态,同步,公开访问点
{
if(intance == null)
{
intance = new Singleton();
}
return intance;
}
lock双重判断
public static Singleton getInstance(){
if(intance == null){
lock.lock();
try {
if(intance == null){
intance = new Singleton();
}
} finally{
lock.unlock();
}
}
return intance;
}
---
/
- 双重检查锁定模式(并行模式)
- 为什么要双重检测
- 同步的代价很高,会降低100倍或者更高的性能(Boehm, Hans-J. "Threads Cannot Be Implemented As a Library", ACM 2005, p265)所以为了减少不必要的同步,要验证锁定条件
- 双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。
- 其实就是线程A发现变量没有被初始化才需要加锁同步去创建实例,如果说已经初始化了,就只要调用就好了。
- 为什么同步块内还需再做一次检测呢?举个例子,线程A访问到if块时被线程B挤下去了,然后
线程B获取锁并创建实例退出,线程A再次获取锁,检测instance是否为null,此时可以避免线程B所创建的值。所以,double check的目的才是避免线程不安全。single check只是为了提高性能。
- 至于这里采用指令重排,不是很懂
静态内部类创建实例
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
为什么要使用静态内部类来创建单例模式呢?饿汉已经很简便了,但饿汉式在类加载时就会初始化,不管用不用都会占用内存,所以采用静态内部类来完善。
想想以前陈老师讲过,C++的单例模式也是靠这种方式来释放instance的内存的。肯定不能再析构函数中释放。
其实这种写法也比较麻烦,虽然创建静态内部类没什么不好?枚举,最为简单。默认线程安全并且可以防止反序列化重新创建对象。不能反序列化这个倒是可以解释:
public enum EasySingleton{
INSTANCE;
}
因为序列化反序列都是针对对象的,被 static 修饰的变量就不能序列化。而枚举等同于实现static
、final的功能。所以不能序列化是可以解释的。
package com.oreilly.tiger.ch03;
public enum Grade {
A, B, C, D, F, INCOMPLETE
};
public class OldGrade {
public static final int A = 1;
public static final int B = 2;
public static final int C = 3;
public static final int D = 4;
public static final int F = 5;
public static final int INCOMPLETE = 6;
}
至于枚举类为什么是线程安全的?
java类的加载和初始化过程都是线程安全的
阅读更多,点击一下链接:
- 维基百科-双重检查锁定模式
https://zh.wikipedia.org/wiki/%E5%8F%8C%E9%87%8D%E6%A3%80%E6%9F%A5%E9%94%81%E5%AE%9A%E6%A8%A1%E5%BC%8F
- 如何正确地写出单例模式
http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
- 枚举类型入门-IBM
http://www.ibm.com/developerworks/library/j-enums/j-enums-pdf.pdf
- 深度分析Java的ClassLoader机制(源码级别)
http://www.hollischuang.com/archives/199
- Java类的加载、链接和初始化
http://www.hollischuang.com/archives/201