Java单例,一篇就够了
2020-11-11 本文已影响0人
南风向北zhy
本文从几种代码实现以及探究为啥写的方式叙述。话不多少,开始~
- 单例实现思路:
1.私有化构造方法
2.创建对象
3.提供获取对象的方法
第一种:饿汉式
-
1.1 常见的写法一
/**
* 恶汉式
*/
public class Hangury {
private Hangury(){}
private static final Hangury HANGURY=new Hangury();
public static Hangury getInstance(){
return HANGURY;
}
}
-
1.2 常见的写法 二 (比较常见,kotlin object 实现单例 编译以后的java文件就是这个)
/**
* 恶汉式
*/
public class Hangury {
private Hangury(){}
private static Hangury HANGURY;
static {
HANGURY =new Hangury();
}
public static Hangury getInstance(){
return HANGURY;
}
}
恶汉分析:
优点:
1.线程安全
缺点:
1.类创建的时候对象就创建了,可能不会使用
2.可能会造成资源浪费,比如下面的代码
/**
* 恶汉式
*/
public class Hangury {
/***
* 造成byte 数组占用内存
*/
private byte[] b1=new byte[1024*1024*1024];
private Hangury(){}
private static Hangury HANGURY;
static {
HANGURY =new Hangury();
}
public static Hangury getInstance(){
return HANGURY;
}
}
第二种:懒汉式
-
1.1 经典版
/**
* 懒汉式
*/
public class LazyMan {
private LazyMan(){}
private static LazyMan lazyMan ;
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan =new LazyMan();
}
return lazyMan;
}
}
避免了懒汉式的缺点,但是在多线程是不安全的,如下代码:
/**
* 懒汉式
*/
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"---run");
}
private static LazyMan lazyMan ;
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan =new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
LazyMan.getInstance();
}
}).start();
}
}
}
多次运行的结果可能不相同,线程创建多对象,导致创建的对象不是唯一
- 运行结果:
Thread-0----run
Thread-2----run
-
1.2 双重检验锁 DCL
/**
* 懒汉式
*/
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"---run");
}
private static LazyMan lazyMan ;
public static LazyMan getInstance(){
if(lazyMan == null){ // ①
synchronized (LazyMan.class){ //②
if(lazyMan == null){ // ③
lazyMan =new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
LazyMan.getInstance();
}
}).start();
}
}
}
- 解释:(面试高频)
- 1.针对多线程 线程 1 和 线程 2 同时进度 getInstance,都能通过第一个非空判断进入到同步锁中,不管是谁先进入同步锁内,一个进入了另一个就会在锁外面,等待进入锁内的线程执行完毕
- 2.当进入锁内的线程执行完毕创建了对象,锁外的进入锁内进行第二个非空判断,直接返回已经创建的对象
- 3.第一个非空判断是增加性能,不能每次进来都执行锁的判断
市面上大多都是这样的,但是这个不是最优化的单例,因为创建对象涉及到原子性:Volatile.
- 优化的代码如下 成员变量添加 Volatile 修饰
/**
* 懒汉式
*/
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"---run");
}
private volatile static LazyMan lazyMan ;
public static LazyMan getInstance(){
if(lazyMan == null){ // ①
synchronized (LazyMan.class){ //②
if(lazyMan == null){ // ③
lazyMan =new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
LazyMan.getInstance();
}
}).start();
}
}
}
解释 :
- 原子性涉及到的是指令重排的问题
创建对象在底层涉及到三点: - 1.分配内存
- 2.创建对象
- 3.对象指向内存空间
正常的我们都认为 顺序是 1->2->3,但是有些时候 会是1->3->2,就会造成线程1进入锁创建对象的时候还没有创建完成,只是单纯的指向了内存,当第二个线程进入锁后直接返回的是没有创建成功的对象导致空指针
第三种 内部类
public class Holder {
private Holder(){}
public static Holder getInstance(){
return InnerClass.holder;
}
private static class InnerClass {
private static final Holder holder =new Holder();
}
}
第四种 枚举
public enum SingleEmum {
INSTANCE ;
public static SingleEmum getInstance(){
return INSTANCE;
}
}
class test{
public static void main(String[] args) {
final SingleEmum instance1 = SingleEmum.INSTANCE;
final SingleEmum instance2 = SingleEmum.INSTANCE;
System.out.println(instance1);
System.out.println(instance2);
}
}
- 单例安全性最好的还是枚举类型的,为什么?
-
反射机制
在这里插入图片描述 - 枚举为什么不会因为反射多次创建 ? 在这里插入图片描述
这些你都应该知道的,但是如何选择?
-
单例对象 占用资源少,不需要延时加载,枚举 好于 饿汉
-
单例对象 占用资源多,需要延时加载,静态内部类 好于 懒汉式