目录
- [Day 9:多线程(7)](#Day 9:多线程(7))
-
- 多线程代码案例
-
- [1. 案例一:单例模式](#1. 案例一:单例模式)
-
- [1.1 饿汉模式](#1.1 饿汉模式)
- [1.2 懒汉模式](#1.2 懒汉模式)
- [1.3 线程安全问题](#1.3 线程安全问题)
- [1.4 懒汉模式修改](#1.4 懒汉模式修改)
Day 9:多线程(7)
多线程代码案例
1. 案例一:单例模式
单例模式 是一种经典的++设计模式++,面试中非常常见
设计模式 类似于"棋谱",将编程中各种经典的问题场景进行整理 ,并且提供一些解决方案;设计模式其实有很多种,绝对不止23种,随着时代的变化,新的设计模式不断地诞生,旧的模式也就在消亡
简单来说,单例模式就是单个实例
- 整个进程中的某个类,有且只有一个对象,并不会new出多个对象,这样的对象,称为单例
- 但是如何保证这个类只有一个实例呢,靠程序员口头保证肯定是不可行的
- 需要让编译器来帮我们做一个强制的检查,通过一些编码上的技巧,使编译器可以自动发现代码中是否有多个实例,并且在尝试创建多个实例的时候,直接编译出错
代码中的有些对象,本身就不应该是有多个实例的,从业务角度就应该是单个实例
-
比如,写的服务器,要从硬盘上加载100G的数据到内存中(加载到若干个哈希表里)
肯定要写一个类,封装上述加载操作,并且写一些获取/处理数据的业务逻辑
这样的类,就应该是单例的,一个实例,就管理100G的内存数据,搞多个实例就是N*100G的内存数据,机器吃不消也没必要
-
再比如,服务器也可能涉及到一些"配置项"(MySQL有配置文件)
代码中也需要有专门的类,管理配置,需要加载配置数据到内存中供其他代码使用
这样的类的实例也应该是单例的,如果是多个实例,就存储了多份数据,如果一样还可以接受,如果不一样,以哪一个为准?
根本上保证对象是唯一实例 ,这样的代码,就称为单例模式,单例模式有很多不同的写法,下面主要介绍两种:饿汉模式与懒汉模式
1.1 饿汉模式
java
package thread;
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){
}
}
public class Demo27 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1==s2);
}
}
private static Singleton instance = new Singleton();
- static成员初始化时机是在类加载的时候,此处可以简单地认为,JVM已启动,就立即加载(具体情况可能有变数)
- static修饰的,其实是**"类属性"**,就是在"类对象"上的,每个类的类对象在JVM中只有一份
- 此时的Singleton类只存在一个实例instance,初始化的时候只执行一次
getInstance()
:此处后续需要使用这个类的实例,就可以通过getInstance()
来获取已经new好的这个实例,而不是重新实例化private Singleton(){ }
:- 这样的private构造方法可以防止其他代码重新实例化这个类
- 类之外的代码,尝试实例化的时候,就必须调用构造方法,由于构造方法是私有的,无法调用,就会编译出错
1.2 懒汉模式
懒在计算机中往往是一个褒义词,而且是高效率的代表
懒汉模式不是在程序启动的时候创建实例,而是在第一次使用的时候才去创建
如果不使用了,就会把创建实例的代码节省下来了
如果代码中存在多个单例类
- 使用饿汉模式,就会导致这些实例都是在程序启动的时候扎堆创建的,可能会把程序启动时间拖慢,
- 使用懒汉模式,什么时候首次调用,调用时机是分散的,化整为零,用户不太容易感知到卡顿
java
package thread;
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if (instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {
}
}
public class Demo28 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
if (instance == null) {instance = new SingletonLazy();}
:什么时候调用就什么时候创建,如果不调用,就不创建了
1.3 线程安全问题
上述两种单例模式,是否是线程安全的
考虑有多个线程同时调用getInstance,是否会产生线程安全问题
-
对于饿汉模式
- 创建实例的时机是在Java进程启动,比main调用还早的时机
- 后续代码里创建线程,一定比上述实例创建要更迟
- 后续执行getInstance的时候,意味着上述实例早都已经有了
- 每个线程的getInstance只做了一件事,就是读取上述静态变量的值
- 多个线程读取同一个变量,是线程安全的
-
对于懒汉模式
if (instance == null) {instance = new SingletonLazy();}
:这行代码可以理解为,包含了读和写- 读:查看一下instance变量的值
- if条件判定,拿出来instance里面的引用的地址,看一下是否为null
- 写:修改,赋值就是修改
- 上述代码在多线程环境下就可能产生问题
1.4 懒汉模式修改
上述的懒汉模式造成的多线程不安全问题,本质是因为,如果执行顺序如下:
- 线程t1判断了instance为null后
- 此时,线程t2也进行了判断,同样认为instance为null
- 接下来线程t1开始创建实例
- 由于t2之前判断instance为null,于是也创建实例
- 最后导致第二个对象的地址覆盖了第一个
那么我们通过加锁的方式来进行尝试
java
public static SingletonLazy getInstance(){
if (instance == null){
synchronized (locker){
instance = new SingletonLazy();
}
}
return instance;
}
但是上述代码仍然存在问题:
- 线程t1判断了instance为null后
- 此时,线程t2也进行了判断,同样认为instance为null
- 接下来线程t1进行加锁,之后,线程t1开始创建实例,并释放锁
- t2拿到锁之后,由于t2之前判断instance为null,于是也创建实例
- 最后同样导致第二个对象的地址覆盖了第一个
于是应该把if和new打包成一个原子操作
java
public static SingletonLazy getInstance(){
synchronized (locker){
if (instance == null){
instance = new SingletonLazy();
}
}
return instance;
}
同时,上述代码还有修改的空间,懒汉模式只是在最开始调用getInstance会存在线程安全问题,一旦把实例创建好了,后续再调用,就只是读操作了,就不存在线程安全问题了,针对后续调用,明明没有线程安全问题,还要加锁,就是画蛇添足(加锁本身,也是有开销的,可能会使线程阻塞)
代码如下:
java
package thread;
class SingletonLazy {
private static volatile SingletonLazy instance = null;
private static Object locker = new Object();
public static SingletonLazy getInstance(){
if (instance == null){
synchronized (locker){
if (instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
public class Demo28 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
- 第一个if用于判定是否要加锁
- 实例化之后,线程自然就安全了,无需加锁了
- 实例化之前,应该要加锁
- 第二个if用于判断是否要创建对象
- 同时
volatile
也是有必要的,避免触发了优化,避免内存可见性与指令重排序问题