乐趣区

关于java:单利模式与多线程

我是阿福,公众号「JavaClub」作者,一个在后端技术路上摸盘滚打的程序员,在进阶的路上,共勉!
文章已收录在 JavaSharing 中,蕴含 Java 技术文章,面试指南,资源分享。

一、为什么应用单利模式

单利:外表的意思就是一个类只能存在一个实例,那咱们什么时候会用到单利模式呢?

最常见的有以下几种场景

1、Windows 的 Task Manager(工作管理器)就是很典型的单例模式
2、我的项目中,读取配置文件的类,个别也只有一个对象。没有必要每次应用配置文件数据,每次 new 一个对象去读取。
3、数据库连接池的设计个别也是采纳单例模式,因为数据库连贯是一种数据库资源。
4、在 Spring 中,每个 Bean 默认就是单例的,这样做的长处是 Spring 容器能够治理。
5、在 servlet 编程中 /spring MVC 框架,每个 Servlet 也是单例 / 控制器对象也是单例.

综上所述单利有两个益处:

1、节俭内存
2、方便管理

二、单利模式的定义及特点

单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创立这个实例的一种模式。

单例模式有 三个特点:

1、类只有一个实例对象;
2、该单例对象必须由单例类自行创立;
3、类对外提供一个拜访该单例的全局拜访点;

三、单利模式的实现形式

咱们实现单利模式次要抓住三个特点:

1、将构造函数私有化
2、在类的外部创立实例
3、提供获取惟一实例的办法

1、懒汉模式

该模式的特点是在类加载时没有创立实例,只有第一次调用办法 getInstance()时才会创立单利对象,具体代码如下:

/**
 * 懒汉式单利模式(线程平安,调用效率不高,然而,能够实现延时加载)*/
public class SingletonL {
    private static SingletonL instance = null;

    private SingletonL() {  // 防止类在内部被实例化}

    // 创建对象的内部办法
    public static SingletonL getInstance() {if (null == instance) {instance = new SingletonL();
        }
        return instance;
    }
}

这个试验尽管能获得一个对象的实例,然而在多线程的环境中,就会取的多个实例对象,那么如何解决懒汉模式在多线程中的问题呢?

申明 synchronized 关键字

既然多线程能够同时进入 getInstance 办法,那么只须要对 getInstance 办法申明 synchronized 关键字即可。

批改 SingletonL.java 后的代码如下:

public static synchronized SingletonL getInstance() 

自定义线程类 MyThread1.java, MyThread2.java 代码如下:

public class MyThread1 extends Thread{

    @Override
    public void run() {System.out.println(SingletonL.getInstance().hashCode());
    }
}

public class MyThread2 extends Thread{

    @Override
    public void run() {System.out.println(SingletonL.getInstance().hashCode());
    }
}

创立测试类Test.java, 代码如下:

public class Test {public static void main(String[] args) {MyThread1 myThread1=new MyThread1();
        myThread1.start();
        MyThread2 myThread2=new MyThread2();
        myThread2.start();}
}

程序运行后果:

1823766773
1823766773

此办法退出同步办法 synchronized 关键字失去了雷同实例对象,然而此办法运行效率很低,因为是同步运行的,下一个线程想要获得对象,必须等上一个线程开释锁之后,能力继续执行。

同步代码块

同步办法是对办法的整体加锁,而同步代码块是对实例变量做操作。

批改代码批改 SingletonL.java 后的代码如下:
`

public static  SingletonL getInstance() {synchronized (SingletonL.class){if (null == instance) {instance = new SingletonL();
        }
        return instance;
    }
}

`
其实应用同步代码块操作效率也很低,那咱们如何批改同步代码块的代码来提高效率呢?

针对同步代码做一些解决,代码如下:

public static  SingletonL getInstance() {if (instance==null){synchronized (SingletonL.class){if (null == instance) {instance = new SingletonL();
                }
                return instance;
            }
        }
      return instance;
    }

这样解决是因为当第一个线程调用办法创建对象,其余线程调用办法的时候发现该实例变量存在,就会间接调用而不会须要做期待的操作。

2、饿汉式模式

该模式的特点在类加载的时候就创建对象,在调用 getInstance()是对象曾经存在。

/**
 * 饿汉式单利模式(线程平安,调用效率高,然而,不能够延时加载)*/
public class SingletonE implements Serializable {
    // 避免被援用
    private static SingletonE instance = new SingletonE();

    // 避免被实例化
    private SingletonE() {}

    public static SingletonE getInstance() {return instance;}
}

阐明饿汉式单例模式代码中,static 变量会在类装载时初始化,此时也不会波及多个线程对象拜访该对象的问题。虚拟机保障只会装载一次该类,必定不会产生并发拜访的问题。

3、动态外部类

该模式的特点是通过一个动态外部类创立实例对象,而后调用 getInstance()实现单利,是线程平安的

/**
 * 动态外部类单利模式(线程平安,调用效率高。然而,能够延时加载)*/
public class SingletonJt {
    private static class staticFactory {private  static   final  SingletonJt instance = new SingletonJt();
    }

    // 避免被实例化
    private SingletonJt() {}

    public static SingletonJt getInstance() {return staticFactory.instance;}
}

阐明外部类没有 static 属性,则不会像饿汉式那样立刻加载对象,只有真正调用 getInstance(), 才会加载动态外部类。加载类时是线程 平安的。instance 是 static final 类型,保障了内存中只有这样一个实例存在,而且只能被赋值一次,从而保障了线程安全性。

4、枚举单利

/**
 * 枚举单利模式(线程平安,调用效率高,不能延时加载)*/
public enum  Singletonenum {INSTANCE;}

public class Client {public static void main(String[] args) {
        Singletonenum singletonenum=Singletonenum.INSTANCE;
        Singletonenum singletonenum1=Singletonenum.INSTANCE;
        System.out.println(singletonenum==singletonenum1);
    }
}

阐明枚举自身就是单利模式,不能够实现延时加载。

四、单例模式的效率平安问题

两种形式实现懒汉模式的平安问题:
1、反射
2、序列化和反序列化

通过反射中的 setAccessible(true)办法, 破解懒汉式单利模式,具体代码如下:

/**
     * 测试懒汉式单利模式(反射)*/
    public static void testSingleLFs() throws Exception {Class c= Class.forName("com.designpattern.pattern.singletonpattern.SingletonL");
       Constructor constructor=c.getDeclaredConstructor(null);
       constructor.setAccessible(true);
       SingletonL singletonL1=(SingletonL) constructor.newInstance();
       SingletonL singletonL11=(SingletonL) constructor.newInstance();
        System.out.println(singletonL1==singletonL11); // 返回为 false
    }

那咱们如何防止这个问题呢? 只须要在在单利模式中退出这段代码即可:即在公有结构器中加一个判断规定即可实现,System.out.println(singletonL1==singletonL11); // 返回为 true

private SingletonL() {  // 防止类在内部被实例化
        if (null==instance){throw new RuntimeException("实例曾经存在");
        }
    }

通过序列化和反序列化破解单利模式,思路是先把实例的对象写到一个文件当中,而后在读到程序当中,具体代码如下:

/**
     * 测试懒汉式单利模式(序列化和反序列化)* @param singletonL
     * @throws Exception
     */
    public static void testSingleLxL(SingletonL singletonL) throws Exception {ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("D:/a.txt"));
        oos.writeObject(singletonL);
        oos.close();
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("D:/a.txt"));
        System.out.println((SingletonL)objectInputStream.readObject());

    }

防止这个问题的形式是在懒汉式单利模式中退出一段代码即可:

// 反序列化时,如果定义了 readResolve()则间接返回此办法指定的对象。而不须要独自再创立新对象!private Object readResolve() throws ObjectStreamException {return instance;}

单利模式实现形式的效率问题,阐明几点:咱们须要借助这个类

CountDownLatch – 同步辅助类,在实现一组正在其余线程中执行的操作之前,它容许一 个或多个线程始终期待。
• countDown() 以后线程调此办法,则计数减一
• await(),调用此办法会始终阻塞以后线程,直到计时器的值为 0

具体代码如下:

/**
 * 测试单利模式的效率
 */
public class ClientTest {public static void main(String[] args) throws InterruptedException {long start = System.currentTimeMillis();
        int count = 10;
        final CountDownLatch countDownLatch=new CountDownLatch(count);
        for (int j = 0; j < count; j++) {  // 多线程环境下
            new Thread(new Runnable() {public void run() {for (int i = 0; i < 10000; i++) {
                        // 测试那种实现形式批改这里即可
                        SingletonL singletonL = SingletonL.getInstance(); // 测试懒汉式}
                    countDownLatch.countDown();}
            }).start();}
        countDownLatch.await();    //main 线程阻塞,直到计数器变为 0,才会持续往下执行!long end = System.currentTimeMillis();
        System.out.println("总耗时:"+(end-start));
    }
}

五、小结

常见的单利模式实现形式
次要:
饿汉式:(线程平安,调用效率高,然而,不能够实现延时加载)
懒汉式:(线程平安,调用效率不高,然而,能够实现延时加载)
动态外部类式:(线程平安,调用效率高,然而,能够延时加载)
枚举式:(线程平安,调用效率高,然而,不能够实现延时加载)
选用形式:
单利对象 占用资源少,不须要延时 枚举好于饿汉式
单利对象 占用资源大须要延时加载,动态外部类好于懒汉式

看到这里明天的分享就完结了,如果感觉这篇文章还不错,来个 分享、点赞、在看 三连吧,让更多的人也看到~

欢送关注集体公众号 「JavaClub」,定期为你分享一些技术干货。

退出移动版