转载

Java多线程编程笔记10:单例模式

立即加载就是指使用类的时候已经将对象创建完毕,常见的实现方法就是直接new实例化。也就是在调用方法前,实例就被创建了。示例代码如下所示:

class MyObject {
    private static MyObject myObject=new MyObject();
    private MyObject(){}
    public static MyObject getInstance(){
        //如果还有其他代码,存在线程安全问题
        return myObject;
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}
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();
    }
}
复制代码

运行结果如下:

58615885
58615885
58615885
复制代码

可以发现,实现了单例模式,因为多个线程得到的实例的hashCode是一样的。

延迟加载:“懒汉模式”

延迟加载就是在调用getInstance()方法时实例才被创建,常见的方法就是在getInstance()方法中进行new实例化。实现代码如下:

class MyObject {
    private static MyObject myObject;
    private MyObject(){}
    public static MyObject getInstance(){
        if(myObject==null){
            myObject=new MyObject();
        }
        return myObject;
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}

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();
    }
}
复制代码

但是由于在getInstance()中,存在多条语句,因此可能存在线程安全问题。运行结果也显示了这一点:

2041531420
1348345633
1348345633
复制代码

甚至,当getInstance()中,有更多的语句,会出现不同的三个对象,在if(myObject==null)语句块中加入Thread.sleep(3000),运行结果如下所示:

218620763
58615885
712355351
复制代码

解决方案:DCL

如果使用synchronized关键字,对整个getInstance()上锁或者对整个if语句块加锁,会存在效率问题。

最终采用了DCL(Double-Check Locking)双检查锁机制,也是大多数多线程结合单例模式使用的解决方案。第一层主要是为了避免不必要的同步,第二层判断则是为了在null情况下才创建实例。

public class MyObject {
    private static MyObject myObject;

    private MyObject() {
    }

    public static MyObject getInstance() {
        if (myObject == null) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (MyObject.class) {
                if (myObject == null) {
                    myObject = new MyObject();
                }
            }
        }
        return myObject;
    }
}
复制代码

测试结果,得到的是相同的hashcode。

静态内置类

public class MyObject{
    private static class MyObjectHandler{
        private static MyObject myObject=new MyObject();
    }

    private MyObject() {
    }

    public static MyObject getInstance() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return MyObjectHandler.myObject;
    }
} 
复制代码

采用静态内置类的方法,是线程安全的。

使用static代码块

静态代码块的代码再使用类的时候就已经执行了,所以可以应用静态代码块的这个特性来实现单例设计模式。

public class MyObject {
    private static MyObject myObject=null;

    static{myObject=new MyObject();}

    private MyObject() {
    }

    public static MyObject getInstance() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
}
复制代码

使用enum枚举数据类型

使用枚举类时,和静态代码块的特性相似,构造方法会被自动调用。枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。

所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。同时,枚举可解决反序列化会破坏单例的问题。

enum MyObject{
    INSTANCE;
}
复制代码

SimpleDataFormat

SimpleDataFormat使用了单例模式,具有线程安全问题。SimpleDateFormat中的日期格式不是同步的。推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。

解决方案1:需要的时候创建新实例

public class DateUtil {
    
    public static  String formatDate(Date date)throws ParseException{
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }
    
    public static Date parse(String strDate) throws ParseException{
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.parse(strDate);
    }
}

复制代码

在需要用到SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。

解决方案2:同步SimpleDateFormat对象

public class DateSyncUtil {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      
    public static String formatDate(Date date)throws ParseException{
        synchronized(sdf){
            return sdf.format(date);
        }  
    }
    
    public static Date parse(String strDate) throws ParseException{
        synchronized(sdf){
            return sdf.parse(strDate);
        }
    } 
}
复制代码

当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。

解决方案3:使用ThreadLocal

public class ConcurrentDateUtil {

    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }

    public static String format(Date date) {
        return threadLocal.get().format(date);
    }
}
复制代码

使用ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。

原文  https://juejin.im/post/5c122d00e51d4541284cc592
正文到此结束
Loading...