单例模式
1.如何编写一个单例模式?
答: 1.控制构造方法不能被外界调用public protected default private
2.在当前类中组合当前类对象作为属性并实例化
3.定义一个方法 向外界提供当前类对象
2.一个类使用了单例模式,类中有哪些组成部分?
答:如果一个类使用了单例模式 那么当前类中基本会有三个组成部分:
1私有的构造方法
2私有的当前类的对象作为静态属性
3公有的向外界提供的当前对象的一个静态方法
3.单例模式的优缺点是什么?
答:
优点:
1、减少内存开销,尤其是频繁的创建和销毁实例 2、避免对资源对过多占用。 |
缺点:
1、没有抽象层,不能继承扩展很难。 2、违背了“单一职责原则”,一个类只重视内部关系,而忽略外部关系。 3、不适用于变化对象。 4、滥用单例会出现一些负面问题,如为节省资源将数据库连接池对象设计为单例,可能会导致共享连接池对象对程序过多而出现连接池溢出。如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这样将导致对象状态丢失。
|
4.单例模式的模式的作用及应用场景有哪些?
答:
作 用: 1) 解决一个全局使用的类,频繁创建和销毁。拥有对象的唯一性,并保证内存中对象的唯一。可以节省内存, 因为单例共用一个实例,有利于Java的垃圾回收机制。
2) 也就是控制资源使用,通过线程同步来控制资源的并发访问;
3) 控制实例产生的数量,达到节约资源的目的。
适用场景: 1)需要生成唯一序列的环境
2)需要频繁实例化然后销毁的对象。
3)创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
4)方便资源相互通信的环境
5.单例模式线程安全性问题。
单例模式线程安全吗?
6.你学过的哪些技术用到了单例模式?
1、单例模式
package singleTonDemo;
public class SingleTon {
/*
* 1控制构造方法不能被外界调用 public protected default private
* 2在当前类中组合当前类对象作为属性并实例化
* 3定义一个方法 向外界提供当前类对象
* 如果一个类使用了单例模式 那么当前类中基本会有三个组成部分
* 1私有的构造方法
* 2私有的当前类的对象作为静态属性
* 3公有的向外界提供的当前对象的一个静态方法
*
*/
private static final SingleTon st =new SingleTon();
public static SingleTon getSingleTon(){
return st;
}
private SingleTon(){
}
}
2、考虑如何优化性能
上面的代码有一个问题:无论这个类是否被使用,都会创建一个对象。如果这个创建很耗时,假如说链接2w次数据库,并且这个类还不一定会被使用,那么这个创建过程就显得特别傻。
为了解决这个问题,我们想到的新的解决方案:
懒汉式单例模式
用的时候才实例化,实例化后加个判断不再重复实例化。
class SingleTon2 {
private static SingleTon2 st=null;
//懒汉式单例模式
public static SingleTon2 getSingleTon(){
if(null==st){
st=new SingleTon2();
}
return st;
}
private SingleTon2(){
}
}
代码的变化有俩处----首先,把SingleTon对象设置为 null ,知道第一次使用的时候判是否为 null 来创建对象。因为创建SingleTon对象不在声明处,所以那个 final 的修饰必须去掉。
这个过程就称为lazy loaded ,也就是懒加载或者延迟加载(直到调用的时候才进行加载)。
3、考虑线程安全
有句名言:“80%的错误是由20%的代码优化引起的”。在单线程下,懒加载没什么问题,可是如果是多线程呢,问题就来了,我们来研究一下:
线程A希望使用SingleTon,调用getSingleTon()方法。因为是第一次调用,A就发现对象是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用SingleTon,调用getSingleTon()方法,同样检测到对象是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个SingleTon的对象——单例失败!
解决的办法也很简单,那就是加锁:
class SingleTon3 {
private static SingleTon3 st=null;
//加锁懒汉式单例模式
public synchronized static SingleTon3 getInstance(){
if(null==st){
st=new SingleTon3();
}
return st;
}
private SingleTon3(){
}
}
只要getInstance()加上同步锁,一个线程必须等待另外一个线程创建完后才能使用这个方法,这就保证了单利的唯一性。
4、继续考虑性能
众所周知synchronized修饰的同步块可是要比一般的代码慢上几倍的!如果存在很多次的getInstance()调用,那性能问题就不得不考虑了。
让我们来分析一下,究竟是整个方法都必须加锁,还是紧紧其中某一句加锁就足够了?我们为什么要加锁呢?分析一下懒加载的那种情形的原因,原因就是检测null的操作和创建对象的操作分离了,导致出现只有加同步锁才能单利的唯一性。
如果这俩个操作能够原子的进行,那么单利就已经保证了。于是,进一步改进代码如下:
class SingleTon4 {
private static SingleTon4 st=null;
public static SingleTon4 getInstance(){
synchronized(SingleTon4.class){
if(null==st){
st=new SingleTon4();
}
return st;
}
}
private SingleTon4(){
}
}
首先去掉 getInstance() 的操作,然后把同步锁加载到if语句上。但是,这样的修改起不到任何作用:因为每次调用getInstance()的时候必然要经行同步,性能的问题还是存在。如果............我们事先判断一下是不是为null在去同步呢?
这种方法instance不为null也要等待锁,需要优化。
class SingleTon5 {
private static SingleTon5 st=null;
public static SingleTon5 getInstance(){
if(st== null){
synchronized(SingleTon5.class){
if(null==st){
st=new SingleTon5();
}
}
}
return st;
}
private SingleTon5(){
}
}
还有问题吗?首先判断对象是不是为null,如果为null在去进行同步,如果不为null,则直接返回对象。(该办法可以解决SingleTon对象不为null时的性能问题,为null时和原来一样)
这就是double---checked----locking 设计实现单利模式。到此为止,一切都很完美。我们用一种很聪明的方式实现了单例模式。
5、编译器优化问题(编译器优化本身没错)
下面我们开始说编译原理。所谓编译,就是把源代码”翻译“成目标代码----大多是是指机器代码----的过程。针对Java,它的目标代码不是本地机器代码,而是虚拟机代码。编译原理里面有一个很重要的内容是编译器优化。所谓编译器优化是指,在不改变原来语义的情况下,通过调整语句顺序,来让程序运行的更快。这个过程成为reorder。
要知道,JVM只是一个标准,并不是实现。JVM中并没有规定有关编译器优化的内容,也就是说,JVM实现可以自由的进行编译器优化。
下面来想一下,创建一个变量需要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。
下面我们来考虑这么一种情况:线程A开始创建SingleTon的实例,此时线程B调用了getInstance()方法,首先判断SingleTon对象是否为null。按照我们上面所说的内存模型,A已经把SingleTon对象指向了那块内存,只是还没有调用构造方法,因此B检测到SingleTon对象不为null,于是直接把SingleTon对象返回了——问题出现了,尽管对象不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将对象构造完成之前就是用了这个实例,程序就会出现错误了!
于是,我们想到了下面的代码:
class SingleTon6 {
private static SingleTon6 st=null;
public static SingleTon6 getInstance(){
if(st== null){
SingleTon6 sc;
synchronized(SingleTon6.class){
sc = st;
if(null==sc){
synchronized (SingleTon6.class) {
if(sc == null) {
st=new SingleTon6();
}
}
st = sc;
}
}
}
return st;
}
private SingleTon6(){
}
}
我们在第一个同步块里面创建一个临时变量sc,然后使用这个临时变量进行对象的创建,并且在最后把SingleTon对象指针临时变量的内存空间。写出这种代码基于以下思想,即synchronized会起到一个代码屏蔽的作用,同步块里面的代码和外部的代码没有联系。因此,在外部的同步块里面对临时变量sc进行操作并不影响SingleTon对象,所以外部类在st=sc;之前检测SingleTon对象的时候,结果SingleTon对象依然是null。
不过,这种想法完全是错误的!同步块的释放保证在此之前——也就是同步块里面——的操作必须完成,但是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器完全可以把instance=sc;这句移到内部同步块里面执行。这样,程序又是错误的了!
我的话:即然,我们改好的代码可能被编译器优化后就会出现错误。那么,我们能让编译器就按我们自己的代码顺序编译吗?可以,使用volatile关键字。
6. 解决方法
说了这么多,难道单例没有办法在Java中实现吗?其实不然!在JDK 1.5之后,Java使用了新的内存模型。volatile关键字有了明确的语义——在JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把SingleTon对象加上volatile关键字就可以了。
public class SingleTon7 {
private volatile static SingleTon7 st= null;
public static SingleTon7 getInstance() {
if (st== null) {
synchronized (SingleTon7.class) {
if(st == null) {
st = new SingleTon7();
}
}
}
return st;
}
private SingleTon7 () {
}
}
然而,这只是JDK1.5之后的Java的解决方案,那之前版本呢?其实,还有另外的一种解决方案,并不会受到Java版本的影响:
public class SingleTon8 {
private static class SingleTonInstance {
private static final SingleTon8 st= new SingleTon8();
}
public static SingleTon8 getInstance() {
return SingleTonInstance.st;
}
private SingleTon8() {
}
}
在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为SingleTon没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingleTonInstance类,这个类有一个static的SingleTon实例,因此需要调用SingleTon的构造方法,然后getInstance()将把这个内部类的SingleTon对象返回给使用者。由于这个SingleTon对象是static的,因此并不会构造多次。
由于SingleTonInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是非并发的,所以getInstance()也并不需要加同步。
哪种更好?
答:建议使用第2种,Java高效编程书也推荐使用第2种。
部分转自其他人,重新排版了下,方便大家阅读。