狂神的单例模式是真的讲的非常好理解,这里附上链接:
【狂神说Java】单例模式-23种设计模式系列_哔哩哔哩_bilibili
单例中最重要的就是构造方法私有(一旦构造方法私有,别人就无法new这个对象了)
是23种设计模式中最简单的
目录
单例模式的概念及优缺点
单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式包含角色:Singleton
这样的模式有几个好处:
1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。节约系统资源
2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。
缺点:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。在一定程度是违背了“开闭原则”。
在以下情况下可以使用单例模式:
饿汉式单例
一开始就将所有东西全部加载进来,相当于对象已经存在了,非常占用内存资源
//饿汉式单例
public class Hungry{
//可能会浪费空间
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
//构造方法私有
private Hungry(){
}
private final static Hungry HUNGRY = new Hungry()l;
public static Hungry getInstance(){
return HUNGRY;
}
}
懒汉式单例
对象是用的时候才去加载,而不是一开始就加载
//懒汉式单例
public class LazyMan{
private LazyMan(){
}
private static LazyMan LAZYMAN;
public static LazyMan getInstance(){
if(LAZYMAN == null){
LAZYMAN = new LazyMan();
}
return LAZYMAN;
}
}
这个方式的单线程下是OK的,在多线程并发中是会出现问题的:
//懒汉式单例
public class LazyMan{
private LazyMan(){
//打印线程名称
System.out.println(Thread.currentThread().getName());
}
private static LazyMan LAZYMAN;
public static LazyMan getInstance(){
if(LAZYMAN == null){
LAZYMAN = new LazyMan();
}
return LAZYMAN;
}
//多线程并发
public static void main(String[] args){
for(int i=0;i<10;i++){
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
运行结果(偶尔成功,偶尔失败,结果随机):
针对多线程,添加锁模式
双重检测模式的懒汉式单例(DCL懒汉式)
注意volatile和synchronized的区别:前者保证有序性,后者保证原子性
volatile只能保证单个线程的可见性,不能保证多个线程的可见性
之所以去加volatile的原因就是因为实例化时不时原子性操作
//懒汉式单例
public class LazyMan{
private LazyMan(){
//打印线程名称
System.out.println(Thread.currentThread().getName());
}
private volatile static LazyMan LAZYMAN;
public static LazyMan getInstance(){
if(LAZYMAN == null){
synchronized (LazyMan.class){
if(LAZYMAN == null) {
LAZYMAN = new LazyMan();//不是原子性操作
/**
* 会进行的操作:
* 1,分配内存空间
* 2,执行构造方法,初始化对象
* 3,将这个对象指向这个空间
* 容易出现顺序乱的情况,比如132等
*
* 添加volatile关键字和synchronized加锁
*/
}
}
}
return LAZYMAN;
}
//多线程并发
public static void main(String[] args){
for(int i=0;i<10;i++){
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
静态内部类实现(推荐)
//静态内部类
public class Holder{
private Holder() {
}
//获取实例
public static Holder getInstance() {
return InnerClass.HOLDER;
}
//在内部类中创建对象
private static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
反射(单例不安全)
上述三种单例都是不安全的,只要有反射,任何代码都是不安全的
反射:可以通过class模板得到你写的所有方法和属性
在懒汉式单例中
import java.lang.reflect.Constructor;
//懒汉式单例
public class LazyMan{
private LazyMan(){
}
private volatile static LazyMan LAZYMAN;
public static LazyMan getInstance(){
if(LAZYMAN == null){
synchronized (LazyMan.class){
if(LAZYMAN == null) {
LAZYMAN = new LazyMan();
}
}
}
return LAZYMAN;
}
//反射
public static void main(String[] args) throws Exception{
LazyMan instance = LazyMan.getInstance();
//添加null,无参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//setAccessible是一个非常霸道的方法,会无视私有的构造器,通过反射来创建对象
declaredConstructor.setAccessible(true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
可以看到结果是已经破坏了。
在私有的构造方法中加锁,不允许破坏
private LazyMan(){
synchronized (LazyMan.class) {
if(LAZYMAN == null){
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
再次运行就会报错
但是如果两次都用反射破坏呢?这要怎么解决呢?可以设置一个“红绿灯”,也就是定义一个都不知道的布尔变量,外部通过反射的情况是找不到这个变量的。
import java.lang.reflect.Constructor;
//懒汉式单例
public class LazyMan{
//设置一个“红绿灯”
private static boolean pipisong = false;
private LazyMan(){
synchronized (LazyMan.class) {
if(pipisong == false) {
pipisong = true;
}else {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
private volatile static LazyMan LAZYMAN;
public static LazyMan getInstance(){
if(LAZYMAN == null){
synchronized (LazyMan.class){
if(LAZYMAN == null) {
LAZYMAN = new LazyMan();
}
}
}
return LAZYMAN;
}
//反射
public static void main(String[] args) throws Exception{
//添加null,无参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//setAccessible是一个非常霸道的方法,会无视私有的构造器,通过反射来创建对象
declaredConstructor.setAccessible(true);
LazyMan instance = declaredConstructor.newInstance();
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
实际上,别人也可以获取到我们设置的“红绿灯”,并且将其破坏掉。所以程序没有办法做到绝对的安全,所谓“道高一尺,魔高一丈”。
Field pipisong = LazyMan.class.getDeclaredField("pipisong");
pipisong.setAccessible(true);
枚举enum与反射_A person,A fool的博客-CSDN博客
所以为了单例安全,我们可以考虑使用枚举,但是需要注意的是枚举中没有无参构造,而是有两个参数(String,int)
序列化和反序列化也能破坏单例
首先我们要知道什么是序列化和反序列化:
序列化:把对象转换为字节序列的过程称为对象的序列化
反序列化:把字节序列恢复为对象的过程称为对象的反序列化
可以添加方法解决这个问题:
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
public Object readResolve() {
return getInstance();
}