单例模式
单例模式,保障内存中只有一个对象 在实际的工作中,常常会用到一些配置类等等,这些类不需要多个对象, 所以通过单例模式可以减少无用对象的创建
单例模式的设计仅仅只能防止正常创建对象的方式进行创建 如果是以反射的方法进行创建对象的话是没办法的
枚举的方式可以解决反射创建对象的问题…(待了解)
饿汉式
饿汉式便是无论我调用与否,他都会在内存中加载该对象且只能一个
package it.luke.singleton;
/*
* 饿汉式
* 单例模式->保证程序中对于该类只会有一个对象
* */
public class Singleton01 {
//创建实例
private static final Singleton01 Instance = new Singleton01();
//无参构造私有化-->保证其他人调用不了
private Singleton01(){
}
//通过调用静态方法的形式,获取该类的实例,所以可以说静态get对象是单例的标识
public static Singleton01 getInstance(){
return Instance;
}
/**
* 测试多次调用的类是否同一个对象
* @param args
*/
public static void main(String[] args) {
Singleton01 instance1 = Singleton01.getInstance();
Singleton01 instance2 = Singleton01.getInstance();
System.out.println(instance1 == instance2);
}
}
//返回的结果为true
不允许自己封装的组件,在被别人的时候随意new对象出来(防止占用不必要的内存空间)
原理
- 构造方法私有化
问题:
- 如果我不用到实例的时候,我不需要加载(饿汉式比较占用内存)
懒汉式
package it.luke.singleton;
/**
* 懒汉式
* 构造方法私有化,
* 让你通过调用方法的时候,在对所需类的初始化
*/
public class Singleton02 {
private static Singleton02 Instance;
//私有化构造
private Singleton02(){
}
public static Singleton02 getInstance(){
//为了防止多个对象的生成,先进行判断
if(Instance == null){
//为了测试多线程访问的问题,添加线程睡眠
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Instance = new Singleton02();
}
return Instance;
}
/**
* 创建多个线程调用方法,查看返回对象的hashcode
* @param args
*/
public static void main(String[] args) {
for(int i = 0;i < 10 ;i++){
new Thread(new Runnable() {
@Override
public void run() {
Singleton02 instance = Singleton02.getInstance();
System.out.println(instance.hashCode());
}
}).start();
}
}
}
//结果:
/**
245887817
2049367639
1027487819
195071900
1553816201
1011435609
1699630862
1617048292
245887817
1030697658
*/
原理
- 构造方法私有化
- 通过get的方法来创建实例,创建方法中添加判断是否为空的控制条件
问题
- 线程不安全
- 多个线程访问的时候,做完非空判断后,另一个线程进行访问,判断非空又通过了,结果又new了一个对象,这样就不是同一个对象了
解决办法
- 加锁
//加一个同步锁,保证执行锁里面内容的串行化
public static synchronized Singleton03 getInstance() {
//为了防止多个对象的生成,先进行判断
if (Instance == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Instance = new Singleton03();
}
return Instance;
}
/** 结果:
980142845
980142845
980142845
980142845
980142845
980142845
980142845
980142845
980142845
980142845
*/
但是会带来效率降低的问题,因为每次进行调用的时候都要进行加锁(加锁的原理–>为什么会效率降低)
如果将锁下至到调用构造方法之前
public static Singleton04 getInstance() {
//为了防止多个对象的生成,先进行判断
if (Instance == null) {
//加一个同步锁,保证执行锁里面内容的串行化
synchronized (Singleton04.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Instance = new Singleton04();
}
}
return Instance;
}
/**
结果:
2049367639
1787716501
664286818
711020178
1110432790
158723118
1101855987
76872907
980142845
1553816201
*/
不能解决,因为非空判断和加锁没有一体化,所以会导致多个线程判断完后,等待锁的过程中,另一个线程释放锁了,还是会造成多个对象不一致的问题
解决办法
public static Singleton05 getInstance() {
//为了防止多个对象的生成,先进行判断
if (Instance == null) {
//加一个同步锁,保证执行锁里面内容的串行化
synchronized (Singleton05.class) {
//双重锁
if (Instance == null)
{
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Instance = new Singleton05();
}
}
}
return Instance;
}
/**
结果:
1030697658
1030697658
1030697658
1030697658
1030697658
1030697658
1030697658
1030697658
1030697658
1030697658
*/
双重判断
- 在判断非空后排队等待获得锁的时候,在加锁的逻辑过程中进行多一次对象的非空判断,如果是空的才进行new对象,保障了对象的单例
(疑问点,是否可以去除外层的判断)
双重判断除了可以保障判断和加锁的一致性,还可以防止每次调用的时候都去加锁,降低效率,在第一次判断对象非空的时候,就直接返回了,不需要在去加锁做第二次判断
补充:
private static volatile Singleton05 Instance; //volatile修饰变量保证对象的原子性
(待补充)
不加volatile的话,INSTANCE没有被初始化也有可能会被返回
volatile在这里体现的是原子性,保证该对象在创建的过程中,如果发生了重排序的化,返回的内存地址是还未初始化的对象
静态内部类的方式
package it.luke.singleton;
/**
* 静态内部类的方式
*/
public class Singleton06 {
//私有化构造方法
private Singleton06(){
}
//静态内部类
private static class Singleton06_new{
final static Singleton06_new Instance = new Singleton06_new();
}
//静态方法获取类对象
public static Singleton06_new getInstance(){
//线程睡眠
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Singleton06_new.Instance;
}
/**
* 多线程访问测试
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Singleton06_new instance = Singleton06.getInstance();
System.out.println(instance.hashCode());
}
}).start();
}
}
}
/**
610025186
610025186
610025186
610025186
610025186
610025186
610025186
610025186
610025186
610025186
*/
在类加载的时候,类的静态内部类是不会被加载的(内部类的加载原理):–待补充
什么时候会被加载?当你调用了方法去调用的时候,他才会被加载
推荐原因:
上种方式的问题:线程不安全
静态内部类的方式的线程安全是通过JVM来保证的
因为虚拟机在加载类的时候,是只加载一次的
(类似内部饿汉式)
枚举类的方式
package it.luke.singleton;
public enum Singleton07 {
Singleton01,
enum_demo;
public static void main(String[] args) {
for (int i = 0 ;i<10 ;i++){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Singleton07.enum_demo.hashCode());
}
}).start();
}
}
}
/**
结果:
1110432790
1110432790
1110432790
1110432790
1110432790
1110432790
1110432790
1110432790
1110432790
1110432790
*/
不仅解决多线程线同步,还防止反序列化
问题:枚举单例是如何创建对象…
用枚举类来设计单例模式比较少见,虽然枚举类可以类似class的语法,写方法等等
但是他本身已经继承了一个类,而java又是单继承的,所以扩展性大大降低了(待补充)