我是清风~,每天学习一点点,快乐成长多一点,这些都是我的日常笔记以及总结。
适用场景
优点
缺点
重点
单例-Double Check
注重延迟加载,只有使用它的时候,才会初始化,不使用就不会加载LazySingleton对象
synchronized 这里的关键字可以防止两个拿到不是同一对象
通过这种同步的方式,解决了懒汉式在多线程可能引起的问题
但是我们也知道同步锁synchronized也比较消耗资源,有加锁和解锁的开销,
而且synchronized修饰static时候,锁的是class,这个锁范围是很大的,对性能有一定的影响。
public class LazySingleton {
/**首先申明静态对象**/
private static LazySingleton lazySingleton = null;
//懒汉式就是比较懒,初始化的时候是没有被创建的
//延迟加载,构造器是private
private LazySingleton(){
if(lazySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
/**获取LazySingleton的方法**/
//不加synchronized时候,单线程是可以的
//但是多线程来使用单例的话
//lazySingleton = new LazySingleton();
// 这里的lazySingleton会被new两次
public synchronized static LazySingleton getInstance(){
//空判断
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
public class T implements Runnable {
@Override
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName()+" "+lazySingleton);
}
}
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("program end");
}
}
输出结果
program end
Thread-0 com.qingfeng.singleton.LazySingleton@710a3057
Thread-1 com.qingfeng.singleton.LazySingleton@710a3057
懒汉式,这种方式兼顾了性能和性能安全
它是在哪里检查呐?
首先方法不用锁了,而是把锁定放到了方法体中。
所有还是先进行判断,判断完成之后,我们把锁定这个单例的类
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
//锁定这个类
synchronized (LazyDoubleCheckSingleton.class){
//加锁之后,还要做一层空的判断
//不如不加锁,不给返回,不为null也只有一个线程进去
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
//这里有个隐患,这里有可能还没有初始化们,这里经历了三个步骤
//1.分配内存给这个对象
// //3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
//2.初始化对象(出现重排序,颠倒了)
// intra-thread semantics互换位置不会改变单线程内的重排结果
// ---------------//3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
}
}
}
return lazyDoubleCheckSingleton;
}
}
演示的是单线程情况
首先从上到下,是线程执行的时间,
首先分配对象内存空间
第二步,正常应该是初始化对象
第三步,设置lazyDoubleCheckSingleton 指向内存空间(lazyDoubleCheckSingleton为instance)
(所以2和3怎么换顺序,第四步都是在最后)
2和3的重排序对结果没有影响
下面是多线程演示
时间还是从上之下,
【线程0】在左侧,右侧是【线程1】
第一步,先分配内存空间
假设【线程0】重排了,先设置了单例对象指向内存空间,
这个时候【线程1】判断instance是否为null,这个时候判断出来,instance并不为null
因为它有指向内存空间
然后【线程1】开始访问内存对象。就说线程1比线程0更早的访问对象
所以【线程1】访问到的对象是在线程0中还没有初始化完成的对象,这个时候就有问题了
这个对象并没有完整的被初始化,系统要报异常
对于第二步和第三步重排序,并不影响第四步步骤
解决:
方案一:可以不让第二步和第三步重排序,
方案儿:允许线程0里面的2和3重排序,但是不能允许线程1看到这个重排序
代码实现
加一个关键字volatile实现线程安全的初始化
加了volatile之后,所有线程都能看见共享内存的这些状态,保证了内存的可见性
在volatile修饰的共享变量,在进行写操作的时候,会多一些汇编代码
起到两个作用
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
//锁定这个类
synchronized (LazyDoubleCheckSingleton.class){
//加锁之后,还要做一层空的判断
//不如不加锁,不给返回,不为null也只有一个线程进去
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
这个解决方案是通过静态内部类解决
权限要声明为private
public class StaticInnerClassSingleton {
//权限要声明为private
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton
= new StaticInnerClassSingleton();
}
//对外开放这个方法
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
if(InnerClass.staticInnerClassSingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
}
JVM在类的初始化阶段,也就是class被加载后,并且被线程使用之前,都是类 的初始化阶段。
在这个阶段会执行类的初始化
在执行类的初始化期间,JVM会获取一个锁,这个锁可以同步多个线程对一个类的初始化(就说绿色部分)
基于这个特性,可以实现基于静态内部类的,并且线程安全的延迟初始化方案
对于步骤2和步骤3,线程1并不会看到,就是说非构造线程是不允许看到这个重排序的
因为之前是【线程0】来构造这个单例对象
初始化一个类,包括执行这个类的静态初始化,还有初始化种,在这个类申明的静态变量
根据Java语言规范,主要分为五种情况
关于面试方面:单例模式在面试过程非常重要,一层一层迭代讲解
最简单的实例化
在类加载的时候就完成实例化
优点:
缺点:
public class HungrySingleton implements Serializable,Cloneable{
//这样对象就不可改了,因为在类加载的时候就初始化好了
private final static HungrySingleton hungrySingleton = new hungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
也可以把单例实例化过程放到静态代码块里面
public class HungrySingleton implements Serializable,Cloneable{
//这样对象就不可改了,因为在类加载的时候就初始化好了
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
延迟加载
在类加载的时候初始化——饿汉式
调用之后在初始化——懒汉式
有什么技巧记忆
饿汉式,很饿,一上来就要吃东西,一上来就把对象new好了
懒汉式,比较懒,你不用它的时候,他都不会创建对象
public class Test {
public static void main(String[] args) {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
//读取ObjectInputStream,跟上面声明的做对比,是不是同一个对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (HungrySingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
输出结果
com.qingfeng.singleton.HungrySingleton@312b1dae
com.qingfeng.singleton.HungrySingleton@5a2e4553
false
看到instance这个对象312b1dae和
newInstance是5a2e4553,他们是不相等的
这就违背了单例模式的初衷
通过序列化和反序列拿到了不同的对象
解法在单例类里面写一个方法
public class HungrySingleton implements Serializable,Cloneable{
//这样对象就不可改了,因为在类加载的时候就初始化好了
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object readResolve(){
return hungrySingleton;
}
}
在运行测试test
输出结果
com.qingfeng.singleton.HungrySingleton@312b1dae
com.qingfeng.singleton.HungrySingleton@312b1dae
true
这里结果就相等了,肯定会存在疑问,为什么在单例里面加上readResolve就可以了
用的反射
反射攻击解决方案
通过反射,打开构造器的权限,然后获取这个对象
通过权限将构造器设置为ture,这个单例里面的构造器private权限就放开了
对象就能实例化出来
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException,
NoSuchMethodException, IllegalAccessException, InvocationTargetException,
InstantiationException {
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
输出显示
com.qingfeng.HungrySingleton@12edcd21
com.qingfeng.HungrySingleton@34c45dca
false
在多线程的时候是没有问题的
枚举类天然的可序列化机制,能够强有力的保证不会出现多次实例化情况
枚举类的单例可能是单例模式中最佳实践
public enum EnumInstance {
INSTANCE{
protected void printTest(){
System.out.println("qingfeng Print Test");
}
};
protected abstract void printTest();
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
容器单例就是
通过map来实现一个单例对象的容器
这里面保证key的合法性和唯一性
这种写法,非常适合在程序初始化的适合
我们把多个单例对象放入到singletonMap统一管理
使用的时候,通过key直接map当中获取单例对象
public class ContainerSingleton {
private ContainerSingleton(){
}
private static Map<String,Object> singletonMap = new HashMap<String,Object>();
public static void putInstance(String key,Object instance){
if(StringUtils.isNotBlank(key) && instance != null){
if(!singletonMap.containsKey(key)){
singletonMap.put(key,instance);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
这个单例不能保证整个应用全局唯一,但是可以保证线程唯一
TreadLocal隔离多个线程,对数据的访问冲突
对于多线程共享问题,如果我们使用同步锁,其实就是以时间和空间的方式,因为要排队
要是 TreadLocal,就是用空间换时间的方式,他会创建很多对象,至少在一个线程里创建一个
但是对于这个线程,他获取对象是唯一的
public class ThreadLocalInstance {
private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
= new ThreadLocal<ThreadLocalInstance>(){
//重写初始化方法
@Override
protected ThreadLocalInstance initialValue() {
return new ThreadLocalInstance();
}
};
private ThreadLocalInstance(){
}
public static ThreadLocalInstance getInstance(){
return threadLocalInstanceThreadLocal.get();
}
}
ctrl+n 在Runtime下面
这里直接返回了currentRuntime
进到返回currentRuntime
这个对象是一个static,并且在类加载的时候就创建出来了,属于饿汉式
spring的单例是基于容器的
如果写一个主函数获得一个应用,启动了多个容器,在每个容器都能达到这个单例对象
spring单例是bean作用域中的一个,
这个作用域在每一个应用程序上下文中仅创建一个,我们设置应该单例的属性的实例
和设计模式最大区别
所有spring容器即使是单例,也可以拿出来使用
spring中也可以找到单例的影子
进去,后可以发现这里做了一些判断
AbstractFactoryBean
如果整个对象是单例,
就执行return程序,,可以看到是否已经初始化,初始化了直接返回,
如果没有初始化,调用getEarlySingletonInstance方法
否则不是单例,直接创建instance
[Java设计模式] 建造者模式
[Java设计模式] 抽象工厂—产品等级结构与产品族
[Java设计模式] 简单工厂和工厂方法