(六)单例模式与多线程时的安全问题以及解决办法

单例模式:

首先明白单例模式是什么,简单来讲,就是说多个线程获取到的对象是同一个对象,只new了一次,那么创建单例有两种方式:

1.立即加载:即在程序一开始就new了一个对象,之后用的时候直接进行获取,这种一般是定义静态对象,因为静态对象会预加载。

2.延迟加载:顾名思义,指在第一次用的时候才创建对象,除了第一次获取以外的是直接获取。

所以,当我们将单例模式和多线程结合,会有什么问题呢?

如下是一个延迟加载和多线程的例子:

MyObject.java:延迟加载方式创建对象

package 第六章;

public class MyObject {
    private static MyObject myObject;
    public MyObject(){

    }
    public static MyObject getInstance(){
        if(myObject == null){    //只有第一次创建对象
            myObject = new MyObject();
        }
        return myObject;
    }
}
View Code

MyThread.java:  输出当前线程获取到的对象的hashcode

package 第六章;

public class MyThread extends Thread {
    public void run(){
        System.out.println("线程"+Thread.currentThread().getName()+"@@@"+MyObject.getInstance().hashCode());
    }
}
View Code

test.java: 运行类,创建三个线程并运行,

package 第六章;

public class test {
    public static void main(String[] args){
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        MyThread myThread3 = new MyThread();
        myThread1.start();
        myThread2.start();
        myThread3.start();
    }
}
View Code

运行结果如下:

很明显,我们可以看到三个线程获取到的对象并不都是同一个,原因很简单,由于在创建对象的代码没有加锁,是异步的,所以有多个线程if条件判断成立,new了多个对象,解决方法也很简单,将相应的代码或者方法变成同步的。如下, 

MyObject.java:  sleep模拟创建过程

package 第六章;

public class MyObject {
    private static MyObject myObject;
    public MyObject(){

    }
    public static MyObject getInstance(){
        synchronized (MyLock.lock) {
            try{
                if (myObject == null) {    //只有第一次创建对象
                    Thread.sleep(1000);
                    myObject = new MyObject();
                    System.out.println("时间"+System.currentTimeMillis());
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            return myObject;
        }
    }
}
View Code

运行之后可以看到实现了单例效果,对象只创建了一次

但是,这种方法很蠢,因为每一次一个线程想要获取实例,就必须等待上一个线程之内的同步代码块执行完毕才行,这样子效率很低,事实上,我们只是想在第一次加载对象的时候使用同步,之后我们都不需要同步,因为对象已经创建好了,异步直接获取就行。

解决办法:

DCL双检查机制:只同步创建对象的代码块,并且进行两次检查,防止多次创建,

更改MyObject.java

package 第六章;

public class MyObject {
    private static MyObject myObject;
    public MyObject(){

    }
    public static MyObject getInstance(){
            try{
                if (myObject == null) {    //只有第一次创建对象
                    Thread.sleep(1000);
                    synchronized (MyLock.lock) {
                        if(myObject==null){
                            myObject = new MyObject();
                            System.out.println("时间" + System.currentTimeMillis());
                        }
                    }
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            return myObject;
    }
}
View Code

个人感觉还是比较巧妙的吧,这样子相比原来尽可能的异步执行了更多的代码块。

当然也可以使用enum,static代码块实现单例,因为枚举类的构造函数是预加载的,static代码块也是预加载的,不过这种就是立即加载了,实现方法,

static代码块,将new对象的语句写在static{}之中就ok

还有静态内置类,但是这种在序列化和反序列化时候会出现一定的问题,需要用readResolve()方法,emmm,这块没太看懂想干什么,日后再说

posted @ 2019-08-22 16:09  _Ennio  阅读(1871)  评论(0编辑  收藏  举报