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

栏目: 后端 · 发布时间: 5年前

内容简介:立即加载就是指使用类的时候已经将对象创建完毕,常见的实现方法就是直接new实例化。也就是在调用方法前,实例就被创建了。示例代码如下所示:运行结果如下:可以发现,实现了单例模式,因为多个线程得到的实例的hashCode是一样的。

立即加载就是指使用类的时候已经将对象创建完毕,常见的实现方法就是直接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, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

iOS软件开发揭密

iOS软件开发揭密

虞斌 / 电子工业出版社 / 2011-5-1 / 79.00元

本书以严密的体系性提供了iPhone和iPad软件开发从入门到专家的系统性知识,并提供来源于真实项目的可重用商业代码。书中的每个实例都是项目经验的提炼,深入浅出地讲解iPhone和iPad软件开发的核心技术要点,基本涵盖了iOS软件开发在真实商业项目中所需要的所有主题,并将实例介绍的技术深度和超值的实用性结合在一起,成为本书的特色。 随书附赠的光盘中包含了书中大量案例的完整工程源代码,可以让......一起来看看 《iOS软件开发揭密》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具