单利模式与多线程

329 阅读7分钟

我是阿福,公众号「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」,定期为你分享一些技术干货。