这篇是入职之后的第二篇了,上一篇我简单介绍了一下LOCK里面的类的方法,感兴趣的话可以去了解一下,以后坚持每周至少会更新一篇关于多线程方面的文章,希望博友们可以一起加油成长。

这篇主要的内容是单例模式在多线程环境下的设计,这篇算是比较重要的内容,我会进行文字和代码的共同说明来讲解记录

1、立即加载(饿汉模式)

说到标题,有人会说什么是立即加载呢?立即加载就是使用类的时候已经将对象创建完毕了,比如说直接new实例化对象。也就是在调用方法之前,实例已经被创建了

  

public class MyObject {
private static MyObject myObject = new MyObject();
private MyObject(){ }
public static MyObject getInstance(){
return myObject;
}
}

看这段代码,他的缺点是不能有其他实例变量。外部的使用者需要使用MyObject实例的时候,只能通过getInstance方法,另外假如没有用到这个实例的时候,他已经创建了出现,会有资源浪费的情况出现的。还有因为getInstance方法没有同步,所以有可能出现非线程安全的问题。

2、延迟加载(懒汉模式)

延迟加载就是在调用要使用的那个方法(假如MyObject方法)的时候实例才会被创建,实现方法就是在MyObject方法里面进行new实例化。

  

public class MyObject {
private static MyObject myObject;
private MyObject(){ }
public static MyObject getInstance(){
if(myObject==null){
myObject = new MyObject();
}
return myObject;
}
}

此代码虽然取得了一个对象,没毛病。单例!但是如果在多线程情况下,就会取出多个实例的情况,这个是与单例模式的初衷背道而驰的。

  

public class MyObject {
private static MyObject myObject;
private MyObject(){ }
public static MyObject getInstance(){
if(myObject==null){
try {
Thread.sleep(3000);
myObject = new MyObject();
}catch(Exception e){
e.printStackTrace();
}
}
return myObject;
}
}

可以试着自己多建立几个线程,运行一下这段代码,发现在多线程环境在建立出来了多个实例(可以打印对象的hashcode值进行比较)

那我们应该怎么去解决这个问题呢?

3、延迟加载(懒汉模式)——synchronized

  

public class MyObject {
private static MyObject myObject;
private MyObject(){ }
synchronized public static MyObject getInstance(){
if(myObject==null){
try {
Thread.sleep(3000);
myObject = new MyObject();
}catch(Exception e){
e.printStackTrace();
}
}
return myObject;
}
}

这样OK?看上去是的,是解决了得到了相同的实例,但是见到了synchronized这个东西,你不得犹豫一下么?加在了整个方法上啊,如果这个方法设计到比如说很多的过程或者运算,下一个线程想要取得对象,不是要等到程序员找到女朋友才行么。也就是要等到上一个线程释放锁之后,才可以继续进行。

有人说,我不加全部,我加部分不行么?

  

   public static MyObject getInstance(){
synchronized (MyObject.class) {
if (myObject == null) {
try {
Thread.sleep(3000);
myObject = new MyObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return myObject;
}
}

你仔细看,有啥大的变化么,并没有吧。因为效率还是一样的低低低低。每次我调用getIstance的时候是不是还要同步啊,所以太大变化啦

然后机智的我想出了这样的方法

  

public class MyObject {
private static MyObject myObject;
private MyObject(){ }
public static MyObject getInstance(){ if (myObject == null) {
try {
Thread.sleep(3000);
synchronized (MyObject.class) {
myObject = new MyObject();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return myObject;
}
}

我只加载了需要创建对象的那个关键地方,看到了么,这样效率大大滴提升了。但是,重点来了,我靠,打印出来了两个不同对象,打印出来的对象hashcode值不一样了啊,不是一个对象了,因为两个线程都进入了if语句内,之后没有在进行判断,所以创建了两个对象。我单例什么呢?所以这个方法也pass

那我到底该怎么办呢?别着急,DCL双重检查所机制,不废话直接看代码

  

public class MyObject {
private static MyObject myObject;
private MyObject(){ }
public static MyObject getInstance(){ if (myObject == null) {
try {
Thread.sleep(3000);
synchronized (MyObject.class) {
if(myObject == null) {
myObject = new MyObject();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return myObject;
}
}

哇,终于有方法可以实现单例模式在多线程环境下的正常工作了,哈哈哈哈哈,但是但是那你们就打错特错了。看如下分析

从JVM的角度来说,怎么创建一个对象呢?第一是申请一块内存,调用构造方法进行初始化操作,第二是分配一个指针指向这块内存。这两个操作谁在前面,谁在后面JVM并不会管它。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。

线程A开始创建MyObject的实例,此时线程B调用了getInstance()方法,首先判断MyObject是否为null。假设A已经把MyObject指向了那块内存,只是还没有调用构造方法,因此B检测到MyObject不为null,于是直接把MyObject返回了——问题出现了,尽管MyObject不为null,但它并没有构造完成结束。此时,如果B在A将MyObject构造完成之前就是用了这个实例,程序就会出现错误了!(其实在private static MyObject myObject;  改为   private volatile static MyObject myObject;  就不会发生这样的结果了。被volatile修饰的写变量不能和之前的读写代码调整,这里我们当做这个关键字不存在,以后会有专门的篇幅去详细讲解这个关键字的,这个关键字的坑有许多,我们慢慢踩)

那我们到底的咋整啊?

public class MyObject {
private static MyObject myObject;
private MyObject(){ }
public static MyObject getInstance(){ if (myObject == null) {
MyObject my;
synchronized (MyObject.class) {
my = myObject;
if (my == null) {
synchronized (MyObject.class) {
if (my == null) {
my = new MyObject();
}
}
myObject = my;
}
}
}
return myObject;
}
}

  

我们在第一个同步块里面创建一个临时变量,然后使用这个临时变量进行对象的创建,并且在最后把myObject指针临时变量的内存空间。写出这种代码基于以下思想,即synchronized会起到一个代码屏蔽的作用,同步块里面的代码和外部的代码没有联系。因此,在外部的同步块里面对临时变量my进行操作并不影响myObject,所以外部类在myObject=my;之前检测myObject的时候,结果myObject依然是null。

由于同步块的释放保证在此之前——也就是同步块里面——的操作必须完成,但是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器完全可以把myObject=my;这句移到内部同步块里面执行。又错了。

4、内部类实现方式

  

public class MyObject_inner {
private static class MyObjectHandler{
private static MyObject_inner myObject_inner = new MyObject_inner();
}
private MyObject_inner(){}
public static MyObject_inner getInstance(){
return MyObjectHandler.myObject_inner;
}
}

在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为Myobject_inner没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载MyObjectHandler类,这个类有一个static的MyObject_inne实例,因此需要调用MyObject_inne的构造方法,然后getInstance()将把这个内部类的myobject_inner返回给使用者。由于这个myobject_inner是static的,因此并不会构造多次。

由于MyObjectHandler是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。

但是这种情况完全是对的么?假如遇到序列化的对象呢?会是什么样的结果?

5、序列化与反序列化的单例模式的实现

静态内部类可以达到线程安全的问题,但是如果遇到序列化对象的时候,使用默认的方式运行得到的结果还是多例的。

具体是为什么在序列化的时候不是单例的,本人我掌握的不太好,后续会把其中涉及到的知识补充之后,在完善此篇文章。

序列化会通过反射调用无参数的构造方法创建一个新的对象。解决的方式就是在implements Serializable这个方法里面加上这段代码

  

import java.io.Serializable;

public class MyObject_inner implements Serializable{
private static final long seriaVersionUID = 8899L;
private static class MyObjectHandler{
private static MyObject_inner myObject_inner = new MyObject_inner();
}
private MyObject_inner(){}
public static MyObject_inner getInstance(){
return MyObjectHandler.myObject_inner;
} private Object readResolve(){
return MyObjectHandler.myObject_inner;
}
}

  

6、静态代码块实现单例模式

  

public class MyObject_inner {
private static MyObject_inner instance = null;
private MyObject_inner(){}
static {
instance = new MyObject_inner();
}
public static MyObject_inner getInstance(){
return instance;
}
}

  

7、枚举方法实现单例模式

  

public class MyObject_enum {
public enum EnumSingleton{
Instance;
private MyObject_enum instance;
EnumSingleton(){
instance = new MyObject_enum();
}
public MyObject_enum getInstance(){
return instance;
}
}
}

获取资源的方式很简单,只要 EnumSingleton.INSTANCE.getInstance() 即可获得所要实例。下面我们来看看单例是如何被保证的:

首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。枚举也提供了序列化机制。所以单元素的枚举类型已经成为了实现单例模式的最佳方法

  

这篇文章到此暂时先告一段落,里面有一点设计的序列化与反序列化实现单例模式我以后会在继续的更新补充进去的。 欢迎各位博友批评指正

  
 

  

  

  

JAVA_多线程_单例模式的更多相关文章

  1. java_设计模式_单例模式_Singleton Pattern(2016-08-04)

    概念: 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例. 适用场景: 在计算机系统中,线程池.缓存.日志对象.对话框.打印机.显卡的驱动程序对象常被设计成单例.这些应用都或多或 ...

  2. java多线程与单例模式(Singleton)不得不说的故事

    转发自:http://blog.csdn.net/ligang7560/article/details/50890282 单例模式的多种实现方式 我们都知道单例模式有几种常用的写法: - 饿汉模式 - ...

  3. iOS开发多线程篇—单例模式(ARC)

    iOS开发多线程篇—单例模式(ARC) 一.简单说明: 设计模式:多年软件开发,总结出来的一套经验.方法和工具 java中有23种设计模式,在ios中最常用的是单例模式和代理模式. 二.单例模式说明 ...

  4. Java多线程_生产者消费者模式2

    在我的上一条博客中,已经介绍到了多线程的经典案列——生产者消费者模式,但是在上篇中用的是传统的麻烦的非阻塞队列实现的.在这篇博客中我将介绍另一种方式就是:用阻塞队列完成生产者消费者模式,可以使用多种阻 ...

  5. 多线程安全单例模式学习代码 c++11

    // Singleton.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <mutex> #include & ...

  6. python的单例模式--解决多线程的单例模式失效

    单例模式 单例模式(Singleton Pattern) 是一种常用的软件设计模式,主要目的是确保某一个类只有一个实例存在.希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场 比如,某个 ...

  7. java多线程环境单例模式实现详解

    Abstract 在开发中,如果某个实例的创建需要消耗很多系统资源,那么我们通常会使用惰性加载机制,也就是说只有当使用到这个实例的时候才会创建这个实例,这个好处在单例模式中得到了广泛应用.这个机制在s ...

  8. java设计模式_单例模式

    懒汉式 非线程安全 特点:Lazy 初始化.非多线程安全.易实现 描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程.因为没有加锁 synchronized,所以严格意义上它并不算单 ...

  9. java 多线程,单例模式类(创建对象)最优写法

    单例模式 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一.这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式. 这种模式涉及到一个单一的类,该类负责创 ...

随机推荐

  1. 原生JSdom节点相关(非原创)

    节点属性 Node.nodeName //返回节点名称,只读 Node.nodeType //返回节点类型的常数值,只读 Node.nodeValue //返回Text或Comment节点的文本值,只 ...

  2. 【无旋式treap】例题

    [bzoj3223]文艺平衡树 Description 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[ ...

  3. tensorflow max_pool(最大池化)应用

    1.最大池化 max pooling是CNN当中的最大值池化操作,其实用法和卷积很类似. tf.nn.max_pool(value, ksize, strides, padding, name=Non ...

  4. (转)Java compiler level does not match解决方法

    背景:工作中导入以前的项目,导出报Java compiler level does not match the versionof the installed Java project facet. ...

  5. (转)每天一个linux命令(8):cp 命令,复制文件和文件夹

    场景:自动部署脚本中为了部署方便,将配置文件放在服务器端,每次部署都使用服务端的配置文件覆盖上传上去的配置文件. cp命令用来复制文件或者目录,是Linux系统中最常用的命令之一. 一般情况下,she ...

  6. js常用内置对象及方法

    在js中万物皆对象:字符串,数组,数值,函数...... 内置对象都有自己的属性和方法,访问方法如下: 对象名.属性名称: 对象名.方法名称 1.Array数组对象 unshift( )    数组开 ...

  7. Unity3D 中材质球(Material)预制体打包成AB(AssetBundle)出现材质丢失问题的解决方案

    关于Unity3D中打AB(AssetBundle)资源包,默认是不连Shader一同打包进去的,所以得自行添加,添加方式比较简单,不需要往项目中添加Shader,只是点选一下就可以了,具体实现方式如 ...

  8. Mybatis框架分析

    摘要 本篇文章只是个人阅读mybatis源码总结的经验或者个人理解mybatis的基本轮廓,作为抛砖引玉的功能,希望对你有帮助,如果需要深入了解细节还需亲自去阅读源码. mybatis基本架构 myb ...

  9. CentOS6.3 下安装codeblocks

    本人用的系统是centos6.3(虚拟机) 需要预先安装gcc编译器(参考:http://www.cnblogs.com/magialmoon/archive/2013/05/05/3061108.h ...

  10. AVR开发 Arduino方法(一) 端口子系统

    Arduino UNO R3使用的主处理器ATMega328P上有3个8位的输入/输出端口,它们分别是PB,PC和PD.Arduino IDE提供的Blink示例可以帮助我们了解端口的数字输出功能: ...