废话不多说,直接看代码:
1 饿汉模式:
/**
*
* @author admin 单例模式中的饿汉模式
*
*/
public class SingleTonHungry {
// 1 私有化构造函数 让外界无法直接创建对象
private SingleTonHungry() {
}
// 2 自己将对象创建出来 但是不让外界调用 所以用private修饰
private static SingleTonHungry hungryInstance = new SingleTonHungry();
// 3 提供一个外界可以访问的方法 将对象返回出去
public static SingleTonHungry getInstance() {
return hungryInstance;
}
}
2 懒汉模式:
/**
*
* @author admin 单利模式 懒汉模式
*
*/
public class SingletonLazy {
// 1 私有化构造函数
private SingletonLazy() {
}
// 2 自己创建对象 单不初始化
public static SingletonLazy lazyInstance = null;
// 3 向外提供返回对象的方法 并在该方法中创建对象
public static SingletonLazy getInstance() {
if (lazyInstance == null) {
lazyInstance = new SingletonLazy();
}
return lazyInstance;
}
}
测试类看看是否实现了真的单例:
/**
*
* @author admin 单利模式的测试类
*
*/
public class SingletonTest {
public static void main(String[] args) {
SingleTonHungry hungry1 = SingleTonHungry.getInstance();
SingleTonHungry hungry2 = SingleTonHungry.getInstance();
if (hungry1 == hungry2) {
System.out.println("是同一个实例");
} else {
System.out.println("不是同一个实例");
}
SingletonLazy lazy1 = SingletonLazy.getInstance();
SingletonLazy lazy2 = SingletonLazy.getInstance();
if (lazy1 == lazy2) {
System.out.println("是同一个实例");
} else {
System.out.println("不是同一个实例");
}
}
}
打印结果:是同一个实例
是同一个实例
那说明不管是饿汉还是懒汉,都实现了单例;
比较下两种创建对象的异同:
1 饿汉模式:类加载的时候静态对象就创建了,所以类加载的比较慢,在运行的时候获取对象的时候比较快,因为这个时候对象已经创建好,还有就是线程安全;
2 懒汉模式:类加载的时候并没有创建对象,所以类加载比较快,但是在运行的时候创建对象就比较慢,还有就是线程不安全;
可能,大家就疑惑了,什么是线程安全?为什么一个是安全一个是不安全的呢?
线程安全:简单的来讲,你的代码也就是你写的程序中存在多个线程在运行,而这些线程可能会同时触及到某一段代码,如果每次运行的结果和单线程运行的结果是一样的而且其他的变量的值也和预期的结果一样的就是线程安全的。
接下来我们来证明为什么饿汉是安全的懒汉是不安全的:
/**
*
* @author admin 开启线程测试饿汉
*
*/
public class TestHungry extends Thread {
@Override
public void run() {
super.run();
System.out.println(SingleTonHungry.getInstance().hashCode());
}
}
/**
*
* @author admin 开启线程测试懒汉
*
*/
public class TestLazy extends Thread {
@Override
public void run() {
super.run();
System.out.println(SingletonLazy.getInstance().hashCode());
}
}
下面看测试代码:
/**
*
* @author admin 单利模式的测试类
*
*/
public class SingletonTest {
public static void main(String[] args) {
// SingleTonHungry hungry1 = SingleTonHungry.getInstance();
// SingleTonHungry hungry2 = SingleTonHungry.getInstance();
// if (hungry1 == hungry2) {
// System.out.println("是同一个实例");
// } else {
// System.out.println("不是同一个实例");
// }
//
// SingletonLazy lazy1 = SingletonLazy.getInstance();
// SingletonLazy lazy2 = SingletonLazy.getInstance();
// if (lazy1 == lazy2) {
// System.out.println("是同一个实例");
// } else {
// System.out.println("不是同一个实例");
// }
TestLazy singletonTest2 = new TestLazy();
TestLazy singletonTest3 = new TestLazy();
singletonTest2.start();
singletonTest3.start();
TestHungry testHungry1 = new TestHungry();
TestHungry testHungry2 = new TestHungry();
testHungry1.start();
testHungry2.start();
}
}
这段测试的代码就是分别对饿汉模式和懒汉模式开启线程创建对象,打印出对象的hashcode值,看结果:
可以看见懒汉模式的对象hash值不同,那么可以得出就不是同一个对象,而饿汉模式的hash值是相同的,那就说明是同一个对象,从而也论证了饿汉的线程安全性。
下面我们来解决懒汉模式的线程安全问题:
3 懒汉模式(同步锁 synchronized)
/**
*
* @author admin 同步锁保证创建对象的线程安全
*
*/
public class SingletonLazySafe1 {
private SingletonLazySafe1() {
}
public static SingletonLazySafe1 lazyInstance = null;
public static synchronized SingletonLazySafe1 getInstance() {
if (lazyInstance == null) {
lazyInstance = new SingletonLazySafe1();
}
return lazyInstance;
}
}
以上的代码只是在懒汉模式的基础上加了一个字段,就是在返回对象的方法上加上了synchronized,下面看下加上这个字段是否安全了呢?
/**
*
* @author admin 开启线程测试懒汉
*
*/
public class TestLazy extends Thread {
@Override
public void run() {
super.run();
System.out.println(SingletonLazySafe1.getInstance().hashCode());
}
}
/**
*
* @author admin 单利模式的测试类
*
*/
public class SingletonTest {
public static void main(String[] args) {
TestLazy singletonTest2 = new TestLazy();
TestLazy singletonTest3 = new TestLazy();
singletonTest2.start();
singletonTest3.start();
}
}
打印结果:
可见这种方式实现了线程安全,但是每次调用都需要进行同步,对性能的损耗比较大。
4 懒汉模式(双重锁 synchronized)
public class SingletonLazySafe2 {
private SingletonLazySafe2() {
}
public static volatile SingletonLazySafe2 lazyInstance = null;
public static SingletonLazySafe2 getInstance() {
if (lazyInstance == null) {
synchronized (SingletonLazySafe2.class) {
if (lazyInstance == null) {
lazyInstance = new SingletonLazySafe2();
}
}
}
return lazyInstance;
}
}
public class TestLazy extends Thread {
@Override
public void run() {
super.run();
System.out.println(SingletonLazySafe2.getInstance().hashCode());
}
public class SingletonTest {
public static void main(String[] args) {
TestLazy singletonTest2 = new TestLazy();
TestLazy singletonTest3 = new TestLazy();
singletonTest2.start();
singletonTest3.start();
}
}
打印结果:
hash值也是相同的,也达到了线程安全的目的,这种写法和上面一种区别是:在返回对象的getInstance()方法中进行了两次的null判断,确保了只有第一次调用的时候回进行同步,后面再次调用不会进行同步,这样降低了性能损耗。
这里为什么要用volatile修饰instance?(自己还不是很清楚,此处摘抄的,后期会花时间搞明白)
原因:在于instance = new SingletonLazySafe2()的时候,在内存中实际上是分3步执行的:
1)分配对象的内存空间:memory = allocate();
2)初始化对象:ctorInstance(memory);
3)指向分配的地址:instance =memory
多线程在执行的时候,2 3可能发生重排序。即有可能线程A执行到第3步的时候,读取到instance不为null,就返回。实际上此时还未执行第二部即未初始化。
加上volatile就可以避免2 3步重排序来保证线程安全。
5 静态内部类(static class)
public class SingletonLazySafe3 {
private SingletonLazySafe3() {
}
private static class LazyHolder {
private static final SingletonLazySafe3 INSTANCE = new SingletonLazySafe3();
}
public static final SingletonLazySafe3 getInstance() {
return LazyHolder.INSTANCE;
}
}
这种方式测试后的结果也是一样的,我就不再一一写出来测试了。这种方式是利用了classLoader的加载机制,达到线程安全的同时也避免了性能损耗,因为静态内部类不会随着类的加载就直接加载,所以类SingletonLazySafe3加载的时候LazyHolder并没有加载,只有在getInstance()方法的时候才会去加载,才会创建实例。大家仔细观察就会静态内部类创建对象的方式其实就是饿汉模式的升级版,因为饿汉模式类加载的时候就创建对象了,如果一直不用这个对象,那么这个对象就占着内存然而却没得用处是很浪费内存的。大家也知道创建对象的方式有很多种,new 反射 克隆 反系列化,我们写的这几种创建单例的方式都是基于new的情况下来保证对象的唯一性,别的情况下也没研究,如果大家向深入研究的话,可以自己去探讨。