文章目录
模式定义:单例模式,就是只有一个实例,并且这个类会自己负责创建自己的对象,并且还会提供了一种访问其唯一对象的方式。保证一个类只有一个实例,并且提供一个全局访问点访问这个实例。比如线程池的场景:就比较适合单例模式
1. 懒汉式:实例在需要用的时候再去创建
看下面的代码
class Car{
private static Car instance; //私有静态属性
private Car(){ } //私有构造函数
//提供一个全局访问点
public static Car getInstance(){
if(instance == null){
instance = new Car();
}
return instance;
}
}
public class Demo {
public static void main(String[] args) {
//一个线程
Car i = Car.getInstance();
Car i2 =Car.getInstance();
System.out.println(i);//只有一个实例
System.out.println(i2);//只有一个实例
}
}
结果显示,是只有一个实例。
但是如果是在多线程中跑这段代码,就会出现问题:
class Car{
private static Car instance; //私有静态属性
private Car(){ } //私有构造函数
//提供一个全局访问点
public static Car getInstance(){
if(instance == null){
instance = new Car();
}
return instance;
}
}
public class Demo {
public static void main(String[] args) {
//多个线程访问,就不是同一个实例了
new Thread(()->{
Car i = Car.getInstance();
System.out.println(i);
}).start();
new Thread(()->{
Car i = Car.getInstance();
System.out.println(i);
}).start();
}
}
结果:
为了防止在多线程中的问题,解决方法是加锁,如下:
class Car{
private static Car instance; //私有静态属性
private Car(){ } //私有构造函数
//全局访问点
public static Car getInstance(){
if(instance == null){
//1.解决方法是加锁
synchronized (Car.class){
//2. 这里再次做判断的原因是如果有多个线程同时进入外部的 if(instance == null) ,其中一个线程抢到了锁然后 开始 new Car 对象然后返回,
//另一个线程因为已经进入内部所以还是会继续进行 new Car 的过程;所以为了防止创建两个对象,需要再加一层判断。
//为什么不直接在 if(instance == null) 外部加锁? 因为我们的代码只需要对new Car对象 的过程加锁,
//如果线程进来的时候,内存中已经存在了Car对象,那么就不存在需要new car 对象,也就不需要加锁,直接返回已有对象就可以;而且加锁是很消耗资源的事情,所以先进行判断再进行加锁
if(instance==null){
instance = new Car();
}
}
}
return instance;
}
}
这样就可以确保单例模式在多线程环境中不出错吗?换不够哦!看下面这句创建对象的代码:
instance = new Car();
创建一个对象的代码在字节码层是需要三步的,如下:其中第二步与第三步的过程是完全可能颠倒运行的
。
1.创建空间:为Car对象在堆中开辟空间
2.初始化:初始化Car对象
3.引用赋值:为instance赋值Car对象的引用地址
代码如下:假设此时有两个线程在运行下面这段代码:线程一先运行到 instance = new Car(); 这一句,并且在字节码层开始 1.创建空间,2引用赋值,当还没来的及进行3.初始化的时候,线程二进来了,并且判断出此时内存中已经存在Car对象了(因为线程一已经引用赋值了),然后线程二就将还没有进行初始化的对象返回了,就会造成空指针问题。
public static Car getInstance(){
if(instance == null){
synchronized (Car.class){
if(instance==null){
instance = new Car();
}
}
}
return instance;
}
为了防止这个问题,我们的解决方式是:加入关键字 volatile ,如下。它的作用之一就是:禁止指令重排序优化
,有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
private volatile static Car instance;
所以懒汉式单例模式的实现:
1。线程不安全
class Car{
private static Car instance; //私有静态属性
private Car(){ } //私有构造函数
//提供一个全局访问点
public static Car getInstance(){
if(instance == null){
instance = new Car();
}
return instance;
}
}
2. 线程安全(双重锁检测)
class Car{
private volatile static Car instance; //私有静态属性
private Car(){ } //私有构造函数
//全局访问点
public static Car getInstance(){
if(instance == null){
synchronized (Car.class){
if(instance==null){
instance = new Car();
}
}
}
return instance;
}
}
2. 饿汉式:在初始化阶段就进行实例的创建。
代码如下:借助了jvm的类加载机制来保证实例的唯一性,所以不存在线程不安全
class hungrySingleton{
private static hungrySingleton instance = new hungrySingleton();
//私有构造函数,不允许外部实例化
private hungrySingleton(){ }
public static hungrySingleton getInstance(){
return instance;
}
}
类加载过程:
- 加载类的二进制数据到内存中,生成对应的class数据结构
- 连接: 1,验证 2, 准备(给类的静态成员变量赋默认值)3, 解析
- 初始化:
给类的静态变量赋初值
初始化的时候同时赋值,不存在懒汉式的多线程问题
用静态内部类的方式来实现单例模式
class InnerClassSingleton{
//内部类
private static class InnerClassHolder{
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton(){ }
public static InnerClassSingleton getInstance(){
return InnerClassHolder.instance;
}
}
静态内部类实现的单例模式本质是懒加载机制:因为如果不运行getInstance(),就不会创建对象。静态内部类依赖jvm的类加载机制来保证线程安全。
通过反射来创建对象可以攻击单例模式
假设在静态内部类中,使用反射来创建对象,那么对象是否会是同一个呢?看代码:
//内部类实现单例模式
class InnerClassSingleton{
private static class InnerClassHolder{
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton(){ }
public static InnerClassSingleton getInstance(){
return InnerClassHolder.instance;
}
}
测试代码
public class Demo2 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//通过反射来获取对象
Constructor<InnerClassSingleton> Constructor = InnerClassSingleton.class.getDeclaredConstructor();
Constructor.setAccessible(true);//取消private的限制
InnerClassSingleton i1 = Constructor.newInstance();
//正常获取对象
InnerClassSingleton i2 = InnerClassSingleton.getInstance();
System.out.println(i1==i2); //运行结果是 false
}
}
因为运行结果是false,说明通过反射来创建对象是可以对单例模式进行攻击的。如何解决呢?在 饿汉模式或者静态内部类的初始化代码中加入以下代码:(懒汉没办法)
private InnerClassSingleton(){
//饿汉模式或者静态内部类里面加上;懒汉没办法
if(InnerClassHolder.instance!=null){ //如果这个类已经在内存中加载过了
throw new RuntimeException("单例不允许多个实例");
}
}
如何阻止反射来构建对象,从而破坏单例模式呢?用枚举来实现单例模式
如何用枚举来实现单例模式?
//用枚举实现单例模式,也是jvm的类加载机制保证线程安全(单例对象是在枚举类被加载的时候进行初始化的)
public enum EnumSingleton {
INSTANCE;
}
//为什么这短短三行代码就可以实现单例模式?实际上创建枚举,就相当于创建了一个继承java.lang.Enum的类,
//每个枚举类型都可以看作枚举类的实例,而且enum的构造方法是private的,同时每个枚举实例都是static final 类型的,表明只能被实例化一次
测试代码
class Demo4{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingleton i1 = EnumSingleton.INSTANCE; //每一个枚举类型成员都可以看作是 枚举类的实例
EnumSingleton i2 = EnumSingleton.INSTANCE;
System.out.println(i1==i2); //返回为true
//通过反射来获取对象,会报错的。有了enum语法糖,JVM会阻止反射获取枚举类的私有构造方法
Constructor<EnumSingleton> Constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class); //enum的构造函数是有参数的
Constructor.setAccessible(true);//取消private的限制
EnumSingleton i3 = Constructor.newInstance("INSTANCE", 0);
}
}
使用枚举实现的单例模式就可以防止反射构造对象
,但是它并非懒加载
序列化与反序列化会破坏单例模式
看下面的代码,我们对 InnerClassSingleton类 的实例进行序列化与反序列化后,得到的两个实例还是同一个实例吗?
class InnerClassSingleton implements Serializable{
static final long serialVersionUID = 42L;//序列化时serialVersionUID表示类的不同版本间的兼容性。版本号
private static class InnerClassHolder{
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton(){
if(InnerClassHolder.instance!=null){
throw new RuntimeException("单例不允许多个实例");
}
}
public static InnerClassSingleton getInstance(){
return InnerClassHolder.instance;
}
}
测试代码
public class Demo2 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
InnerClassSingleton ins = InnerClassSingleton.getInstance();
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("outFile"));
oos.writeObject(ins);
oos.close();
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("outFile"));
InnerClassSingleton obj = ((InnerClassSingleton) ois.readObject());
System.out.println(ins==obj);
}
}
运行结果是 false,所以说序列化与反序列化会破坏单例模式,怎么处理?实现下图中的方法:
class InnerClassSingleton implements Serializable{
....
//实现这个方法。当JVM从内存中反序列化地"组装"一个新对象时,就会自动调用这个 readResolve方法来返回指定的对象了, 从而保障了单例规则
Object readResolve() throws ObjectStreamException{
return InnerClassHolder.instance; //返回原对象
}
}
值得注意的是:如果是枚举类型实现的单例模式,就不需要这一步操作来保证单例模式了,枚举类型本身就可以保证在反序列化中保持单例模式
。原因:在底层代码中,这两种模式的 反序列化 readObject()方法的实现的方式不同。
举例:单例模式在底层的运用
比如这个类: Runtime
public class Runtime {
private static Runtime currentRuntime = new Runtime(); //初始化的时候实例化
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
//全局访问点
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {} //私有构造方法
完