多线程案例(一):单例模式

1 单例模式背景概念

场景: 代码中有些概念,不应该存在多个实例,此时便可以使用单例模式来解决,如DataSource这样的类,就应该是在一个程序中只有一个实例,不应该实例化多个DataSource对象.
单例模式是一种比较常见的"设计模式",以后在工作中会接触一些服务器,服务器里就需要依赖一些数据(可能是数据库中,也可能是在内存中),如果是在内存中的话,往往需要一个"数据管理器"实例来管理这些数据,像这种"数据管理器"对象也应该是单例的.

2 实现方式

2.1 饿汉模式

 饿汉模式的代码实现如下:

public class ThreadDemo {
    static class Singleton {
        private Singleton() { }
        //私有构造方法,要求外部无法new这个类的实例
        private static Singleton instance = new Singleton();
        public static Singleton getInstance() {
            return instance;
        }
    }
    public static void main(String[] args) {
        //getInstance就是获取该类实例的唯一方式,不应该使用其他方式来创建实例
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

== 代码解读==:

  • 先创建一个表示单例的类"Singleton",要求这个类只能有一个实例;
  • 饿汉模式的"饿"指的是只要类被加载,实例就会立即创建,也就是说实例创建的时机比较早,后续无论怎么操作,只要严格使用getInstance,就不会出现其他实例;
  • 把构造方法变成私有,此时在该类外部就无法new在这个类的实例;
  • static成员表示Singleton类的唯一实例;
  • static和类相关,和实例无关,类在内存中只有一份,static成员也就只有一份.

那么饿汉模式是否是线程安全的呢?

导致线程不安全的情况如下:

  • 线程的调度是抢占式执行;
  • 修改操作不是原子的;
  • 多线程同时修改同一个变量;
  • 内存可见性;
  • 指令重排序.

对于饿汉模式来说,多线程同时调用getInstance,由于getInstance只做了一件事:读取 instance实例的地址,多个线程在同时读同一个变量,根据上述的情况,不符合多线程同时修改同一个变量,因此饿汉模式是线程安全的!

2.2 懒汉模式

 类加载的时候,没有立刻实例化,第一次调用getInstance的时候,才真的实例化(“延时加载”);如果代码一整场都没有调用getInstance,此时实例化的过程就被省略了;一般认为,懒汉模式比饿汉模式效率更高,懒汉模式有很大的可能是"实例用不到",此时就节省了实例化的开销.饿汉模式和懒汉模式的区别可以用以下的例子来说明:

各种编译器,有两种主要的打开方式
1.记事本–>饿汉模式:此时会尝试把整个文件的内容都读取到内存中,然后再展示给用户;
2.vscode–>懒汉模式:只会把当前这一个屏幕内的内容(以及周围的一小点内容)加载到内存中,随着翻页,会继续加载内存.

2.2.1 单线程版本

代码如下:

public class ThreadDemo {
    //Singleton 类被加载的时候,不会立即实例化,
    //等到第一次使用这个实例的时候再实例化.
    static class Singleton {
        private Singleton() {}

        private static Singleton instance = null;
        public static Singleton getInstance() {
            if (instance == null) {
                 instance = new Singleton();
            }
            return instance;
        }
    }
}

代码解读:
对于懒汉模式来说,多线程同时调用getInstance,做了四件事:

  • 读取instance的内容;
  • 判断instance是否为null;
  • 如果为空,就new一个实例;
  • 返回实例的地址.
    ***当new一个实例的时候,就会修改instance的值,因此懒汉模式是线程不安全的,***只有在实例化之前调用,存在线程安全问题,如果要是已经把实例创建好了,后面再去并发调用getInstance就是线程安全的.具体流程如下所示:
    在这里插入图片描述

2.2.2 多线程版本–1

以下有两种实现方式:
方式一:

public class ThreadDemo {
    static class Singleton {
        private Singleton() { }
        private static Singleton instance = null;

        public static Singleton getInstance() {
            //这样加锁是可以保证原子性的
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
            return instance;
        }
    }
}

方式二:

public class ThreadDemo {
    static class Singleton {
        private Singleton() { }
        private static Singleton instance = null;

        synchronized public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
}

代码解读:

  • 方式一和方式二的区别是return是在锁内部完成的还是在锁外部完成的,因为return只是进行读操作,因此放在内部还是外部都不会影响结果;
  • 虽然两种方式都可以,但是方式一的锁粒度(锁中包含的代码越多,就认为粒度越大)比较小,锁粒度越大,说明这段代码的并发能力越受限.
    方式一的时间线如下:
    在这里插入图片描述

2.2.3 多线程版本–2

 对于懒汉模式来说,当实例被创建之前,存在线程不安全问题,但是一旦实例创建好之后,此时就不再涉及线程安全问题,在上面的代码中,哪怕是实例已经创建好了,每次调用getInstance还是会涉及加锁解锁,而这里的加锁解锁其实已经不必要了,只要代码中涉及到锁,基本上就和高性能无缘了!!!因此,只有在实例化之前调用的时候加锁,后面就不用加锁,实现方式如下:

public class ThreadDemo {
    static class Singleton {
        private Singleton() { }
        private static Singleton instance = null;

        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}

此时的时间线如图所示:
在这里插入图片描述
在这里插入图片描述

截止目前,此代码还有一个致命的问题:当线程二两次进行读instance的时候,或者进行多次读操作时,可能会被编译器优化,也就是说可能只有第一次读才是从内存中读,后续的读操作就直接从CPU中读取寄存器(和上次结果一致),这样的结果就是线程一修改后,线程二没有读到最新的值,毕竟多线程在调用getInstance时,先加锁的线程在修改,后加锁的线程在读取,这是内存可见性问题.
为了解决上述问题,可以稍作一下变动:
在这里插入图片描述
volatile的作用:

  • 保持内存可见性;
  • 相当于强行禁止编译器对读操作的优化,虽然牺牲了性能,但是保证了结果的正确性;

3 总结

为了保证线程安全,涉及到三个要点:

  • 加锁->保证线程安全;
  • 双重if->保证效率;
  • volatile->避免内存可见性引来的问题.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值