java 单例模式模拟实现
单例模式就是说保证一个类只有一个实例。
我们的生活中有很多单例:太阳,地球,皇帝,董事长,女神(baby,你就是我的唯一…)…
思路分析:如何能使n对象变成只有1对象?
答案其实很简单直接用private
关键字来修饰构造方法…
目标:一个类只能产生一个实例
-
实例从哪里来?(构造方法,所以要设置构造方法对外不可见)
-
构造方法对外不可见,那怎样创建实例?(谁能产生实例就找谁!只有它自已可以)
-
怎样保证只有一个实例?(静态变量在内存中只有一份,你自已保证你只创建一次就可以了)
单例模式又分为: 饿汉模式,懒汉模式
饿汉模式: 在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。形象的说就是不管有没有人要使用此类的实例,先创建一个再说
下面用代码模拟实现:
package cn;
public class Hungry {
//将构造方法私有化,其他类就不能创建本类的对象
private Hungry(){
}
private static Hungry hungry= new Hungry();//静态变量只有一份,对象的唯一性,饿汉模式就是不管用不用先创建一个对象再说。
//之所以声明为静态的是因为它其他类调用不到他的构造方法,只有他自己能调用,所以是只能自己
//创建一个类返回去
public static Hungry getInstance(){
//Hungry h = new Hungry();不能这样写,因为每调用一次这个方法就创建一次,
//达不到只有一个对象的目标
return hungry;
}
}
测试类:
package cn;
public class TestMain {
public static void main(String[] args) {
Hungry h1 = Hungry.getInstance();//调用静态方法返回一个创建好的对象
Hungry h2 = Hungry.getInstance();
Hungry h3 = Hungry.getInstance();
System.out.println(h1);
System.out.println(h2);
System.out.println(h3);
}
}
运行结果:
懒汉模式: 在类加载时不初始化。如果没有人要获取此类的实例,就不创建,直到有人获取的时候才创建。
下面用代码模拟一下(非线程安全):
package cn;
public class Lazy {
public static Lazy lazy =null;
private Lazy(){
}
public static Lazy getInstance(){
if(lazy==null){
lazy = new Lazy();
}
return lazy;
}
}
测试类:
package cn;
public class TestMain {
public static void main(String[] args) {
Lazy l1 = Lazy.getInstance();
Lazy l2 = Lazy.getInstance();
Lazy l3 = Lazy.getInstance();
System.out.println(l1);
System.out.println(l2);
System.out.println(l3);
}
}
运行结果:
但是这样会存在一个线程安全问题,当多个线程同时调用这个方法的时候,if(lazy==null){ lazy = new Lazy(); }
还未来得及更新,后面线程一个就已经在调用了,这样就产生了两个不同的对象,这和单例的初衷违背。什么?你不信?那就模拟一下。
package cn;
public class Lazy {
public static Lazy lazy =null;
private Lazy(){
}
public static Lazy getInstance(){
if(lazy==null){
System.out.println(Thread.currentThread().getName()+"进入");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazy = new Lazy();//延迟1秒创建对象
// System.out.println(Thread.currentThread().getName()+"----->"+lazy);
}
return lazy;
}
}
这里的System.out.println(Thread.currentThread().getName()+"----->"+lazy);
打印语句和在线程里面的run()
方法里面的打印输出语句是一样的,所以只需要写那个地方就可以。
线程类:
package cn;
public class MyRunnable implements Runnable{
@Override
public void run() {
Lazy lazy = Lazy.getInstance();//这个地方才获取Lazy对象,
System.out.println(Thread.currentThread().getName()+"创建了----->"+lazy);
}
}
让它在线程run()
方法里面打印。
package cn;
public class TestMain {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
Thread t2 = new Thread(new MyRunnable());
t1.setName("t1线程");
t2.setName("t2线程");
t1.start();
t2.start();
}
}
这个运行结果真的是超级有意思,我搞了一晚上,差点被整模糊了。
下面来把几组运行结果看一下
第一组:
第二组:
最最难以理解的东西来了!!
我让模拟并发的线程Lazy
类里面,同时执行getInsance()
方法,注意观察if
语句,if
语句是只有对象为空的时候才会进入到if语句,然后我在if
里面,注意,这里是if
里面,不是外面,一旦进入到if
语句里面就说明他们可以获取到两个不同的对象,这和单例原则相违背,巧就巧在我明明让两个线程都进入到了if
里面还用下面的输出语句证明他们一定都进入到了这个if
语句里面了
System.out.println(Thread.currentThread().getName()+"进入");
输出结果也显示了他们已经进入了,这时候其实线程安全问题就发生了,但是他们还是获取到了相同的对象,比如第一种情况,这是为什么??因为这个问题搞了一晚上。
其实那会我已经在钻牛角尖了,其实很简单,就是存在这么一种情况,他们同时睡眠了一秒以后,同时执行了new Lazy()
,恰巧的是,他们同时选中一个地址块空间,然后把哈希值(或者说是地址)返回去了,这样就出现了第一种情况,而且这种概率还很高…
第二种情况就是我们所期待的线程并发的安全问题了,单例模式目的是让调用它的类只获取到唯一的一个对象,而不是多个不同的对象。
总结:懒汉模式在高线程并发的情况下会产生线程安全问题,因为它是在调用的时候才会去创建对象;但是饿汉模式不会,因为饿汉模式在被调用的时候就已经创建好了对象,就算是高并发情况下,它也是已经创建好了对象了,直接返回去的,所以不存在线程安全问题。
那要怎么解决懒汉模式的线程安全问题呢?
毫无疑问直接加锁synchronized
。
下面展示第一种方案:
package cn;
public class Lazy {
public static Lazy lazy =null;
private Lazy(){
}
public static synchronized Lazy getInstance(){
if(lazy==null){
System.out.println(Thread.currentThread().getName()+"进入");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazy = new Lazy();//延迟1秒创建对象
// System.out.println(Thread.currentThread().getName()+"----->"+lazy);
}
return lazy;
}
}
其他类不变,运行结果为:
可以看到结果就只有t1
一个线程进入到了if
里面,也不一定是t1
就看谁先抢到时间片的问题。这不过这种方式并不是最优的,一旦方法锁了,方法里面的其他东西也被锁了。这种写法在getInstance()方法中加入了synchronized锁。能够在多线程中很好的工作,而且看起来它也具备很好的,但是效率很低(因为锁),并且大多数情况下不需要同步。
第二种方案:
package cn;
public class Lazy {
public static Lazy lazy =null;
private Lazy(){
}
public static Lazy getInstance(){
if(lazy==null){
synchronized (Lazy.class){
if(lazy==null){
System.out.println(Thread.currentThread().getName()+"进入");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazy = new Lazy();
}
}
}
return lazy;
}
}
第一种方案的升级版,俗称双重检查锁定,注意在JDK1.5之后,双重检查锁定才能够正常达到单例效果。