单例模式的五种写法
程序员文章站
2022-07-13 23:38:04
...
一、饿汉式单例
饿汉式单例模式,实例对象在初始化的时候创建,不管有没有得到调用。好处是没有线程安全的问题,坏处是浪费内存空间。
//饿汉式单例
public class ehan {
/**
* 存在的问题:
* 饿汉式强调的是一上来就初始化对象,初始化的对象里面又有很多像如下定义的很多数组同时也被初始化,
* 但没有得到调用,就会存在浪费空间的情况
* */
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 ehan(){}
//实例对象初始化时创建
private static final ehan HUNGRY = new ehan();
public static ehan getInstance(){
return HUNGRY;
}
public static void main(String[] args) {
ehan ehan1 = ehan.getInstance();
ehan ehan2 = ehan.getInstance();
System.out.println(ehan1==ehan2);//true
}
}
二、懒汉式
我们知道饿汉式在初始化的时候就完成了对象的创建,但存在浪费空间的问题,因此,懒汉式单例模式应运而生。
懒汉式顾名思义就是只有当需要的时候才会去初始化数据。
//懒汉式单例模式
public class lanhan {
//构造器私有
private lanhan(){
}
private static lanhan lazyMan;
public static lanhan getInstance(){
if(lazyMan==null){
lazyMan = new lanhan();
}
return lazyMan;
}
public static void main(String[] args) {
lanhan instance1 = lanhan.getInstance();
lanhan instance2 = lanhan.getInstance();
System.out.println(instance1==instance2);
}
}
三、DCL(Double Check Lock)懒汉式
由于上述的懒汉式在单线程里确实是安全的,但是在多线程中,其实是并不安全的,我们可以通过以下代码测试:
我们会发现,得出的结果并不是单例的。
//懒汉式单例模式
public class lanhan {
//构造器私有
private lanhan(){
System.out.println(Thread.currentThread().getName()+"OK");
}
private static lanhan lazyMan;
public static lanhan getInstance(){
if(lazyMan==null){
lazyMan = new lanhan();
}
return lazyMan;
}
/**
* 单线程下确实单例安全,但是多线程下是存在安全问题的
* 我们得加锁
* */
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(()->{
lanhan.getInstance();
}).start();
}
TimeUnit.SECONDS.sleep(3);
lanhan instance1 = lanhan.getInstance();
lanhan instance2 = lanhan.getInstance();
System.out.println(instance1==instance2);
}
}
因此我们得给他上锁,保证一个线程只能创建一个对象,也有且只能有一个对象。同时为了防止指令重排现象出现,我们得对变量加volatile锁。
//DCL懒汉式单例模式
public class lanhan {
//构造器私有
private lanhan(){
System.out.println(Thread.currentThread().getName()+"OK");
}
//防止指令重排
private static volatile lanhan lazyMan;
//双重检测锁模式--DCL懒汉式检测
public static lanhan getInstance(){
//先判断有没有对象实例被创建,没有的话就锁住这个类,确保只有一个Class对象
if(lazyMan==null){
synchronized (lanhan.class){
if(lazyMan==null){
//但是还可能存在一个问题,因为new lanhan()不是一个原子操作
/*
* 1.分配内存
* 2.执行构造方法
* 3.指向地址
* 有可能存在的问题就是由于指令重排,它的执行顺序完全有可能是132,这个时候由于指向了内存地址,
* 会被以为创建了对象,导致无法创建对象,为了保证不发生指令重排现象,我们必须加上volatile关键字
* */
lazyMan = new lanhan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(()->{
lanhan.getInstance();
}).start();
}
TimeUnit.SECONDS.sleep(3);
lanhan instance1 = lanhan.getInstance();
lanhan instance2 = lanhan.getInstance();
System.out.println(instance1==instance2);
}
}
四、静态内部类
可以延时加载,并且线程安全
//静态内部类实现
public class Holder {
//构造器私有
private Holder(){
}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
//静态内部类
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
public static void main(String[] args) {
Holder instance = Holder.getInstance();
Holder instance2 = Holder.getInstance();
System.out.println(instance==instance2);
}
}
五、反射破坏单例模式
java中存在反射机制,能够改变内部的private关键字,因此使用private修饰的其实都不算是安全的,由此引出枚举单例模式。
//反射破坏单例模式
public class fanshe {
//构造器私有
private fanshe(){
System.out.println(Thread.currentThread().getName()+"OK");
}
private static volatile fanshe lazyMan;
public static fanshe getInstance(){
if(lazyMan==null){
synchronized (fanshe.class){
if(lazyMan==null){
lazyMan = new fanshe();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
fanshe instance = fanshe.getInstance();
//利用反射获取其对应的构造器方法
Constructor<fanshe> declaredConstructor = fanshe.class.getDeclaredConstructor(null);
//无视私有构造器,通过反射创建对象
declaredConstructor.setAccessible(true);
fanshe instance2 = declaredConstructor.newInstance();
System.out.println(instance==instance2);
}
}
如何解决反射破坏单例现象?第一种方法,我们可以在构造器里面对类加锁。
//反射破坏单例模式
public class fanshe {
//构造器私有
private fanshe(){
synchronized (fanshe.class){
if(lazyMan==null){
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
private static volatile fanshe lazyMan;
public static fanshe getInstance(){
if(lazyMan==null){
synchronized (fanshe.class){
if(lazyMan==null){
lazyMan = new fanshe();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
fanshe instance = fanshe.getInstance();
//利用反射获取其对应的构造器方法
Constructor<fanshe> declaredConstructor = fanshe.class.getDeclaredConstructor(null);
//无视私有构造器,通过反射创建对象
declaredConstructor.setAccessible(true);
fanshe instance2 = declaredConstructor.newInstance();
System.out.println(instance==instance2);
}
}
但是上面那种方法治标不治本,因为第一个instance我们是通过getInstance方法创建的,没经过反射,如果我们两个对象都是通过反射创建,如下代码所示,还是会存在问题。
//反射破坏单例模式
public class fanshe {
//构造器私有
private fanshe(){
synchronized (fanshe.class){
if(lazyMan!=null){
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
private static volatile fanshe lazyMan;
public static fanshe getInstance(){
if(lazyMan==null){
synchronized (fanshe.class){
if(lazyMan==null){
lazyMan = new fanshe();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
//fanshe instance = fanshe.getInstance();
//利用反射获取其对应的构造器方法
Constructor<fanshe> declaredConstructor = fanshe.class.getDeclaredConstructor(null);
//无视私有构造器,通过反射创建对象
declaredConstructor.setAccessible(true);
fanshe instance2 = declaredConstructor.newInstance();
fanshe instance3 = declaredConstructor.newInstance();
System.out.println(instance3==instance2);
}
}
由此引入第二种方法,通过定义一个信号量来解决:
//反射破坏单例模式
public class fanshe {
//定义一个信号量
private static boolean flag = false;
//构造器私有
private fanshe(){
synchronized (fanshe.class){
//通过反射的方式无法获取到这个信号量,因此不能通过反射创建
if(flag==false){
flag=true;
}
else{
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
private static volatile fanshe lazyMan;
public static fanshe getInstance(){
if(lazyMan==null){
synchronized (fanshe.class){
if(lazyMan==null){
lazyMan = new fanshe();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
//fanshe instance = fanshe.getInstance();
//利用反射获取其对应的构造器方法
Constructor<fanshe> declaredConstructor = fanshe.class.getDeclaredConstructor(null);
//无视私有构造器,通过反射创建对象
declaredConstructor.setAccessible(true);
fanshe instance2 = declaredConstructor.newInstance();
fanshe instance3 = declaredConstructor.newInstance();
System.out.println(instance3==instance2);
}
}
六、枚举单例
利用枚举的方式创建单例模式,可以避免反射对其内部进行关键字的破坏,继而导致单例模式失效。它的缺点也是不能延时加载
public enum meiju{
INSTANCE;
public meijugetInstance(){
return INSTANCE;
}
}
class test05{
public static void main(String[] args) {
meijuinstance1 = SingletonTest05.INSTANCE;
meijuinstance2 = SingletonTest05.INSTANCE;
System.out.println(instance1==instance2);
}
}