利用单例模式解决全局访问问题
在面向对象编程中,我们无时无刻都可能在产生对象,因为我们的代码需要对象,但值得注意的是,我们有时候也有可能是在无谓的产生对象,更加可怕的是,这些累赘的对象会造成难以排查的BUG,尤其是在多线程编程中。
所以,合理的产生对象也是一个学问。
有些对象我们只需要一个,像是线程池,缓冲等,这类对象只能有一个实例,一旦产生多个实例就会出现问题。所以,我们必须找到一种方法来确保我们的代码中只有一个实例。
首先我们想到的第一个解决方法就是声明一个全局变量,然后将对象赋值给该全局变量,但是这意味着我们必须在程序一开始的时候就要创建好该对象,但我们应该是在需要的时候才创建对象,而且如果这个对象本身非常耗费资源的话,这就是一种浪费。
提到创建对象,这里有一点必须要提到:
static BluetoothSocket socket = null;
因为java鼓励人们在声明对象的时候赋予对象初始值以免出现问题,但对象这时候并没有被创建!真正的创建对象应该是通过new和反射机制来产生。
使用全局变量的时候,我们必须确保整个程序中只有一份,因为使用全局变量的目的就是为了共享资源,而共享资源必须到达两个条件:不变和共享。不变,指的是这个资源在整个程序中必须只有一份,而共享是指它的变化必须对共享它的所有对象是可见的,否则,所有对象都有自己的一份,何来共享呢?
因此,我们必须确保一件事:一个类只有一个实例,其他类无法自行产生它的实例。
使用单例模式就能确保这点。
单例模式的意图是:确保一个类只有一个实例,并且提供一个全局访问点。它到底是怎么做到这点的呢?
我们知道,一般类是通过构造器来产生的,而构造器一般都是public,也就是可访问的,但如果我们将构造器设为private,那么也就能阻止其他类产生该对象的实例,但问题也就来了:私有的构造器只有该类才能访问,但是我们无法产生该类的实例,又如何能得到该类的实例呢?
这时我们就需要在该类中声明一个自己本身的静态实例,然后通过静态方法返回。我们知道,静态实例在程序中只有一份,所以这也就能确保该实例在程序中只有一份,并且因为构造器是私有的,其他类也就无法产生实例。
下面是使用单例模式的经典用法:
private static BluetoothSocket bluetoothSocket; private BluetoothSocket(){} public static BluetoothSicket getBluetoothSocket(){ if(blueSocket == null){ bluetoothSocket = new BluetoothSokcet(); } return bluetoothSocket; }
因为getBluetoothSocket()是一个静态方法,因为我们不需要通过创建实例来调用该方法,而且这里还使用了创建对象时经常使用到的方法:延迟实例化,又叫滞后初始化,利用这点,我们可以先判断程序中是否已经创建了实例,如果没有才创建实例,这样就能确保程序中永远只有一份实例了。使用延迟实例化的最大好处就是我们可以在需要的时候才创建对象。
单例模式是为了消除程序员无谓的创建全局变量,尤其是新手,像是我一开始编程的时候,就喜欢创建全局变量,因为不用考虑命名空间这些东西真心舒服,因为那时候我还在学习C和C++,即使java表面上没有命名空间的说法,但其实内部的机制也是基于命名空间,只不过是用包导入机制确保我们不需要为这个问题烦恼而已。
无论是哪种程序语言,都不鼓励使用全局变量,那意味着编程结构不好。
如果是一般的编码,单例模式好像有点大材小用了,但如果是多线程编程这种让人纠结的东西,单例模式在一定程度上给予了线程安全。
同步这个话题是我们学习java跳不过的,而且也是一个非常重要的难点,要想写出一个线程安全的代码,是需要我们不断努力的。
就算是单例模式,我们也无法确保两个线程不会同时创建实例,最糟糕的的情况就是多个线程同时调用静态方法同时产生实例,这在多线程中是非常常见的现象。因此,我们可以在静态方法前添加synchronized关键字来迫使每个线程在进入这个方法前,要先等候别的线程离开该方法,以确保不会有多个线程同时调用该方法。
但问题还是来了:只有第一次执行该方法的时候我们才真正需要同步,因为静态方法在第一次被调用后就能确保不会再产生实例了。synchronized关键字这时就是个累赘了,它只在第一次时有用,以后每次调用该方法的时候都要为此付出无谓的线程消耗。
所以,同步一个方法并不是一个好的做法,它会让我们的程序存在一个无限等待的后台,导致我们的程序效率非常低下。
我们只要稍微改动一下代码就行:
private static BluetoothSocket bluetoothSocket = new BluetoothSocket(); private BluetoothSocket(){} public static BluetoothSocket getBluetooth(){ return bluetoothSocket; }
为什么这段代码就能保证线程安全呢?明明它就只是废弃了延迟实例化而已。因为这样JVM能够确保在任何线程调用静态方法前就已经创建好该实例了。
我们还可以利用线程的其他知识来完成这个任务:
private volatile static BluetoothSocket bluetoothSocket; private BluetoothSocket(){} public static BluetoothSicket getBluetoothSocket(){ if(blueSocket == null){ synchronized(BluetoothSocket.class){ if(blueSocket == null){ bluetoothSocket = new BluetoothSokcet(); } } } return bluetoothSocket; }
这就是多重检查加锁的做法。我们首先检查实例是否已经创建,如果尚未创建,才进行同步。
volatile关键字能够确保实例被创建时,所有线程都能正确处理该变量。什么叫正确的处理,就是我们利用volatile同步了该实例,在该实例发生变化的时候,其他共享线程都能知道从而进行相应的处理。多重检查加锁需要利用到对象锁,每个java对象身上都有一个锁,当一个线程获取到该锁后,就会防止其他线程试图访问该对象,除非该线程释放了这个对象的锁。volatile一般都是要和对象锁一起搭配才能发挥真正的作用,一个控制住了变化,另一个则是控制住了访问。
单例模式之所以能够确保只有一个实例,也是建立在只有一个类加载器的前提下,如果有两个以上的类加载器,就有可能产生多个单例并存的奇怪现象。所以,这时我们必须指定类加载器。但一般的程序都只有一个类加载器,但涉及到多线程的话,这个可就不知道了。
总结一下,单例模式的最大作用就是将一个对象的职责全部集中在一个实例上,这样就能避免无谓的浪费,但单例模式也并不是一个可以随便乱用,它就和全局变量的使用是一样的,将所有职责交给一个实例,这对实例本身就是一个不公平,尤其是职责非常重大的时候。