多线程之单例模式

文章详细介绍了Java编程中的两种单例模式实现:饿汉模式和懒汉模式。饿汉模式在类加载时即创建实例,保证线程安全;懒汉模式则延迟到首次使用时创建,线程不安全,但可以通过synchronized关键字或双重检查锁定实现线程安全。总结强调了并发编程中确保线程安全的重要性。
摘要由CSDN通过智能技术生成


一、什么是单例模式?

      单例模式是属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程(线程)中只有一个实例。
      这种模式涉及一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,并且这个类提供了访问其唯一对象的方法。

二、单例模式的实现

1.饿汉模式

1.1 饿汉模式的定义

      饿汉模式就是在类加载的时候立刻就会实例化对象,并且只会实例化一个对象,后续使用就只会出现一份实例。

1.2 饿汉模式的特点

  • 将构造方法私有化
  • 在类的内部只定义一个 private static 修饰的实例对象
  • 提供一个 getInstance() 获取实例对象的方法
  • 饿汉模式天然就是线程安全的

1.3 饿汉模式的实现

class HungrySingleton {

	// private static 修饰实例对象
    private static HungrySingleton singleton = new HungrySingleton();

    //私有化构造方法
    private HungrySingleton() {

    }

    //提供获取该实例对象的方法
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class HungrySingletonDemo {
    public static void main(String[] args) {
        HungrySingleton singleton1 = HungrySingleton.getInstance();
        HungrySingleton singleton2 = HungrySingleton.getInstance();

        System.out.println(singleton1 == singleton2);
    }
}

结果展示:
在这里插入图片描述
      在饿汉模式实现的代码中,将构造方法私有化,能够保证只有在本类中能够创建对象;通过 private static 修饰实例对象,表示这是一个类对象,被该类的所有对象所共有;提供一个 getInstance() 方法,保证通过 getInstance() 获取到的对象是同一个对象。因此在测试类中,无论调用几次 getInstance(),得到的对象都是同一个对象。

1.4 为什么说饿汉模式天然就是线程安全的?

      首先在饿汉模式中,实例化对象是随着类的加载而加载的,也就是说类加载完成,实例化对象也就完成了。另外,在饿汉模式中,如果多个线程同时调用 getInstance() 获取实例对象,只是会触发多个线程读取实例化对象,并没有对实例化对象进行修改,因此,不会触发引起线程不安全的五个原因中任何一个,所以说饿汉模式天然就是线程安全的。

引起线程不安全的因素

2.懒汉模式

2.1 懒汉模式的定义

      懒汉模式就是指,在类加载的时候不直接实例化对象,而是在第一次使用的时候才会创建实例化对象。

2.2 懒汉模式的特点

  • 构造方法私有化
  • 用 private static 修饰实例对象,但是没有直接初始化
  • 提供 getInstance() 方法获取实例对象,同时实例化对象采用双重校验锁的方式实现
  • 懒汉模式天然不是线程安全的

2.3 线程安全的懒汉模式的实现方式

2.3.1 用 synchronized 修饰获取实例化对象的方法
class SingletonLazy {

    //创建对象时,没有立即初始化
    private static SingletonLazy singletonLazy = null;

    //构造方法私有化
    private SingletonLazy() {

    }

    synchronized public static SingletonLazy getInstance() {
        if (singletonLazy == null) {
            singletonLazy = new SingletonLazy();
        }
        return singletonLazy;
    }
}

public class SingletonLazyDemo {

    public static void main(String[] args) throws InterruptedException {
    
        Thread t1 = new Thread(() -> {
            SingletonLazy singletonLazy1 = SingletonLazy.getInstance();
            System.out.println(singletonLazy1);
        });

        Thread t2 = new Thread(() -> {
            SingletonLazy singletonLazy2 = SingletonLazy.getInstance();
            System.out.println(singletonLazy2);
        });

        Thread t3 = new Thread(() -> {
            SingletonLazy singletonLazy3 = SingletonLazy.getInstance();
            System.out.println(singletonLazy3);
        });

        t1.start();
        t2.start();
        t3.start();

    }
}

结果展示:
在这里插入图片描述

      通过 synchronized 修饰获取实例的方法,可以保证每次只有一个线程可以可以调用获取实例化对象的方法,并且只会在第一次调用该方法时完成实例化对象的创建和初始化。

2.3.2 用 volatile 修饰实例化对象,并采用双重锁机制创建实例化对象
class SingletonLazy {

    //创建对象时,没有立即初始化
    volatile private static SingletonLazy singletonLazy = null;

    //构造方法私有化
    private SingletonLazy() {

    }

    //采用双重校验锁机制,提供获取实例对象的方法
    public static SingletonLazy getInstance() {
        //判断是否需要加锁
        if (singletonLazy == null) {
            synchronized (SingletonLazy.class) {
                //判断是否需要创建对象
                if (singletonLazy == null) {
                    singletonLazy = new SingletonLazy();
                }
            }
        }
        return singletonLazy;
    }
}

public class SingletonLazyDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            SingletonLazy singletonLazy1 = SingletonLazy.getInstance();
            System.out.println(singletonLazy1);
        });

        Thread t2 = new Thread(() -> {
            SingletonLazy singletonLazy2 = SingletonLazy.getInstance();
            System.out.println(singletonLazy2);
        });

        Thread t3 = new Thread(() -> {
            SingletonLazy singletonLazy3 = SingletonLazy.getInstance();
            System.out.println(singletonLazy3);
        });

        t1.start();
        t2.start();
        t3.start();

    }
}

结果展示:
在这里插入图片描述

  • 第一个 if 的作用,是判断是否需要加锁。主要是用于优化代码的执行效率,如果没有这个 if,那么当多个线程执行到这里时,都会阻塞等待,影响程序的执行效率;
  • 第二个 if 的作用是判断是否需要实例化对象,主要作用是优化代码的执行效率。
  • 虽然加锁确保同一时刻只有一个线程能够调用实例化方法,但是并不能保证线程安全,因为存在内存可见性和指令重排序的问题,会引起线程安全问题,要解决这个问题,就需要使用 volatile 修饰实例化对象。这样就可以保证该方法实现的懒汉模式是线程安全的。

总结

      并发编程是一个复杂的过程,要想在实际操作中保证线程的安全,就要搞清楚导致线程安全问题的原因。

点击这里了解引起线程不安全的原因

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值