首页 技术 正文
技术 2022年11月12日
0 收藏 640 点赞 3,173 浏览 8760 个字

本文只需要考虑一件事:如何使单例模式遇到多线程是安全的、正确的

1.立即加载 / "饿汉模式"

什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接 new 实例化。

public class MyObject {    private static MyObject myObject = new MyObject();    public MyObject(){    }    public static MyObject getInstance(){        return myObject;    }    public static void main(String[] args) {        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();    }}

打印结果:

985396398985396398985396398

控制台打印的 hashCode 是同一个值,说明对象是同一个,也就实现了立即加载型单例设计模式。

此版本的缺点是不能有其他其他实例变量,因为getInstance()方法没有同步,所以有可能出现非线程安全问题。

2.延迟加载 / "懒汉模式"

什么是延迟加载?延迟加载就是在调用 get() 方法时实例才被创建,常见的实现方法就是在 get() 方法中进行 new() 实例化。

测试代码:

public class MyObject {    private static MyObject myObject;    public MyObject() {    }    public static MyObject getInstance() {        try {            if (myObject == null) {                //模拟对象在创建之前做的一些准备工作                Thread.sleep(3000);                myObject = new MyObject();            }        } catch (InterruptedException e) {            e.printStackTrace();        }        return myObject;    }    public static void main(String[] args) {        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();    }}

打印结果:

98539639861002518621895028

从运行结果来看,创建了三个对象,并不是真正的单例模式。原因显而易见,3个线程同时进入了if (myObject == null) 判断语句中,最后各自都创建了对象。

3.延迟加载解决方案

3.1 声明synchronized关键字

既然多个线程可以同时进入getInstance() 方法,那么只需要对其进行同步synchronized处理即可。

public class MyObject {    private static MyObject myObject;    public MyObject() {    }    synchronized public static MyObject getInstance() {        try {            if (myObject == null) {                //模拟对象在创建之前做的一些准备工作                Thread.sleep(3000);                myObject = new MyObject();            }        } catch (InterruptedException e) {            e.printStackTrace();        }        return myObject;    }    public static void main(String[] args) {        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();    }}

打印结果:

961745937961745937961745937

虽然运行结果表明,成功实现了单例,但这种给整个方法上锁的解决方法效率太低。

3.2 尝试同步 synchronized 代码块

同步方法是对方法的整体加锁,这对运行效率来讲很不利的。改成同步代码块后:

public class MyObject {    private static MyObject myObject;    public MyObject() {    }    public static MyObject getInstance() {        try {            synchronized (MyObject.class) {                if (myObject == null) {                    //模拟对象在创建之前做的一些准备工作                    Thread.sleep(3000);                    myObject = new MyObject();                }            }        } catch (InterruptedException e) {            e.printStackTrace();        }        return myObject;    }    public static void main(String[] args) {        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();    }}

打印结果:

355159803355159803355159803

运行结果虽然表明是正确的,但同步synchronized语句块依旧把整个 getInstance()方法代码包括在内,和synchronize 同步方法效率是一样低下。

3.3 针对某些重要的代码进行单独同步

所以,我们可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅度提升。

public class MyObject {    private static MyObject myObject;    public MyObject() {    }    public static MyObject getInstance() {        try {            if (myObject == null) {                //模拟对象在创建之前做的一些准备工作                Thread.sleep(3000);                synchronized (MyObject.class) {                    myObject = new MyObject();                }            }        } catch (InterruptedException e) {            e.printStackTrace();        }        return myObject;    }    public static void main(String[] args) {        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();    }}

运行结果:

98539639821895028610025186

此方法只对实例化对象的关键代码进行同步,从语句的结构上来说,运行的效率的确得到的提升。但是在多线程的情况下依旧无法解决得到一个单例对象的结果。

3.4 使用DCL双检查锁机制

在最后的步骤中,使用DCL双检查锁机制来实现多线程环境中的延迟加载单例设计模式。

public class MyObject {    private volatile static MyObject myObject;    public MyObject() {    }    public static MyObject getInstance() {        try {            if (myObject == null) {                //模拟对象在创建之前做的一些准备工作                Thread.sleep(3000);                synchronized (MyObject.class) {                    if (myObject == null) {                        myObject = new MyObject();                    }                }            }        } catch (InterruptedException e) {            e.printStackTrace();        }        return myObject;    }    public static void main(String[] args) {        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();    }}

运行结果:

860826410860826410860826410

使用DCL双重检查锁功能,成功地解决了“懒汉模式”遇到多线程的问题。DCL也是大多数多线程结合单例模式使用的解决方案。

4.使用静态内置类实现单例模式

DCL可以解决多线程单例模式的非线程安全问题。当然,还有许多其它的方法也能达到同样的效果。

public class MyObject {    public static class  MyObjectHandle{        private static MyObject myObject = new MyObject();        public static MyObject getInstance() {            return myObject;        }    }    public static MyObject getInstance(){        return MyObjectHandle.getInstance();    }    public static void main(String[] args) {        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();    }}

打印结果:

103505773910350577391035057739

静态内置类可以达到线程安全问题,但如果遇到序列化对象时,使用默认的方式运行得到的结果还是多例的。

解决方法就是在反序列化中使用readResolve()方法:

public class MyObject implements Serializable {    //静态内部类    public static class  MyObjectHandle{        private static final MyObject myObject = new MyObject();    }    public static MyObject getInstance(){        return MyObjectHandle.myObject;    }    protected Object readResolve(){        System.out.println("调用了readResolve方法");        return MyObjectHandle.myObject;    }    public static void main(String[] args) throws IOException, ClassNotFoundException {        MyObject myObject = MyObject.getInstance();        FileOutputStream outputStream = new FileOutputStream(new File("myObject.txt"));        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);        objectOutputStream.writeObject(myObject);        objectOutputStream.close();        System.out.println(myObject.hashCode());        FileInputStream inputStream =  new FileInputStream(new File("myObject.txt"));        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);        MyObject object = (MyObject) objectInputStream.readObject();        objectInputStream.close();        System.out.println(object.hashCode());    }}

运行结果:

621009875调用了readResolve方法621009875

5.使用static代码块实现单例模式

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

public class MyObject {    private static MyObject myObject = null;    static {        myObject = new MyObject();    }    public static MyObject getInstance(){        return myObject;    }    public static void main(String[] args){        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();    }}

运行结果:

355159803355159803355159803

6.使用enum枚举数据类型实现单例模式

枚举enum 和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,也可以应用其这个特性实现单例设计模式。

public enum Singleton {    INSTANCE;    private MyObject myObject = null;    Singleton() {        myObject = new MyObject();    }    public MyObject getInstance(){        return myObject;    }    public static void main(String[] args){        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(Singleton.INSTANCE.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(Singleton.INSTANCE.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(Singleton.INSTANCE.getInstance().hashCode());            }        }).start();    }}

运行结果:

151613398715161339871516133987

这样实现的一个弊端就是违反了“职责单一原则”,完善后的代码如下:

public class MyObject {    public enum Singleton {        INSTANCE;        private MyObject myObject = null;        Singleton() {            myObject = new MyObject();        }        public MyObject getInstance() {            return myObject;        }    }    public static MyObject getInstance(){        return Singleton.INSTANCE.getInstance();    }    public static void main(String[] args){        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                System.out.println(MyObject.getInstance().hashCode());            }        }).start();    }}

运行结果:

610025186610025186610025186

7.文末总结

本文使用若干案例来阐述单例模式与多线程结合遇到的情况与解决方案。

参看

《Java多线程编程核心技术》高洪岩著

扩展

Java多线程编程核心技术(一)Java多线程技能

Java多线程编程核心技术(二)对象及变量的并发访问

Java多线程编程核心技术(三)多线程通信

Java多线程核心技术(四)Lock的使用

Java多线程核心技术(六)线程组与线程异常

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,494
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,907
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,740
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,495
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:8,133
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:5,297