单例模式
当某个对象全局只需要一个实例时,就可以使用单例模式。
特点:
能够避免对象的重复创建,节约空间并提升效率。
避免由于操作不同实例从而导致逻辑错误。
单例模式总共有两种: 饿汉式和懒汉式
1. 饿汉式
饿汉式是在类加载的时候就会创建一个唯一实例,不管后期是否使用该实例都会创建。
代码
/** * 匿名内部类- 单例模式-饿汉 * */ public static class Hungry{ private static Hungry hungry = new Hungry(); private Hungry() {} public static Hungry getInstance(){ return hungry; } public String getAddress(String address){ return address; } }
测试代码
public static void main(String[] args) { Hungry instance = Hungry.getInstance(); String address = instance.getAddress("这是一个测试的地址"); System.out.println(address); for (int i = 0; i < 100; i++){ System.out.println("对象:" + instance); } }
输出
这里截取了部分。
可以看出,我们把Hungry
的构造方法修饰符修改为private
,使得其他类中无法实例化Hungry
这个类,必须通过getInstance()
这个方法获得唯一的实例。但是饿汉模式有一个弊端,就是即便Hungry
这个类不需要使用,也会在类加载的时候被创建出来,占用一块内存,并增加类初始化的时间。他就像一个修理工修理电器一样,不管有些工具用不用的上,他都会拿出所有的工具,就像一个饿汉,所以称之为饿汉式。
2. 懒汉式
先声明一个空变量,需要用时才初始化
2.1 不加锁
代码
/** * 匿名内部类-懒汉式 */ public static class Slacker{ private static Slacker slacker = null; private Slacker(){} public static Slacker getInstance(){ if (slacker == null){ slacker = new Slacker(); } return slacker; } public String getAddress(String address){ return address; } }
测试代码
public static void main(String[] args) { Thread thread = new Thread(new Runnable() { public void run() { Slacker instance = Slacker.getInstance(); String address = instance.getAddress("懒汉式的地址"); System.out.println(address); for (int i=0; i<100; i++){ System.out.println(instance); } } }); Thread thread1 = new Thread(new Runnable() { public void run() { Slacker instance = Slacker.getInstance(); String address = instance.getAddress("懒汉式的地址"); System.out.println(address); for (int i=0; i<100; i++){ System.out.println(instance); } } }); thread.start(); thread1.start(); }
输出
从上面的结果可以看出,上面的代码不是线程安全的,如果多个线程同时调用getInstance()
方法,就会别实例化很多次,为了线程安全,必须给判空过程加锁。
2.2 加锁
代码
/** * 匿名内部类-懒汉式 */ public static class Slacker{ private static Slacker slacker = null; private Slacker(){} public static Slacker getInstance(){ synchronized (Slacker.class){ if (slacker == null){ slacker = new Slacker(); } } return slacker; } public String getAddress(String address){ return address; } }
这样就会在多个线程调用时,一次最多只有一个线程能够执行非空判断并实例化的操作
测试代码同上
输出
从结果可以看出,实例化的对象都是同一个,但是每次都需要执行synchronized()
同步化方法,这样会严重影响执行效率,所以在同步化之前,再加一层非空判断。
2.3 再加一层非空判断
代码
/** * 匿名内部类-懒汉式 */ public static class Slacker{ private static Slacker slacker = null; private Slacker(){} public static Slacker getInstance(){ if (slacker == null){ synchronized (Slacker.class){ if (slacker == null){ slacker = new Slacker(); } } } return slacker; } public String getAddress(String address){ return address; } }
测试代码同上
输出
所以2.3 是效率较高的懒汉单例模式,被称之为 双检锁方式
除了双检锁方式外,还有一种比较常见的静态内部类方式保证懒汉式单例的线程安全。
2.4 静态内部类方式
代码
/** * 匿名内部类-懒汉式-静态内部类方式保证懒汉式单例的线程安全 */ public static class Slacker1{ public static class SlackerHandler{ public static Slacker1 slacker1 = new Slacker1(); } private Slacker1(){} public static Slacker1 getInstance(){ return SlackerHandler.slacker1; } public String getAddress(String address){ return address; } }
测试代码
public static void main(String[] args) { Thread thread = new Thread(new Runnable() { public void run() { Slacker1 instance = Slacker1.getInstance(); String address = instance.getAddress("静态内部类方式懒汉式的地址"); System.out.println(address); for (int i=0; i<100; i++){ System.out.println(instance); } } }); Thread thread1 = new Thread(new Runnable() { public void run() { Slacker1 instance = Slacker1.getInstance(); String address = instance.getAddress("静态内部类方式懒汉式的地址"); System.out.println(address); for (int i=0; i<100; i++){ System.out.println(instance); } } }); thread.start(); thread1.start(); }
输出结果
虽然我们经常使用这种静态内部类的懒加载方式,但其中的原理不一定每个人都清楚。接下来我们便来分析其原理,搞清楚两个问题:
静态内部类方式是怎么实现懒加载的
Java 类的加载过程包括:加载、验证、准备、解析、初始化。初始化阶段即执行类的
clinit
方法(clinit= class + initialize
),包括为类的静态变量赋初始值和执行静态代码块中的内容。但不会立即加载内部类,内部类会在使用时才加载。,所以当此Slacker1
类加载时,SlackerHandler
并不会被立即加载,所以不会像饿汉式那样占用内存。另外,Java 虚拟机规定,当访问一个类的静态字段时,如果该类尚未初始化,则立即初始化此类。当调用
Slacker1
的getInstance
方法时,由于其使用了SlackerHandler
的静态变量slacker1
,所以这时才会去初始化SlackerHandler
,在SlackerHandler
中 new 出Slacker1
对象。这就实现了懒加载。
静态内部类方式是怎么保证线程安全的
Java 虚拟机的设计是非常稳定的,早已经考虑到了多线程并发执行的情况。虚拟机在加载类的
clinit
方法时,会保证clinit
在多线程中被正确的加锁、同步。即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行clinit
方法,其他线程都需要阻塞等待,从而保证了线程安全。
一般的建议是:对于构建不复杂,加载完成后会立即使用的单例对象,推荐使用饿汉式。对于构建过程耗时较长,并不是所有使用此类都会用到的单例对象,推荐使用懒汉式。