使用场景
内存中只需要一个实例
• 比如各种Mgr
• 比如各种Factory
一共有8种写法,但是只有2种写法是完美无缺的。
写法一:饿汉式(最常见的写法,很实用)
保证只有一个实例—》定义一个静态的实例Instance
/**
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全,
* JVM保证每一个class在load到内存中,只是load一次--->static变量是在load到内存之后,马上就进行初始化一次,就初始化这一次,多线程也没有关系
* 简单实用,推荐使用!
* 唯一缺点:不管用到与否,类装载时就马上完成实例化
* Class.forName("") ----加载一个类,只是把class放到内存中,但是不对其进行实例化
* (话说你不用的,你装载它干啥)
*/
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {}; //构造方法是private,使得别的类无法new --->只有一个实例
public static Mgr01 getInstance() { //别人要想使用,必须要调用这个方法---》所以就只有这一个实例
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2); //true
}
}
写法二:和第一个是一个意思
/**
* 跟01是一个意思
*/
public class Mgr02 {
private static final Mgr02 INSTANCE;
static { //静态语句块
INSTANCE = new Mgr02();
}
private Mgr02() {};
public static Mgr02 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr02 m1 = Mgr02.getInstance();
Mgr02 m2 = Mgr02.getInstance();
System.out.println(m1 == m2);
}
}
写法三:lazy loading
也叫懒汉式,什么时候使用,什么时候初始化
/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题(具体来说就是多线程访问的时候会有影响)
*/
public class Mgr03 {
private static Mgr03 INSTANCE; //一开始INSTANCE不进行初始化
private Mgr03() { //private导致没法new
}
public static Mgr03 getInstance() { //getInstance是初始化的方法
if (INSTANCE == null) {
try { //测试多线程访问的问题,其实多线程访问的时候又可能创建多个Mgr03(实例化了多个)
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03(); //判断INSTANCE为空,就初始化
}
return INSTANCE; //如果INSTANCE不为空,就不再初始化,仍旧用原先的那个实例
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) { //创建100个线程
new Thread(()-> //Java8的lambda表达式
System.out.println(Mgr03.getInstance().hashCode()) //不同对象hashcode是不同的
).start(); //不写lambda的话,需要在new Thread()时传入一个new Runnable,其中重写run方法(匿名内部类)
}
}
}
补充:lambda表达式就是只有一个方法 的匿名内部类(接口)的对象
说一下上面的执行结果:很多线程的getInstance的哈希值都是相同的,理论上来说应该是不一样的,但是因为线程执行速度过快,所以导致很多hashcode的值都相同,为了模拟实际情况才加入了上面的try-catch来演示结果。(哈希值相同也有可能不是同一个对象)
同一个类的不同对象其哈希值是不同的。上面其实不打印hashCode而直接打印地址也可以
特别注意:最一开始定义的private static Mgr03 INSTANCE; 不能加final因为加了final必须要初始化。
写法四:懒汉式(加锁synchronized)
/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 解决:可以通过synchronized解决,但也带来效率下降
*/
public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04() {
}
public static synchronized Mgr04 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr04.getInstance().hashCode()); //输出的是相同的哈希值
}).start();
}
}
}
static synchronized锁定的是Mgr04.class对象
写法4相对于写法3就多了一个static synchronized,别的没有增加
但是这种方法也有缺陷:内存中的对象比这个大得多,每次用的时候,都要申请锁,加锁,效率就低了。
写法5:
妄图通过减小同步代码块的方式提高效率,然后不可行
/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Mgr05 {
private static Mgr05 INSTANCE;
private Mgr05() {
}
public static Mgr05 getInstance() {
if (INSTANCE == null) {
//妄图通过减小同步代码块的方式提高效率,然后不可行
synchronized (Mgr05.class) { //在需要的地方加锁
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr05.getInstance().hashCode());
}).start();
}
}
}
这种方法并不能保证在多线程访问的时候做到只有一个实例!!
原因:锁的范围太小(不够“原子性”,在if判断的时候和下面加锁的地方多线程访问,导致创建了多个实例)
写法6:双判断(双重检查)
/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Mgr06 {
private static volatile Mgr06 INSTANCE; //JIT(这个编译器把Java语言直接编译城本地语言),注意要加上volatile,因为这个牵扯到Java虚拟机内部执行的时候汇编语言的优化(语句重排,指令重排,这个会非常频繁),不加volatile会使得没有初始化的时候就返回INSTANCE
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) { //防止多余上锁
//双重检查
synchronized (Mgr06.class) {
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}
这种写法是不会出问题的,很完美
写法7:静态内部类方式(完美)
/**
* 静态内部类方式
* JVM保证单例
* 加载外部类时不会加载内部类,这样可以实现懒加载
*/
public class Mgr07 {
private Mgr07() {
}
private static class Mgr07Holder { //静态内部类,在加载Mgr07的时候,没有初始化这个静态内部类的,这就是优于写法1的地方。只有在调用getInstance方法时,才会加载这个内部类(懒加载也能实现)
private final static Mgr07 INSTANCE = new Mgr07(); //初始化Mgr07
}
public static Mgr07 getInstance() {
return Mgr07Holder.INSTANCE; //返回的是静态内部类中的INSTANCE
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr07.getInstance().hashCode());
}).start();
}
}
}
上面方法为什么线程安全?是JVM帮我们搞定的。虚拟机在加载一个class的时候只会加载一次。
写Java的大牛还出过一本书叫做“Effective Java”,书中写到了一个单例的写法,就是下面的写法8:
写法8:完美中的完美——枚举单例
/**
* 不仅可以解决线程同步,还可以防止反序列化。
*/
public enum Mgr08 {//枚举类
INSTANCE;
public void m() {}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr08.INSTANCE.hashCode());
}).start();
}
}
}
实际应用中,其实写法1就够了,没必要追求不必要的完美。
关于序列化和反序列化
Java的反射可以通过一个class文件将整个class load到内存,然后再new一个实例出来---->反序列化,“不可阻挡”
为了防止反序列化,当然用写法8是能够解决这个问题的(网安中有严重 的反序列化漏洞,简单来说一些白帽子或者一些黑客可能会利用反序列化漏洞)。
写法8之所以能够避免反序列化漏洞是因为这个类(枚举类)没有构造方法。所以说写法8是最完美的方法。但是实际中确实方法1用的最多。因为明明应该是一个类,好好的把它搞成枚举,确实有点让人觉得奇怪了。
单例总结
语法上最完美的是写法8,但是实际中用的最多的是写法1