今天小菜鸡我又来了,分享一个单例模式,叫我知识的搬运工
此博客的源泉: https://www.bilibili.com/video/BV1K54y197iS 【小狂神】
一、了解最基本的单例
1.1 概念:
- 单例类只能有一个实例
- 这个实例由这个单例类内部创建
- 这个实例必须提供给外界
1.2 创建方式
【重要】:构造器私有化
个人认为创建方式可以分为两大类:
1.饿汉式:需要时才创建
2.懒汉式:加载的时候就创建
二、思路及代码实现
2.1 饿汉式基本实现
代码:
public class HungerSingle {
//构造器私有化
private HungerSingle(){}
//创建这个类实例
private HungerSingle single = new HungerSingle();
//给外界一个接口,提供单例实例
//注意:这里只能是static方法,因为该类无法在外部new实例化,构造器私有化了
public static HungerSingle getSingle(){
return single;
}
}
在类加载时就已经创建了,这个模式下,线程是安全的,不同的线程拿到的都是同一个实例,但是这个也存在空间浪费的问题,我不需要的时候也创建了
为了解决上述问题,可以利用懒汉模式
2.2 懒汉式
我需要用的时候再去加载
public class LazySingle{
//构造器私有化
private LazySingle(){}
//定义
private static LazySingle single;
//给外界提供实例
public static LazySingle getSingle(){
//没有才创建
if(single == null){
single = new LazySingle();
}
return single;
}
}
然而。。。。
事情永远都是这么简单就好了
这样写单例会出现问题,及线程不安全
测试一下
class LazySingle{
//构造器私有化
private LazySingle(){
System.out.println(Thread.currentThread().getName());
}
//定义
private static LazySingle single;
//给外界提供实例
public static LazySingle getSingle(){
//没有才创建
if(single == null){
single = new LazySingle();
}
return single;
}
}
public class MyTest{
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazySingle.getSingle();
},"No."+i).start();
}
}
}
结果:获得了多个实例
线程不安全,这事好办,加锁
2.3 双重检测锁(DCL)
class LazySingle{
//构造器私有化
private LazySingle(){
System.out.println(Thread.currentThread().getName());
}
//定义
private static LazySingle single;
//给外界提供实例
public static LazySingle getSingle(){
//没有才加锁,增加效率
if(single == null){
synchronized(LazySingle.class){
//没有才创建
if(single == null){
single = new LazySingle();
}
}
}
return single;
}
}
public class MyTest{
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazySingle.getSingle();
},"No."+i).start();
}
}
}
测试之后,没得问题了
但是我细细的比对大佬的代码后,我发现我少了一个关键字 volatile
如何加?
//定义
private volatile static LazySingle single;
啥是volatile?有啥作用?这就有得说了?
任意门👉 (volatile 待更新。。。。)
总之:加volatile是为了出现脏读的出现,保证操作的原子性。
2.4 静态内部类实现单例(推荐方式)
public class Singleton {
private Singleton(){}
private static class SingleIN{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton getInstance(){
return SingleIN.INSTANCE;
}
}
看这里👇(重要)
你会发现它和前面讲的普通饿汉式很像,我把它也归于饿汉式一类,因为它也是直接就new Singleton,但是它却有着懒加载的效果,而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。
【建议】建议使用静态内部类实现
三、如何破化单例
无聊的操作,没啥卵用哈
【1】如何破坏?
java语言实现动态化的灵魂——反射,说:没有什么是我不能改变的,看我来如何操作。
public class LazySingle {
private static LazySingle single;
private LazySingle(){
}
public static LazySinglegetInstance(){
//第一次判断,没有这个对象才加锁
if(single == null){
//哪个需要保护,就锁哪个
synchronized (LazySingle.class){
//第二次判断,没有就实例化
if(single == null){
single = new LazySingle();
}
}
}
return single;
}
//通过反射破化单例
public static void main(String[] args) throws Exception {
LazySingle single = LazySingle.getInstance();
Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingle single1 = constructor.newInstance();
System.out.println(single == single1);//false
}
}
步骤:
- 通过反射得到字节码对象
- 设置
setAccessible(true)
- 通过构造器对象的
newInstance()
;
【2】如何防止被破坏?
正所谓道高一尺魔高一丈,来battle,battle!
既然这次你是通过得到构造器破化的,那我给构造器加个方法,如果你已经创建了实例,那就抛出异常
public class LazySingle{
private static LazySingle single;
//给构造器加个方法
private Single(){
synchronized(LazySingle.class){
if(single!=null){
throw new RuntimeException("破坏失败");
}
}
}
public static LazySingle getInstance(){
//第一次判断,没有这个对象才加锁
if(single == null){
//哪个需要保护,就锁哪个
synchronized (LazySingle.class){
//第二次判断,没有就实例化
if(single == null){
single = new LazySingle();
}
}
}
return single;
}
//通过反射破化单例
public static void main(String[] args) throws Exception {
LazySingle single = LazySingle.getInstance();
Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingle single1 = constructor.newInstance();
System.out.println(single == single1);//false
}
}
但是这个又有问题,这里的判断是private static SingleSingle single 是否有值,如果我们都不通过getInstance()方法创建对象,而是这样
public static void main(String[] args) throws Exception {
// LazySingle single = LazySingle.getInstance();
Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
//注意:这里的对象不是单例类中里面属性的那个对象
LazySingle single = constructor.newInstance();
LazySingle single1 = constructor.newInstance();
System.out.println(single == single1);//false
}
这里根本不会抛出异常呀,继续改进代码
我们可以利用红路灯原理,防止破化:
及给个全局变量,如果实例化了,这个变量就改变,第二次实例化对象就会匹配不上这个标志了,实现单例化
//加个标志
private static String sign = "password";
private LazySingle(){
synchronized(LazySingle.class){
if(single!=null || !"password".equals(sign)){
throw new RuntimeException("破坏失败");
}else{
sign = "no";
}
}
}
此刻你通过上述main()方法里面的内容测试,发现又会抛出异常。然而我们能通过反射获得构造方法,那我们同样也能通过反射获取对象的属性以及值吧
public static void main(String[] args) throws Exception {
Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
Field field = LazySingle.class.getDeclaredField("sign");
//此处省略通过反射获取该属性的类型和方法....
LazySingle single1 = constructor.newInstance();
//重新变回原标志位
field.set("sign","password");
LazySingle single2 = constructor.newInstance();
System.out.println(single2 == single1);//false
}
再实例化之前,我把标志改回来,又不能绝对单例了,emmmm。。。。
四、枚举实现绝对单例(拓展)
利用枚举实现不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化(菜鸟教程官方术语)
public enum Singleton {
INSTANCE;
public Singleton getInstance() {
return INSTANCE
}
}
这里又涉及到枚举了。。。。。
通过反射不能破化枚举
原因:枚举的构造方法不能为public ,语言特性。。。
详情了解。。。
任意门👉 (枚举,待更新。。。。。)
吐了吐了。。。。就到这里了,挖的坑有时间再填,写博客这玩意太耗时间了。