设计模式-单例模式(二)

2018-05-22  本文已影响0人  巨子联盟

单例模式,目标就是在JVM内创建线程安全的单个实例.用途很多,比如加载资源配置文件.
在Java中实现单例的方法有很多种.有些是线程安全有不是.线程安全的实现方式有:

  1. 双重检查机制的懒汉式单例
  2. 静态内部类实现的单例
  3. 静态代码块实现的单例
  4. 使用枚举类实现的单例
    其实还有一种有一个线程安全的Map登记实现的单例

下面线程安全和不安全的都分析下:

package com.byedbl.singleton.unsafe.method1;

/**
 * 饿汉模式
 * 这种测得为啥一直是线程安全的??
 *
 * @author : zengzhijun
 * @date : 2018/5/18 17:56
 **/
public class MyObject {

    // 立即加载方式==饿汉模式
    private static MyObject myObject = new MyObject();

    private MyObject() {
    }

    public static MyObject getInstance()  {
        // 此代码版本为立即加载
        // 此版本代码的缺点是不能有其它实例变量
        // 因为getInstance()方法没有同步
        // 所以有可能出现非线程安全问题
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

}

这种方法据说是不安全,但是实际测试的时候每次都是一个实例.具体测得代码如下:
先创建一个线程类

package com.byedbl.singleton.unsafe.method1;


public class MyThread extends Thread {

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

}

再用多个线程一起去跑

package com.byedbl.singleton.unsafe.method1;


import java.util.concurrent.CountDownLatch;

public class Run {

    public static void main(String[] args) {

        int len = 1000;
        CountDownLatch latch = new CountDownLatch(len);
        MyThread[] threads = new MyThread[len];
        for(int i =0 ;i< len;i++) {
            threads[i] = new MyThread();
            latch.countDown();
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for(int i= 0;i<len;i++) {
            threads[i].start();
        }

    }

}

结果都是只有一个实例出来,从类的加载流程来看,private static MyObject myObject = new MyObject(); 这句代码是只执行一次的.我们看JDK源码里java.lang.Runtime这个类的实现也是这种方式.
真正的缺点是不能有实例变量吧.

package com.byedbl.singleton.unsafe.method2;

/**
 * 懒汉式,没加锁,不安全,加锁性能也低
 * @author : zengzhijun
 * @date : 2018/5/18 17:55
 **/
public class MyObject {

    private static MyObject myObject;

    private MyObject() {
    }

    public static MyObject getInstance() {
        try {
            if (myObject != null) {
            } else {
                // 模拟在创建对象之前做一些准备性的工作
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

}

这个是真的不安全,在没有用CountDownLatch的时候就可以测试出来,代码如下:

package com.byedbl.singleton.unsafe.method2;


public class Run {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }

}

MyThread 和上面方法1一个套路.
结果如下:

1770731361
903341402
186668687

打印出3个实例


package com.byedbl.singleton.safe.normal.method3;

/**
 * 这个虽然线程安全,但是效率太低了
 * @author : zengzhijun
 * @date : 2018/5/18 19:00
 **/
public class MyObject {

    private static MyObject myObject;

    private MyObject() {
    }

    // 设置同步方法效率太低了
    // 整个方法被上锁
    synchronized public static MyObject getInstance() {
        try {
            if (myObject != null) {
            } else {
                // 模拟在创建对象之前做一些准备性的工作
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

}

这个直接在整个方法加synchronized效率比较低,不推荐.

package com.byedbl.singleton.safe.normal.method4;

/**
 * 这个虽然线程安全,但是效率太低了
 * @author : zengzhijun
 * @date : 2018/5/18 19:00
 **/
public class MyObject {

    private static MyObject myObject;

    private MyObject() {
    }

    public static MyObject getInstance() {
        try {
            // 此种写法等同于:
            // synchronized public static MyObject getInstance()
            // 的写法,效率一样很低,全部代码被上锁
            synchronized (MyObject.class) {
                if (myObject != null) {
                } else {
                    // 模拟在创建对象之前做一些准备性的工作
                    Thread.sleep(3000);

                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

}

虽然这个不是在方法上加锁,但是将整个代码块锁起来了,效率还是一样的低.不推荐.

package com.byedbl.singleton.unsafe.method5;

public class MyObject {

    private static MyObject myObject;

    private MyObject() {
    }

    public static MyObject getInstance() {
        try {
            if (myObject != null) {
            } else {
                // 模拟在创建对象之前做一些准备性的工作
                Thread.sleep(3000);
                // 使用synchronized (MyObject.class)
                // 虽然部分代码被上锁
                // 但还是有非线程安全问题
                synchronized (MyObject.class) {
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

}


那我们能不能少加一行代码的锁呢?答案是否定的,这样就线程不安全了.而要解决这个问题,就得用下面这种比较推荐的,双重检查机制的懒汉式

package com.byedbl.singleton.safe.suggest.method6;

/**
 * 线程安全之双检测机制
 * DCL 双检查锁机制
 * @author : zengzhijun
 * @date : 2018/5/18 19:08
 **/
public class MyObject {

    //要声明为 volatile
    private volatile static MyObject myObject;

    private MyObject() {
    }

    // 使用双检测机制来解决问题
    // 即保证了不需要同步代码的异步
    // 又保证了单例的效果
    public static MyObject getInstance() {
        try {
            if (myObject != null) {
            } else {
                // 模拟在创建对象之前做一些准备性的工作
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    if (myObject == null) {
                        myObject = new MyObject();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
    // 此版本的代码称为:
    // 双重检查Double-Check Locking

}


这个代码我们用多线程去跑也是没问题的.

package com.byedbl.singleton.safe.suggest.method6;


import java.util.concurrent.CountDownLatch;

public class Run {

    public static void main(String[] args) {
        int len = 100;
        CountDownLatch latch = new CountDownLatch(len);
        MyThread[] threads = new MyThread[len];
        for(int i =0 ;i< len;i++) {
            threads[i] = new MyThread();
            latch.countDown();
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for(int i= 0;i<len;i++) {
            threads[i].start();
        }

    }

}

package com.byedbl.singleton.safe.suggest.method7;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * 线程安全之静态内部类
 *
 *
 * @author : zengzhijun
 * @date : 2018/5/18 19:08
 **/
public class MyObject implements Serializable{

    // 内部类方式
    private static class MyObjectHandler {
        private static MyObject myObject = new MyObject();
    }

    private MyObject() {
    }

    public static MyObject getInstance() {
        return MyObjectHandler.myObject;
    }

    /**
     * 解决在序列化时线程不安全的问题
     **/
    protected Object readResolve() throws ObjectStreamException {
        System.out.println("调用了readResolve方法!");
        return MyObjectHandler.myObject;
    }
}


用这种方法需要注意的是要实现 Serializable接口并覆盖readResolve方法,主要是为了解决持久化在反序列化时得到的是不同的实例问题,可以注释readResolve方法,用下面的代码测试看到效果:

package com.byedbl.singleton.safe.suggest.method7;

import java.io.*;

public class SaveAndRead {

    public static void main(String[] args) {
        try {
            MyObject myObject = MyObject.getInstance();
            FileOutputStream fosRef = new FileOutputStream(new File(
                    "myObjectFile.txt"));
            ObjectOutputStream oosRef = new ObjectOutputStream(fosRef);
            oosRef.writeObject(myObject);
            oosRef.close();
            fosRef.close();
            System.out.println(myObject.hashCode());
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        try {
            FileInputStream fisRef = new FileInputStream(new File(
                    "myObjectFile.txt"));
            ObjectInputStream iosRef = new ObjectInputStream(fisRef);
            MyObject myObject = (MyObject) iosRef.readObject();
            iosRef.close();
            fisRef.close();
            System.out.println(myObject.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

}

package com.byedbl.singleton.safe.suggest.method8;


/**
 * 静态代码块中的代码在使用类的时候就已经执行了,所以可以利用这个特性来实现单例
 * @author : zengzhijun
 * @date : 2018/5/18 19:21
 **/
public class MyObject {

    private static MyObject instance = null;

    private MyObject() {
    }

    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        instance = new MyObject();
    }

    public static MyObject getInstance() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }

}

package com.byedbl.singleton.safe.suggest.method9;

import org.apache.commons.lang3.RandomUtils;

/**
 * 使用枚举类实现单例
 * 终极方法,能用静态枚举类的就用静态枚举类
 * @author : zengzhijun
 * @date : 2018/5/18 19:27
 **/
public class MyObject {

    private enum MyEnumSingleton {
        INSTANCE;
        String arg ;
        private MyObject myObject = null;
        MyEnumSingleton() {
            //这里可以初始化变量,当然也可以在一个 static代码块里面初始化
            arg = "get arg from properties" + RandomUtils.nextInt();
            myObject = new MyObject();
        }


        public MyObject getInstance() {
            return myObject;
        }
    }

    public static MyObject getConnection() {
        return MyEnumSingleton.INSTANCE.getInstance();
    }
    public String getArg() {
        return MyEnumSingleton.INSTANCE.arg;
    }
}

枚举类在JVM里面天然就是单例的.这种方法比较推荐.

上一篇下一篇

猜你喜欢

热点阅读