avatar

一条在知识海洋的咸鱼

这个家伙很懒,啥也没有留下😋

  • 首页
  • Linux
  • OCJP
  • Java核心技术卷
  • J2EE相关标准
  • 深入理解Java虚拟机
  • NIO与SOcket编程技术指南
  • Java多线程编程核心技术
  • Redis开发与运维
  • Spring Cloud Alibaba 微服务原理与实践
  • DevOps
  • Docker
  • MySQL必知必会
  • AI自学路线
  • Spring Boot 编程思想(核心篇)
主页 单例模式与多线程
文章

单例模式与多线程

发表于 2021-11-12 更新于 2026-06- 10
作者 Administrator
22~29 分钟 阅读

单利模式是经常使用到的设计模式,例如spring的默认bean为单例。
同时众多框架体系使用的为多线程体系,所以在使用的时候单例和多线程结合起来可能会有一些没有注意到的事情,例如全局变量的控制等。

  • 单例模式问题
    • 立即加载/饿汉模式=>属性覆盖问题
    • 延迟加载/懒汉模式=>违背单例原则问题
  • 单例模式问题解决
    • 懒汉模式问题synchronized解决
    • 懒汉模式问题volatile和DCL机制解决
    • 静态内部类解决单例
  • 序列化与反序列化的单例模式
    • 问题
    • 解决
  • 使用static代码块实现单例
  • 枚举enum单例

单例模式问题

单例核心就是构造方法私有化,使用static类方法返回对象的唯一实例。

立即加载/饿汉模式=>属性覆盖问题

  • 代码
//立即加载,饿汉
class AA{
    private static AA aa=new AA();
    private AA(){
    }
//方法未同步生成多线程情况下的实例对象属性只能为同一个
    public static AA getInstance(){
//也可以根据不同的需求,同一对象生成不同的属性实例,如果该单例有多个不同属性,且对不同线程要求不同的话,变量的属性会被覆盖
        return aa;
    }
    public static void main(String[] args) {
        AA instance = AA.getInstance();
        new Thread(()-> System.out.println(instance.hashCode())).start();
        new Thread(()-> System.out.println(instance.hashCode())).start();
        new Thread(()-> System.out.println(instance.hashCode())).start();
    }
}

image.png

  • 问题
    如果单例对象被多个线程使用,但是变量里面的属性被多个线程操作会出现线程不安全问题。即属性会被覆盖

延迟加载/懒汉模式=>违背单例原则问题

使用懒汉模式可以改变上述的属性覆盖问题,但是多个线程情况下

//延迟加载,懒汉
class AB {
    private static AB ab;
    private AB() {
    }
    public static AB getInstance() {
        if (ab == null) {
       //线程休眠保障有足够的时间判断ab,模拟多个线程同时进入的情况
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ab = new AB();
        }
        return ab;
    }
    public static void main(String[] args) {
        new Thread(() -> System.out.println(AB.getInstance().hashCode())).start();
        new Thread(() -> System.out.println(AB.getInstance().hashCode())).start();
        new Thread(() -> System.out.println(AB.getInstance().hashCode())).start();
    }
}

如下图,如果多个线程同时生成,可能会同时返回多个实例对象,虽然全局属性ab只会指向同一个AB,但是生成了多个对象实例,且可以被其他变量重新引用,违背了单例原则
image.png

单例模式问题解决

懒汉模式问题synchronized解决

  • 使用synchronized锁方法
    加入static getInstance方法,锁上单例class类,但是这会造成线程效率低下
    public  synchronized static AB getInstance() {...}
  • 使用synchronized锁代码块
 public static AB getInstance() {
//synchronized (ab)如果ab=null则会NPE,锁class
        synchronized (AB.class) {
            if (ab == null) {
                //线程休眠保障有足够的时间判断ab,模拟多个线程同时进入的情况
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ab = new AB();
            }
            return ab;
        }

锁上ab对象,但是还是很慢,synchronized锁方法一样

其他地方没法上锁,因为判断ab==null是唯一条件,锁其他地方没有意义

懒汉模式问题volatile和DCL机制解决

通过DCL(双重检查锁),在new对象之前在加锁检查是否有对象已经生成
通过volatile禁止重排序和增加可见性,直接拉取全局变量到线程私有栈中查看比较

//延迟加载,懒汉解决办法
class AC {
    private static AC ac;
    private AC() {
    }
    public static AC getInstance() {
        if (ac == null) {
            //线程休眠保障有足够的时间判断ab,模拟多个线程同时进入的情况
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (AC.class) {
                if (ac == null) {
                    ac = new AC();
                }
            }
        }
        return ac;
    }
    public static void main(String[] args) {
        new Thread(() -> System.out.println(AC.getInstance().hashCode())).start();
        new Thread(() -> System.out.println(AC.getInstance().hashCode())).start();
        new Thread(() -> System.out.println(AC.getInstance().hashCode())).start();
    }
}

内存的地址一样
image.png

静态内部类解决单例

class AD{
    private static class StaticInnerClass{
        private  static AD ad=new AD();
    }
    private AD(){
    }
    private static AD getInstance(){
        return StaticInnerClass.ad;
    }
}

序列化与反序列化的单例模式

有些时候,例如远程调用和数据库存储等需要实现序列化,不然无法转换为ObjectOutputStream

问题

序列化之后再进行反序列化多个时会生成多例

  • static变量不属于实例,是不会被序列化共享的,其值不会被记录(即不太清楚了。。。)
  • transient关键字声明的也不会被实例化
//序列化与反序列化多例
class BA implements Serializable {
    public static class UserInfo {
    }
    private static final long serialVersionUID = -1L;
    public static UserInfo userInfo = new UserInfo();
    private static BA ba = new BA();
    private BA() {
    }
    public static BA getInstance() {
        return ba;
    }
    public static void main(String[] args)  {
        BA ba = BA.getInstance();
        System.out.println("序列化实例ba=" + ba.hashCode() + "userInfo=" + BA.userInfo.hashCode());
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File("BA.txt")))) {
           objectOutputStream.writeObject(ba);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("BA.txt")))) {
            BA ba1 = (BA)objectInputStream.readObject();
            System.out.println("反序列化实例ba=" + ba1.hashCode() + "userInfo=" + BA.userInfo.hashCode());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

userInfo地址相同,因为其属于class
ba地址不同,其生成了新的实例
image.png

解决

ObjectInputStream在进行反序列化的时候,会检查反序列化的类有没有定义readResolve方法()如果有定义则调用里面的方法,不创建新的对象
image.png
所以新增一个readResolve方法返回实例即可

class BB implements Serializable {
    public static class UserInfo {
    }

    private static final long serialVersionUID = -1L;
    public static UserInfo userInfo = new UserInfo();
    private static BB bb;
    static {
        bb=new BB();
    }

    private BB() {
    }

    public static BB getInstance() {
        return bb;
    }
    private Object readResolve(){
        return BB.getInstance();
    }
    public static void main(String[] args) {
        BB bb = BB.getInstance();
        System.out.println("序列化实例bb=" + bb.hashCode() + "userInfo=" + BB.userInfo.hashCode());
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("BB.txt"))) {
            objectOutputStream.writeObject(bb);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("BB.txt"))) {
            BB bb1 = (BB) objectInputStream.readObject();
            System.out.println("反序列化实例bb=" + bb1.hashCode() + "userInfo=" + BB.userInfo.hashCode());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

hashcode相同
image.png

使用static代码块实现单例

在类加载的时候生成一次实例,缺点是多线程情况下依然会有属性共享问题

class AE {
    private static AE ae;
    static {
        ae = new AE();
    }
    private AE() {
    }
    public static AE getInstance() {
        return ae;
    }
}

枚举enum单例

http://jtao.work/archives/di-wu-zhang--mei-ju
枚举是实现单例的最佳选择,将需要的类定义为枚举类即可。

Java多线程编程核心技术
Java并发
许可协议: 
分享

相关文章

11月 13, 2021

线程其他

线程在不同的层次不同的说法与环境,就Java语言层次来说线程状态验证NEW、RUNNABLE、TERMINATED验证BLOCKED、WAITING、TIMED_WAITING线程状态就线程来说不同的层面有不同的说法。底层P(S),V(S)资源操作,编码层次对象锁操作,在Java层面对线程的状态定义

11月 12, 2021

单例模式与多线程

单利模式是经常使用到的设计模式,例如spring的默认bean为单例。同时众多框架体系使用的为多线程体系,所以在使用的时候单例和多线程结合起来可能会有一些没有注意到的事情,例如全局变量的控制等。单例模式的实现立即加载/饿汉模式单例模式的实现立即加载/饿汉模式

11月 12, 2021

定时器TImer

Java的定时/计划功能功能

下一篇

线程其他

上一篇

定时器TImer

最近更新

  • 55 异常兜底:工具失败、非法输出、空检索与超时
  • 54 Agent Memory:短期记忆、用户上下文与摘要
  • 53 Human-in-the-loop:让关键动作暂停、确认与恢复
  • 52 工单 Tool:字段校验、创建结果与失败补偿
  • 51 订单 Tool:Mock、鉴权、状态解析与脱敏

热门标签

java基础 微服务 maven Spring Tomcat DDD Linux Linux基础 SQL基础 数据结构算法

目录

©2026 一条在知识海洋的咸鱼. 保留部分权利。

使用 Halo 主题 Chirpy