Java多线程核心技术(五)单例模式与多线程

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

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

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();
    }

}

打印结果:

985396398
985396398
985396398

控制台打印的 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();
    }

}

打印结果:

985396398
610025186
21895028

从运行结果来看,创建了三个对象,并不是真正的单例模式。原因显而易见,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();
    }

}

打印结果:

961745937
961745937
961745937

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

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();
    }

}

打印结果:

355159803
355159803
355159803

运行结果虽然表明是正确的,但同步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();
    }

}

运行结果:

985396398
21895028
610025186

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

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();
    }

}

运行结果:

860826410
860826410
860826410

使用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();
    }
}

打印结果:

1035057739
1035057739
1035057739

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

解决方法就是在反序列化中使用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();
    }

}

运行结果:

355159803
355159803
355159803

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();
    }
}

运行结果:

1516133987
1516133987
1516133987

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

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();
    }

}

运行结果:

610025186
610025186
610025186

7.文末总结

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

嗨,你还在看吗?

文章来源:微信公众号 薛勤的博客

目录
相关文章
|
8月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
409 1
|
8月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
384 1
|
9月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
9月前
|
安全 Java API
Java Web 在线商城项目最新技术实操指南帮助开发者高效完成商城项目开发
本项目基于Spring Boot 3.2与Vue 3构建现代化在线商城,涵盖技术选型、核心功能实现、安全控制与容器化部署,助开发者掌握最新Java Web全栈开发实践。
790 1
|
9月前
|
安全 Cloud Native Java
Java 模块化系统(JPMS)技术详解与实践指南
本文档全面介绍 Java 平台模块系统(JPMS)的核心概念、架构设计和实践应用。作为 Java 9 引入的最重要特性之一,JPMS 为 Java 应用程序提供了强大的模块化支持,解决了长期存在的 JAR 地狱问题,并改善了应用的安全性和可维护性。本文将深入探讨模块声明、模块路径、访问控制、服务绑定等核心机制,帮助开发者构建更加健壮和可维护的 Java 应用。
836 0
Java 数据库 Spring
409 0
|
8月前
|
Java 调度 数据库
Python threading模块:多线程编程的实战指南
本文深入讲解Python多线程编程,涵盖threading模块的核心用法:线程创建、生命周期、同步机制(锁、信号量、条件变量)、线程通信(队列)、守护线程与线程池应用。结合实战案例,如多线程下载器,帮助开发者提升程序并发性能,适用于I/O密集型任务处理。
745 0
|
8月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
314 6
|
9月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
573 16
|
11月前
|
Java API 微服务
为什么虚拟线程将改变Java并发编程?
为什么虚拟线程将改变Java并发编程?
472 83