单例模式--导读
在我们编程过程中我们会经常碰到一些对象,他们要频繁的使用,而我们为了方便于是便频繁的对这个对像进行新建,当然这对我们编程的确是很方便但是,这同时会造成很大的资源浪费。所以我们想出了一个限制资源的方法,那就是只能新建一次,而不允许第二次新建,但是我们又需全局对该类的引用所以我们干脆使得该类不需要在其他类中进行实例化,可以对该类进行直接引用,这便是单例模式。
单例模式--定义
从上面的导读我们可以理解到,单例模式的特点为:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式--模型
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式--代码实现
对于单例模式我们想的方法必然是私有化构造方法,同时使用一个静态变量,让其他类无法新建该对象,这样便可以确保该对象只能产生一个
当要引用时先判断静态变量中是否为空,为空则新建,不为空则不新建,私有化构造方法可以确保其他类不能对该类进行实例化
下面进行代码展示:
Singleton.java单例的类
package Singleton; //懒汉式单例 public class Singleton { //定义静态变量,用于判断是否已经被实例化 private static Singleton uniqueSingleton; //私有化构造器,防止其他类进行实例化 private Singleton() { } //返回所需的对象 public static Singleton getInstance() { if(uniqueSingleton==null) uniqueSingleton=new Singleton(); return uniqueSingleton; } public void showMessage() { System.out.println("hello world"); } }
client.java客户端的使用
package Singleton; public class Client { public static void main(String[] args) { // TODO Auto-generated method stub Singleton single=Singleton.getInstance(); single.showMessage(); } }
但是这样可能出现一个问题当我们另外开个线程的时候就会发现,这样的单例根本没有用。以下例子
package Singleton; public class Singleton { //定义静态变量,用于判断是否已经被实例化 private static Singleton uniqueSingleton; //私有化构造器,防止其他类进行实例化 private Singleton() { } //返回所需的对象 public static Singleton getInstance() { //需要消耗资源,和时间 try { if(uniqueSingleton==null) { Thread.sleep(300); uniqueSingleton=new Singleton(); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return uniqueSingleton; } public void showMessage() { System.out.println("hello world"); } }
客户端采用多线程使用该单例:
package Singleton; public class Client extends Thread { @Override public void run() { System.out.println(Singleton.getInstance().hashCode()); } public static void main(String[] args) { // TODO Auto-generated method stub Client[] mts = new Client[10]; for(int i = 0 ; i < mts.length ; i++){ mts[i] = new Client(); } for (int j = 0; j < mts.length; j++) { mts[j].start(); } } }
结果就会发现单例模式根本就没有用:
可以从图中的哈希地址发现根本就不是同一个对象,这是因为:
线程1在判断完s==null后突然交换了cpu的使用权,变为线程2执行,由于s仍然为null,那么线程2中就会创建这个Singleton的单例对象。之后线程1拿回cpu的使用权,而正好线程1之前暂停的位置就是判断s是否为null之后,创建对象之前。这样线程1又会创建一个新的Singleton对象
以下又三种方法解决该单例问题
1.饿汉式
package Singleton; //饿汉单例 public class Singleton { //因为在类加载时就已经实现实例化,所以不用担心线程问题 private static Singleton uniqueSingleton=new Singleton(0;//但是在加载时可能会消耗更多时间 //私有化构造器,防止其他类进行实例化 private Singleton() { } //返回所需的对象 public static Singleton getInstance() { uniqueSingleton=new Singleton(); return uniqueSingleton; } public void showMessage() { System.out.println("hello world"); } }
虽然饱汉式可以解决线程不安全的问题,但是他也有一个严重的问题就是他是在类加载的过程中就开始实例化,这就导致了在加载类的时候效率可能会很低,但是我只是需要一个全局变量
所以并尽量既满足效率有能保证性能安全。所以就引出了下面的线程同步的解决方案。
2.线程同步
出现了上述问题时,我们就应该要考虑到同步(synchronized)问题了,当一个线程获得锁的时候其他线程就只能在外等着 ,这样就能解决线程不安全的问题了
package Singleton; //懒汉式单例 public class Singleton { //定义静态变量,用于判断是否已经被实例化 private static Singleton uniqueSingleton; //私有化构造器,防止其他类进行实例化 private Singleton() { } //返回所需的对象 public synchronized static Singleton getInstance() { if(uniqueSingleton==null) uniqueSingleton=new Singleton(); return uniqueSingleton; } public void showMessage() { System.out.println("hello world"); } }
3.双重校验锁
线程同步虽然可以解决线程不安全的问题,但是他有时可能不能保证。虽然这个想想出现的概率极低,但是为了保证不出现这个问题,所以就提除了双重锁
这里代码就不重复了就是在同步的情况下使用(volatile)来修饰uniqueInstance;
这里说明一下volatile的作用:
1.volatile(java5):可以保证多线程下的可见性;
2.读volatile:每当子线程某一语句要用到volatile变量时,都会从主线程重新拷贝一份,这样就保证子线程的会跟主线程的一致。
3.写volatile: 每当子线程某一语句要写volatile变量时,都会在读完后同步到主线程去,这样就保证主线程的变量及时更新。
单例模式--优缺点
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点:
1.没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
单例模式--使用场景
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。