设计模式之单例模式:单例模式指的是某个类在整个生命周期内,它只能被实例化一次,每次访问这个类获取的实例
都要求获取的是同一个实例,单例模式经常用在下面场景中
- 硬件访问
- 数据库链接
- 全局配置
另外在Spring中的Bean中,默认也都是单例模式,下面是单例模式的常见使用方式,代码中的Thread.sleep(1000)
用来模拟线程在执行的过程中,时间片执行完毕而被CPU退出执行的场景,
/**
* 单例模式实现起来很简单,只不过需要注意的是要保证线程安全
* 所以这个地方有点技巧,而线程安全主要发生的地方是在最开始
* 目标单例对象没有被new出来,而同时又被多线程并发访问获取该对象时
* 就会出现线程安全问题,当对象被这一轮并发访问new出来之后,就不存在线程安全问题了
*/
public class SingleDog {
private static SingleDog singleDog;
/**
* 这个方法在单线程访问情况下
* 不会有问题,能够满足单例模式的需求,保证任何事情获取到的都是同一个对象
* 但是在并发多线程访问的情况下,第一次在singleDog为null时两个线程同时进入该方法if判断
* ,这个时候由于singleDog为null,所以连个线程都会new一个SingleDog 对象出来,这也并没
* 有保证单例模式,这样这两个线程拿到的就不是同一个对象
* @return
*/
public static SingleDog getInstance() throws InterruptedException {
if(singleDog==null){
//测试线程安全
Thread.sleep(1000);
singleDog = new SingleDog();
}
return singleDog;
}
/**
*这个方法保证了线程安全,但实际上当第一次singleDog被new出来
* 之后,就不会存在线程安全问题了,而这个时候synchronized就会反而拖慢
* 方法的执行效率,因为每次这个方法只能被一个线程访问,线程需要等待获取
* 到方法的锁
* @return
*/
public synchronized static SingleDog getInstanceThreadSafe() throws InterruptedException {
Thread.sleep(1000);
if(singleDog==null){
singleDog = new SingleDog();
}
return singleDog;
}
/**
*该方法既解决了线程安全问题,也解决了
* 该方法同时不能被多个线程访问的效率问题
* 首先判断singleDog是否为null,如果为null才进入到被同步的代码块然后new一个SingleDog
* 这样如果singleDog不为null的话 不会对并发访问产生影响,
* 可能有两层if会比较容易迷惑,因为假设在singleDog为null的情况下
* 两个线程A,B通知进入到了第一个if判断,这样假设A线程拿到锁,B
* 线程则会在该if判断内等待,直到A线程new了SingleDog对象,
* 释放了锁,这个时候B拿到了锁,如果同步代码块里面没有再加if判断,那么
* B线程也会new一个SingleDog对象,但实际上线程A前面已经new了一个该对象
* 如果B再new一个就会重复,所以这里面应该加两层判断 来保证对象的唯一不变。
* @return
*/
public static SingleDog getInstanceEffective() throws InterruptedException {
if(singleDog==null){
Thread.sleep(1000);//模拟CPU时间片执行完毕
synchronized(SingleDog.class){
if(singleDog==null){
singleDog= new SingleDog();
}
}
}
return singleDog;
}
public static void setNull(){
singleDog = null;
}
}
测试:
public class Test {
public static void main(String[] args) throws InterruptedException {
TestExample test = new TestExample(1);
//使用方式1线程不安全
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
System.out.println("-----方法getInstance,并发访问单例对象返回不是同一个,存在线程安全问题------");
long startTime = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long endTime = System.currentTimeMillis();
System.out.println("耗时 :"+(endTime-startTime));
SingleDog.setNull();//重新设置为NULL 避免影响下面测试
//使用方式2效率低
test.setMethod(2);
System.out.println("-----方法二getInstanceThreadSafe,线程安全,但是效率比较低------");
startTime = System.currentTimeMillis();
t1 = new Thread(test);
t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
endTime = System.currentTimeMillis();
System.out.println("耗时 :"+(endTime-startTime));
SingleDog.setNull();//重新设置为NULL 避免影响下面测试
//方式3线程安全,效率问题基本解决,但是有一个地方还是有问题
//假设在代码同步快里面被休眠
System.out.println("-----getInstanceEffective,线程安全,效率低得到基本解决------");
test.setMethod(3);
startTime = System.currentTimeMillis();
t1 = new Thread(test);
t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
endTime = System.currentTimeMillis();
System.out.println("耗时 :"+(endTime-startTime));
}
}
class TestExample implements Runnable{
private int method;
public SingleDog singleDog;
public TestExample(int i) {
this.method = i;
}
public int getMethod() {
return method;
}
public void setMethod(int method) {
this.method = method;
}
@Override
public void run() {
try {
if(method==1){
singleDog = SingleDog.getInstance();
}else if(method==2){
singleDog = SingleDog.getInstanceThreadSafe();
}else{
singleDog = SingleDog.getInstanceEffective();
}
System.out.println(singleDog);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
-----方法getInstance,并发访问单例对象返回不是同一个,存在线程安全问题------
com.chen.singleton.SingleDog@281266ef
com.chen.singleton.SingleDog@6e7fb50c
耗时 :1004
-----方法二getInstanceThreadSafe,线程安全,但是效率比较低------
com.chen.singleton.SingleDog@3a237724
com.chen.singleton.SingleDog@3a237724
耗时 :2007
-----getInstanceEffective,线程安全,效率低得到基本解决------
com.chen.singleton.SingleDog@1dbdc081
com.chen.singleton.SingleDog@1dbdc081
耗时 :1003
以上的单例模式使用的是懒加载的模式,懒加载模式指的是当真正有需要这个类的实例,也就是获取实例的方法getInstance,getInstanceThreadSafe,getInstanceEffective被访问时,才会去new这个类的实例;
另外一种则是饥饿模式,在该对象被声明的地方就会去初始化这个类的实例,如下:
public class SingleDog {
private static SingleDog singleDog = new SingleDog();
public static SingleDog getInstance() throws InterruptedException {
return singleDog;
}
}
不过这种方式存在一个问题就是,万一这个实例在类的声明周期内都没有被访问到,那么就造成了一个资源的浪费,因为对象new出来了没有被用到,而对象是要占用内存的!