(六)单例模式与多线程时的安全问题以及解决办法
单例模式:
首先明白单例模式是什么,简单来讲,就是说多个线程获取到的对象是同一个对象,只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; } }
MyThread.java: 输出当前线程获取到的对象的hashcode
package 第六章;
public class MyThread extends Thread {
public void run(){
System.out.println("线程"+Thread.currentThread().getName()+"@@@"+MyObject.getInstance().hashCode());
}
}
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();
}
}
运行结果如下:
很明显,我们可以看到三个线程获取到的对象并不都是同一个,原因很简单,由于在创建对象的代码没有加锁,是异步的,所以有多个线程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;
}
}
}
运行之后可以看到实现了单例效果,对象只创建了一次
但是,这种方法很蠢,因为每一次一个线程想要获取实例,就必须等待上一个线程之内的同步代码块执行完毕才行,这样子效率很低,事实上,我们只是想在第一次加载对象的时候使用同步,之后我们都不需要同步,因为对象已经创建好了,异步直接获取就行。
解决办法:
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; } }
个人感觉还是比较巧妙的吧,这样子相比原来尽可能的异步执行了更多的代码块。
当然也可以使用enum,static代码块实现单例,因为枚举类的构造函数是预加载的,static代码块也是预加载的,不过这种就是立即加载了,实现方法,
static代码块,将new对象的语句写在static{}之中就ok
还有静态内置类,但是这种在序列化和反序列化时候会出现一定的问题,需要用readResolve()方法,emmm,这块没太看懂想干什么,日后再说