单例模式 Singleton
很多时候,我们对于某个类,是不需要有很多实例存在的。打个比方:1个教室里面有很多学生要喝水,但是只有1台饮水机,没有必要给每个学生都安排1台饮水机。
这个时候,我们就需要使用单例模式
一.饿汉式(最常用)
public class Singleton {
//1.final修饰的常量一般字母大写
//2.自己内部new出1个对象后 给构造方法加private让其他人无法new
private static final Singleton INSTANCE = new Singleton();
private Singleton(){};
//3.private修饰的 需要有1个返回方法
public static Singleton getInstance(){return INSTANCE;};
public static void main(String[] args) {
Singleton m1 = Singleton.getInstance();
Singleton m2 = Singleton.getInstance();
//验证 是不是 只会new出1个对象 如果地址相等 那么一定是同1个
System.out.println(m1 == m2);
}
}
饿汉式的优点:
1.类加载到内存后,就实例化一个单例,而且JVM线程安全(因为JVM保证每个class类 只会load到内存一次 static变量是在类load后马上进行初始化的 所以它也只会初始化一次 多线程访问也没有关系)
2.简单方便
private static final Singleton INSTANCE;
//可以把 static修饰符 变成static代码块
static {
INSTANCE = new Singleton();
}
...其他不变
二.懒汉式
懒汉式 :临到用时方恨少 用到时才创建
public class Singleton2 {
//注意我们这个对象不能 定义为final 因为定义了final 就是常量要进行static初始化(要么用static修饰 要么用static静态代码块)
private static Singleton2 INSTANCE;
private Singleton2(){
}
public static Singleton2 getInstance() {
//懒汉式 :临到用时方恨少 用到时才创建
if(INSTANCE == null ){
INSTANCE = new Singleton2();
}
return INSTANCE;
}
public static void main(String[] args) {
//测试一下 是否获取的是同一对象
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton2.getInstance().hashCode());
}).start();
}
}
}
缺点:多线程同时访问时 线程不安全
public static Singleton2 getInstance() {
if(INSTANCE == null ){
INSTANCE = new Singleton2();
}
return INSTANCE;
}
假如第1个线程 调用了getInstance() 但是还没有new Singleton2()
此时另外一个线程来了 它也会判断if 有没有INSTANCE 它此时是null的 那么第二个线程就会new一个Singletion2() 而第一个线程也继续执行 那么这个INSTANCE在两个线程中 已经不是同一个实例了
我们可以做个实验 在INSTACE = new Singleton2()之前 让线程sleep一会儿
if(INSTANCE == null ){
try {
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE = new Singleton2();
}
return INSTANCE;

三.加锁版
为了解决懒汉式线程不安全的问题,我们可以在调用getInstace()前,给方法加上锁
public class Singleton3 {
private static Singleton3 INSTANCE;
private Singleton3(){
}
public static synchronized Singleton3 getInstance(){
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton3();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton3.getInstance().hashCode());
}).start();
}
}
}
缺点:
1.内存中的对象 大得多比原来的
2.用的时候要看 有没有加锁,有没有申请到这把锁 效率降低
四.1个错误的单例模式
想减少锁住的范围,提高效率,但是却让线程不安全
public class Singleton4 {
private static Singleton4 INSTANCE;
private Singleton4(){
}
public static Singleton4 getInstance() {
if(INSTANCE == null ){
synchronized (Singleton4.class) {
INSTANCE = new Singleton4();
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton4.getInstance().hashCode());
}).start();
}
}
}
这种方法不能做到只new出来1个实例:因为当第1个线程 执行到 synchronized之前时,第2个线程 往下执行完了,申请到了锁,并且new出来对象Singleton4;然后第2个线程释放锁,第1个进程进入synchronized继续执行,它又new出来了1个Singleton4对象
本质原因:就是因为 它对象的判空if(INSTANCE == null) 没有和创建对象INSTANCE = new Singleton4(); 同时操作 只要有1个时间间隙 就会出现 不同的对象 就会线程不安全 必须在INSTANCE==null的前提条件 去new Singleton4才可以
五.双重判断/检查法
我们在new Singleton前 我们再判空1次 就行了
public class Singleton5 {
private static Singleton5 INSTANCE;
private Singleton5(){
}
public static Singleton5 getInstance(){
if(INSTANCE == null){
synchronized (Singleton5.class){
//双重检查
if(INSTANCE == null ){
INSTANCE = new Singleton5();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton4.getInstance().hashCode());
}).start();
}
}
}
为什么要判断两次?第一次可以不可以去掉?
if(INSTANCE == null){
synchronized (Singleton5.class){
其实有必要 第一次的判断 因为这样会省很多事 不然很多情况都要被上锁 有很多情况 一旦INSTANCE被初始化后 进来看到不为空后 它就不会继续执行下去了 提高效率
六.静态内部类方法
public class Singleton6 {
//给构造方法加上private是单例模式的标配了
private Singleton6(){
}
//加载外部类时不会加载内部类,这样可以实现懒加载
private static class Singleton6Holder {
//只有它的内部类在内部 才可以访问 外部类访问不了
private final static Singleton6 INSTANCE = new Singleton6();
}
public static Singleton6 getInstance() {
return Singleton6Holder.INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton6.getInstance().hashCode());
}).start();
}
}
}
加载外部类时不会加载内部类,这样可以实现懒加载,只有当我们调用 getInstance()的时候 才会被加载,它是线程安全的,因为JVM只加载class一次
这比上面几种都好
七.枚举类(Java创始人的写法)
public enum Singleton7 {
INSTANCE;
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton7.INSTANCE.hashCode());
}).start();
}
}
}
这样不仅可以解决线程不同步 还可以反序列化(因为java 反射可以通过class文件 把整个class load到内存 new出1个实例 前面的写法 他都可以找到你的class文件 来new 1个INSTANCE出来 通过 反序列化的方式 而枚举单例 不会被反序列化 它只是一个抽象类,因为枚举类 没有构造方法 就算你拿到他的class文件 也没有办法 构造他的对象,返回的只是1个值 返回的是单例创建的1个对象)
缺点:本来你是一个resource manger的类 但是你定义为枚举 有点别扭