Java实现单例的5种方式
1. 什么是单例模式
单例模式指的是在应用整个生命周期内只能存在一个实例。单例模式是一种被广泛使用的设计模式。他有很多好处,能够避免实例对象的重复创建,减少创建实例的系统开销,节省内存。
2. 单例模式和静态类的区别
首先理解一下什么是静态类,静态类就是一个类里面都是静态方法和静态field,构造器被private修饰,因此不能被实例化。Math类就是一个静态类。
知道了什么是静态类后,来说一下他们两者之间的区别:
1)首先单例模式会提供给你一个全局唯一的对象,静态类只是提供给你很多静态方法,这些方法不用创建对象,通过类就可以直接调用;
2)如果是一个非常重的对象,单例模式可以懒加载,静态类就无法做到;
那么时候时候应该用静态类,什么时候应该用单例模式呢?首先如果你只是想使用一些工具方法,那么最好用静态类,静态类比单例类更快,因为静态的绑定是在编译期进行的。如果你要维护状态信息,或者访问资源时,应该选用单例模式。还可以这样说,当你需要面向对象的能力时(比如继承、多态)时,选用单例类,当你仅仅是提供一些方法时选用静态类。
3.如何实现单例模式
1. 饿汉模式
所谓饿汉模式就是立即加载,一般情况下再调用getInstancef方法之前就已经产生了实例,也就是在类加载的时候已经产生了。这种模式的缺点很明显,就是占用资源,当单例类很大的时候,其实我们是想使用的时候再产生实例。因此这种方式适合占用资源少,在初始化的时候就会被用到的类。
class SingletonHungary {
private static SingletonHungary singletonHungary = new SingletonHungary();
//将构造器设置为private禁止通过new进行实例化
private SingletonHungary() {
}
public static SingletonHungary getInstance() {
return singletonHungary;
}
}
2. 懒汉模式
懒汉模式就是延迟加载,也叫懒加载。在程序需要用到的时候再创建实例,这样保证了内存不会被浪费。针对懒汉模式,这里给出了5种实现方式,有些实现方式是线程不安全的,也就是说在多线程并发的环境下可能出现资源同步问题。
首先第一种方式,在单线程下没问题,在多线程下就出现问题了。
// 单例模式的懒汉实现1--线程不安全
class SingletonLazy1 {
private static SingletonLazy1 singletonLazy;
private SingletonLazy1() {
}
public static SingletonLazy1 getInstance() {
if (null == singletonLazy) {
try {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
singletonLazy = new SingletonLazy1();
}
return singletonLazy;
}
}
我们模拟10个异步线程测试一下:
public class SingletonLazyTest {
public static void main(String[] args) {
Thread2[] ThreadArr = new Thread2[10];
for (int i = 0; i < ThreadArr.length; i++) {
ThreadArr[i] = new Thread2();
ThreadArr[i].start();
}
}
}
// 测试线程
class Thread2 extends Thread {
@Override
public void run() {
System.out.println(SingletonLazy1.getInstance().hashCode());
}
}
运行结果:
124191239
124191239
872096466
1603289047
1698032342
1913667618
371739364
124191239
1723650563
367137303
可以看到他们的hashCode不都是一样的,说明在多线程环境下,产生了多个对象,不符合单例模式的要求。
那么如何使线程安全呢?第二种方法,我们使用synchronized关键字对getInstance方法进行同步。
// 单例模式的懒汉实现2--线程安全
// 通过设置同步方法,效率太低,整个方法被加锁
class SingletonLazy2 {
private static SingletonLazy2 singletonLazy;
private SingletonLazy2() {
}
public static synchronized SingletonLazy2 getInstance() {
try {
if (null == singletonLazy) {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
singletonLazy = new SingletonLazy2();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return singletonLazy;
}
}
使用上面的测试类,测试结果:
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
可以看到,这种方式达到了线程安全。但是缺点就是效率太低,是同步运行的,下个线程想要取得对象,就必须要等上一个线程释放,才可以继续执行。
那我们可以不对方法加锁,而是将里面的代码加锁,也可以实现线程安全。但这种方式和同步方法一样,也是同步运行的,效率也很低。
// 单例模式的懒汉实现3--线程安全
// 通过设置同步代码块,效率也太低,整个代码块被加锁
class SingletonLazy3 {
private static SingletonLazy3 singletonLazy;
private SingletonLazy3() {
}
public static SingletonLazy3 getInstance() {
try {
synchronized (SingletonLazy3.class) {
if (null == singletonLazy) {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
singletonLazy = new SingletonLazy3();
}
}
} catch (InterruptedException e) {
// TODO: handle exception
}
return singletonLazy;
}
}
我们来继续优化代码,我们只给创建对象的代码进行加锁,但是这样能保证线程安全么?
// 单例模式的懒汉实现4--线程不安全
// 通过设置同步代码块,只同步创建实例的代码
// 但是还是有线程安全问题
class SingletonLazy4 {
private static SingletonLazy4 singletonLazy;
private SingletonLazy4() {
}
public static SingletonLazy4 getInstance() {
try {
if (null == singletonLazy) { //代码1
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
synchronized (SingletonLazy4.class) {
singletonLazy = new SingletonLazy4(); //代码2
}
}
} catch (InterruptedException e) {
// TODO: handle exception
}
return singletonLazy;
}
}
我们来看一下运行结果:
1210004989
1425839054
1723650563
389001266
1356914048
389001266
1560241484
278778395
124191239
367137303
从结果看来,这种方式不能保证线程安全,为什么呢?我们假设有两个线程A和B同时走到了‘代码1’,因为此时对象还是空的,所以都能进到方法里面,线程A首先抢到锁,创建了对象。释放锁后线程B拿到了锁也会走到‘代码2’,也创建了一个对象,因此多线程环境下就不能保证单例了。
让我们来继续优化一下,既然上述方式存在问题,那我们在同步代码块里面再一次做一下null判断不就行了,这种方式就是我们的DCL双重检查锁机制。
//单例模式的懒汉实现5--线程安全
//通过设置同步代码块,使用DCL双检查锁机制
//使用双检查锁机制成功的解决了单例模式的懒汉实现的线程不安全问题和效率问题
//DCL 也是大多数多线程结合单例模式使用的解决方案
class SingletonLazy5 {
private static SingletonLazy5 singletonLazy;
private SingletonLazy5() {
}
public static SingletonLazy5 getInstance() {
try {
if (null == singletonLazy) {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
synchronized (SingletonLazy5.class) {
if(null == singletonLazy) {
singletonLazy = new SingletonLazy5();
}
}
}
} catch (InterruptedException e) {
// TODO: handle exception
}
return singletonLazy;
}
}
运行结果:
124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
我们可以看到DCL双重检查锁机制很好的解决了懒加载单例模式的效率问题和线程安全问题。这也是我们最常用到的方式。
3. 静态内部类
我们也可以使用静态内部类实现单例模式,代码如下:
//使用静态内部类实现单例模式--线程安全
class SingletonStaticInner {
private SingletonStaticInner() {
}
private static class SingletonInner {
private static SingletonStaticInner singletonStaticInner = new SingletonStaticInner();
}
public static SingletonStaticInner getInstance() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return SingletonInner.singletonStaticInner;
}
}
可以看到使用这种方式我们没有显式的进行任何同步操作,那他是如何保证线程安全呢?和饿汉模式一样,是靠JVM保证类的静态成员只能被加载一次的特点,这样就从JVM层面保证了只会有一个实例对象。那么问题来了,这种方式和饿汉模式又有什么区别呢?不也是立即加载么?实则不然,加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
可以说这种方式是实现单例模式的最优解。
4. 静态代码块
这里提供了静态代码块实现单例模式。这种方式和第一种类似,也是一种饿汉模式。
//使用静态代码块实现单例模式
class SingletonStaticBlock {
private static SingletonStaticBlock singletonStaticBlock;
static {
singletonStaticBlock = new SingletonStaticBlock();
}
public static SingletonStaticBlock getInstance() {
return singletonStaticBlock;
}
}
5. 序列化与反序列化
LZ为什么要提序列化和反序列化呢?因为单例模式虽然能保证线程安全,但在序列化和反序列化的情况下会出现生成多个对象的情况。运行下面的测试类,
public class SingletonStaticInnerSerializeTest {
public static void main(String[] args) {
try {
SingletonStaticInnerSerialize serialize = SingletonStaticInnerSerialize.getInstance();
System.out.println(serialize.hashCode());
//序列化
FileOutputStream fo = new FileOutputStream("tem");
ObjectOutputStream oo = new ObjectOutputStream(fo);
oo.writeObject(serialize);
oo.close();
fo.close();
//反序列化
FileInputStream fi = new FileInputStream("tem");
ObjectInputStream oi = new ObjectInputStream(fi);
SingletonStaticInnerSerialize serialize2 = (SingletonStaticInnerSerialize) oi.readObject();
oi.close();
fi.close();
System.out.println(serialize2.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//使用匿名内部类实现单例模式,在遇见序列化和反序列化的场景,得到的不是同一个实例
//解决这个问题是在序列化的时候使用readResolve方法,即去掉注释的部分
class SingletonStaticInnerSerialize implements Serializable {
/**
* 2018年03月28日
*/
private static final long serialVersionUID = 1L;
private static class InnerClass {
private static SingletonStaticInnerSerialize singletonStaticInnerSerialize = new SingletonStaticInnerSerialize();
}
public static SingletonStaticInnerSerialize getInstance() {
return InnerClass.singletonStaticInnerSerialize;
}
// protected Object readResolve() {
// System.out.println("调用了readResolve方法");
// return InnerClass.singletonStaticInnerSerialize;
// }
}
可以看到:
865113938
1078694789
结果表明的确是两个不同的对象实例,违背了单例模式,那么如何解决这个问题呢?解决办法就是在反序列化中使用readResolve()方法,将上面的注释代码去掉,再次运行:
865113938
调用了readResolve方法
865113938
问题来了,readResolve()方法到底是何方神圣,其实当JVM从内存中反序列化地"组装"一个新对象时,就会自动调用这个 readResolve方法来返回我们指定好的对象了, 单例规则也就得到了保证。readResolve()的出现允许程序员自行控制通过反序列化得到的对象。
Java实现单例的5种方式的更多相关文章
- Objective-C和Swift实现单例的几种方式
在Swift开发中,我们对于跨类调用的变量常量,因为并没有OC中使用的全局头文件中写宏的形式,我们一般采用在类外定义全局变量/常量的形式来跨类调用.而问题在于目前写的项目需要在新添加的OC写的功能模块 ...
- swift实现单例的四种方式
单例模式 单例模式是设计模式中最简单的一种,甚至有些模式大师都不称其为模式,称其为一种实现技巧,因为设计模式讲究对象之间的关系的抽象,而单例模式只有自己一个对象. 当你只需要一个实例的时候需要使用单例 ...
- 【设计模式】Java之单例设计模式
1.单例设计模式:一个类只能有一个对象 1.1 创建单例类的步骤: 1.将构造方法私有化 2.创建私有的静态成员变量 3.共有的静态成员方法,提供当前的唯一对象 1.1 创建单例的两种方式: 1.饿汉 ...
- Java 之单例设计模式
设计模式: 对问题行之有效的解决方式, 其实它是一种思想. 单例设计模式 解决的问题:就是可以保证一个类在内存中的对象唯一性. 即单个实例. 比如对于A 和 B 两个程序使用同一个配置信息对象时, A ...
- java单例的几种实现方法
java单例的几种实现方法: 方式1: public class Something { private Something() {} private static class LazyHolder ...
- iOS单例的两种实现
单例模式算是开发中比较常见的一种模式了.在iOS中,单例有两种实现方式(至少我目前只发现两种).根据线程安全的实现来区分,一种是使用@synchronized,另一种是使用GCD的dispatch_o ...
- objc单例的两种安全实现方案
所有转出博客园,请您注明出处:http://www.cnblogs.com/xiaobajiu/p/4122034.html objc的单例的两种安全实现方案 首先应该知道单例的实现有两大类,一个是懒 ...
- Java中HashMap遍历的两种方式
Java中HashMap遍历的两种方式 转]Java中HashMap遍历的两种方式原文地址: http://www.javaweb.cc/language/java/032291.shtml 第一种: ...
- JAVA中集合输出的四种方式
在JAVA中Collection输出有四种方式,分别如下: 一) Iterator输出. 该方式适用于Collection的所有子类. public class Hello { public stat ...
随机推荐
- Java 之 Collections 工具类
一.Collections 概述 java.utils.Collections 是集合工具类,用来对集合进行操作. 二.常用方法 public static <T> boolean add ...
- Java基础加强-代理
/*代理*//*代理的概念与作用*/ 代理过程架构 客户端Client原来直接调用的是Target目标类 使用代理后,现在让客户端不要调用Target,调用代理类Proxy,代理类Proxy和目标类T ...
- 安装habse
1.下载zookeeper-3.4.5.tar.gz, hbase-0.98.6-hadoop2-bin.tar.gz 2.上传到master的 /usr/local/src/目录下,解压zookee ...
- 【Hibernate】事务处理
一.概述 一.概述 事务 事务就是逻辑上的一组操作,要么全都成功,要么全都失败!!! 事务特性 原子性:事务一组操作不可分割. 一致性:事务的执行前后,数据完整性要保持一致. 隔离性:一个事务在执行的 ...
- spring 时间格式问题
注解@JsonFormat主要是后台到前台的时间格式的转换 注解@DateTimeFormat主要是前后到后台的时间格式的转换 @DateTimeFormat(pattern = "yyyy ...
- 15.Vue组件中的data
1.组件中展示数据和响应事件: // 1. 组件可以有自己的 data 数据 // 2. 组件的 data 和 实例的 data 有点不一样,实例中的 data 可以为一个对象 // 3. 但是组件中 ...
- nginx简单反向代理实例
一.要做什么? 实例最后实现的效果图: 我们在浏览器地址栏上输入 wangtong,代理服务器获取请求,将请求转发至指定的 tomcat 上 二.怎样做? 1.准备环境 虚拟中中需要安装 JDK+To ...
- Oracle 安装步骤
目录 Oracle 安装步骤 一.安装流程 二.登录流程 三.新建数据库 四.图形化连接 Oracle 安装步骤 一.安装流程 解压oracle 11g两个压缩文件 点击安装,修改目录,新建一个文件夹 ...
- JVM内存空间划分与作用详解
在之前已经对Java的字节码进行了非常详细而又系统的学习了,接下来开启jvm内存相关的新篇章,在一个新知识开头之前肯定得理论化的对其进行一个整体的介绍,所以摒弃浮躁,先来看看相关的理论,主要是看一下J ...
- java 基础:方法调用中的值传递是call by value,并且传递的是参数的值的拷贝,而不是引用
public class TestExtends { public static void main(String[]args){ int s = 10; System.out.println(Sys ...