`

java多线程模式(一)

阅读更多
1.Immutable Object (不可变对象) 模式

多线程共享变量的情况下,为了保证数据一致性,往往需要对这些变量的访问进行加锁。而锁本身又会带来一些问题和开销。Immutable Object模式使得我们可以在不使用锁的情况下,既保证共享变量访问的线程安全,又能避免引入锁可能带来的问题和开销。

Immutable Object模式简介

多线程环境中,一个对象常常会被多个线程共享。这种情况下,如果存在多个线程并发地修改该对象的状态或者一个线程读取该对象的状态而另外一个线程试图修改该对象的状态,我们不得不做一些同步访问控制以保证数据一致性。而这些同步访问控制,如显式锁和CAS操作,会带来额外的开销和问题,如上下文切换、等待时间和ABA问题等。Immutable Object模式的意图是通过使用对外可见的状态不可变的对象(即Immutable Object),使得被共享对象“天生”具有线程安全性,而无需额外的同步访问控制。从而既保证了数据一致性,又避免了同步访问控制所产生的额外开销和问题,也简化了编程。

所谓状态不可变的对象,即对象一经创建其对外可见的状态就保持不变,例如Java中的String和Integer。这点固然容易理解,但这还不足以指导我们在实际工作中运用Immutable Object模式。下面我们看一个典型应用场景,这不仅有助于我们理解它,也有助于在实际的环境中运用它。

一个车辆管理系统要对车辆的位置信息进行跟踪,我们可以对车辆的位置信息建立如清单1所示的模型。

清单 1. 状态可变的位置信息模型(非线程安全)

public class Location {

private double x;
private double y;

public Location(double x, double y) {
this.x = x;
this.y = y;
}

public double getX() {
return x;
}

public double getY() {
return y;
}

public void setXY(double x, double y) {
this.x = x;
this.y = y;
}
}
当系统接收到新的车辆坐标数据时,需要调用Location的setXY方法来更新位置信息。显然,清单1中setXY是非线程安全的,因为对坐标数据x和y的写操作不是一个原子操作。setXY被调用时,如果在x写入完毕,而y开始写之前有其它线程来读取位置信息,则该线程可能读到一个被追踪车辆根本不曾经过的位置。为了使setXY方法具备线程安全性,我们需要借助锁进行访问控制。虽然被追踪车辆的位置信息总是在变化,但是我们也可以将位置信息建模为状态不可变的对象如清单2所示。

清单 2. 状态不可变的位置信息模型

public final class Location {
public final double x;
public final double y;

public Location(double x, double y) {
this.x = x;
this.y = y;
}
}
使用状态不可变的位置信息模型时,如果车辆的位置发生变动,则更新车辆的位置信息是通过替换整个表示位置信息的对象(即Location实例)来实现的。如清单3所示。

清单 3. 在使用不可变对象的情况下更新车辆的位置信息

public class VehicleTracker {

private Map<String, Location> locMap
= new ConcurrentHashMap();

public void updateLocation(String vehicleId, Location newLocation) {
locMap.put(vehicleId, newLocation);
}

}
因此,所谓状态不可变的对象并非指被建模的现实世界实体的状态不可变,而是我们在建模的时候的一种决策:现实世界实体的状态总是在变化的,但我们可以用状态不可变的对象来对这些实体进行建模。
Immutable Object模式的架构

Immutable Object模式的主要参与者有以下几种。其类图如图1所示。

图 1. Immutable Object模式的类图





ImmutableClass:负责存储一组不可变状态的类。该类不对外暴露任何可以修改其状态的方法,其主要方法及职责如下:
getStateX,getStateN:这些getter方法返回该类所维护的状态相关变量的值。这些变量在对象实例化时通过其构造器的参数获得值。

getStateSnapshot:返回该类维护的一组状态的快照。

Manipulator:负责维护ImmutableClass所建模的现实世界实体状态的变更。当相应的现实世界实体状态变更时,该类负责生成新的ImmutableClass的实例,以反映新的状态。
changeStateTo:根据新的状态值生成新的ImmutableClass的实例。

不可变对象的使用主要包括以下几种类型:

获取单个状态的值:调用不可变对象的相关getter方法即可实现。

获取一组状态的快照:不可变对象可以提供一个getter方法,该方法需要对其返回值做防御性拷贝或者返回一个只读的对象,以避免其状态对外泄露而被改变。

生成新的不可变对象实例:当被建模对象的状态发生变化的时候,创建新的不可变对象实例来反映这种变化。

Immutable Object模式的典型交互场景如图2所示:

图 2. Immutable Object模式的序列图


1~4、客户端代码获取ImmutableClass的各个状态值。

5、客户端代码调用Manipulator的changeStateTo方法来更新应用的状态。

6、Manipulator创建新的ImmutableClass实例以反映应用的新状态。

7~9、客户端代码获取新的ImmutableClass实例的状态快照。

一个严格意义上不可变对象要满足以下所有条件:

1) 类本身使用final修饰:防止其子类改变其定义的行为;

2) 所有字段都是用final修饰的:使用final修饰不仅仅是从语义上说明被修饰字段的引用不可改变。更重要的是这个语义在多线程环境下由JMM(Java Memory Model)保证了被修饰字段的所引用对象的初始化安全,即final修饰的字段在其它线程可见时,它必定是初始化完成的。相反,非final修饰的字段由于缺少这种保证,可能导致一个线程“看到”一个字段的时候,它还未被初始化完成,从而可能导致一些不可预料的结果。

3) 在对象的创建过程中,this关键字没有泄露给其它类:防止其它类(如该类的匿名内部类)在对象创建过程中修改其状态。

4) 任何字段,若其引用了其它状态可变的对象(如集合、数组等),则这些字段必须是private修饰的,并且这些字段值不能对外暴露。若有相关方法要返回这些字段值,应该进行防御性拷贝(Defensive Copy)。


Immutable Object模式实战案例

某彩信网关系统在处理由增值业务提供商(VASP,Value-Added Service Provider)下发给手机终端用户的彩信消息时,需要根据彩信接收方号码的前缀(如1381234)选择对应的彩信中心(MMSC,Multimedia Messaging Service Center),然后转发消息给选中的彩信中心,由其负责对接电信网络将彩信消息下发给手机终端用户。彩信中心相对于彩信网关系统而言,它是一个独立的部件,二者通过网络进行交互。这个选择彩信中心的过程,我们称之为路由(Routing)。而手机号前缀和彩信中心的这种对应关系,被称为路由表。路由表在软件运维过程中可能发生变化。例如,业务扩容带来的新增彩信中心、为某个号码前缀指定新的彩信中心等。虽然路由表在该系统中是由多线程共享的数据,但是这些数据的变化频率并不高。因此,即使是为了保证线程安全,我们也不希望对这些数据的访问进行加锁等并发访问控制,以免产生不必要的开销和问题。这时,Immutable Object模式就派上用场了。

维护路由表可以被建模为一个不可变对象,如清单4所示。

清单 4. 使用不可变对象维护路由表


public class MMSCRouter {
    private static volatile MMSCRouter instance =new MMSCRouter();      //用volatile修饰保证多线程环境下,该变量的可见性
    private final Map<String,MMSCInfo> routeMap;      //维护手机号码到彩票中心的映射关系
    public MMSCRouter(){
        this.routeMap=MMSCRouter.retrieveRouteMapFromDB();
    }
    private static Map<String,MMSCInfo>  retrieveRouteMapFromDB(){
        Map<String,MMSCInfo> map =new HashMap<String,MMSCInfo>();
        /*此处省略与设计模式无关的业务逻辑代码*/
        return map;
    }
    public static MMSCRouter getInstance(){
        return instance;
    }
    /**
     * 根据手机号码前缀获取对应的彩信中心信息
     * @param msisdnPrefix  手机号码前缀
     * @return 彩信中心信息
     */
     public MMSCInfo getMMSC(String msisdnPrefix){
         return  routeMap.get(msisdnPrefix);
     }
    /**
     * 将当前MMSCRouter的实例更新为指定的新实例
     * @param newInstance 新的MMSCRouter实例
     */
     public static void setInstance(MMSCRouter newInstance){
           instance=newInstance;
     }
    private static Map<String,MMSCInfo> deepCopy(Map<String,MMSCInfo> map){
       Map<String,MMSCInfo> result=new HashMap<String,MMSCInfo>();
       for(String key:map.keySet()){
           result.put(key, new MMSCInfo(map.get(key)));
       }
        return result;
    }
    public Map<String,MMSCInfo>  getRouteMap(){
        return Collections.unmodifiableMap(deepCopy(routeMap));   //做防御性拷贝
    }
}

而彩信中心的相关数据,如彩信中心设备编号、URL、支持的最大附件尺寸也被建模为一个不可变对象。如清单5所示。

清单 5. 使用不可变对象表示彩信中心信息



public   final  class MMSCInfo {
    /**
     * 设备编号
     */
    private final String deviceID;
    /**
     * 彩信中心URL
     */
    private final String url;
    /**
     * 该彩信中心允许的最大附件大小
     */
    private final int maxAttachmentSizeInBytes;
    public MMSCInfo(String deviceID, String url, int maxAttachmentSizeInBytes) {
        this.deviceID = deviceID;
        this.url = url;
        this.maxAttachmentSizeInBytes = maxAttachmentSizeInBytes;
    }
    public MMSCInfo(MMSCInfo prototype){
         this.deviceID=prototype.deviceID;
         this.url=prototype.url;
         this.maxAttachmentSizeInBytes=prototype.maxAttachmentSizeInBytes;
    }
    public String getDeviceID() {
        return deviceID;
    }
    public String getUrl() {
        return url;
    }
    public int getMaxAttachmentSizeInBytes() {
        return maxAttachmentSizeInBytes;
    }
}

彩信中心信息变更的频率也同样不高。因此,当彩信网关系统通过网络(Socket连接)被通知到这种彩信中心信息本身或者路由表变更时,网关系统会重新生成新的MMSCInfo和MMSCRouter来反映这种变更。如清单6所示。

清单 6. 处理彩信中心、路由表的变更

public class OMCAgent extends Thread {

      public void run(){
          boolean isTableModificationMsg=false;
          String updatedTableName=null;
          while(true){
              //省略其它代码
              /*
              * 从与OMC连接的Socket中读取消息并进行解析,
              * 解析到数据表更新消息后,重置MMSCRouter实例。
              */
              if(isTableModificationMsg){
                  if("MMSCInfo".equals(updatedTableName)){
                      MMSCRouter.setInstance(new MMSCRouter());
                  }
              }
              //省略其它代码
          }
      }
}


上述代码会调用MMSCRouter的setInstance方法来替换MMSCRouter的实例为新创建的实例。而新创建的MMSCRouter实例通过其构造器会生成多个新的MMSCInfo的实例。

本案例中,MMSCInfo是一个严格意义上的不可变对象。虽然MMSCRouter对外提供了setInstance方法用于改变其静态字段instance的值,但它仍然可视作一个等效的不可变对象。这是因为,setInstance方法仅仅是改变instance变量指向的对象,而instance变量采用volatile修饰保证了其在多线程之间的内存可见性,这意味着setInstance对instance变量的改变无需加锁也能保证线程安全。而其它代码在调用MMSCRouter的相关方法获取路由信息时也无需加锁。

从图1的类图上看,OMCAgent类(见清单6)是一个Manipulator参与者实例,而MMSCInfo、MMSCRouter是一个ImmutableClass参与者实例。通过使用不可变对象,我们既可以应对路由表、彩信中心这些不是非常频繁的变更,又可以使系统中使用路由表的代码免于并发访问控制的开销和问题。


Immutable Object模式的评价与实现考量

不可变对象具有天生的线程安全性,多个线程共享一个不可变对象的时候无需使用额外的并发访问控制,这使得我们可以避免显式锁(Explicit Lock)等并发访问控制的开销和问题,简化了多线程编程。

Immutable Object模式特别适用于以下场景。

被建模对象的状态变化不频繁:正如本文案例所展示的,这种场景下可以设置一个专门的线程(Manipulator参与者所在的线程)用于在被建模对象状态变化时创建新的不可变对象。而其它线程则只是读取不可变对象的状态。此场景下的一个小技巧是Manipulator对不可变对象的引用采用volatile关键字修饰,既可以避免使用显式锁(如synchronized),又可以保证多线程间的内存可见性。

同时对一组相关的数据进行写操作,因此需要保证原子性:此场景为了保证操作的原子性,通常的做法是使用显式锁。但若采用Immutable Object模式,将这一组相关的数据“组合”成一个不可变对象,则对这一组数据的操作就可以无需加显式锁也能保证原子性,既简化了编程,又提高了代码运行效率。本文开头所举的车辆位置跟踪的例子正是这种场景。

使用某个对象作为安全的HashMap的Key:我们知道,一个对象作为HashMap的Key被“放入”HashMap之后,若该对象状态变化导致了其Hash Code的变化,则会导致后面在用同样的对象作为Key去get的时候无法获取关联的值,尽管该HashMap中的确存在以该对象为Key的条目。相反,由于不可变对象的状态不变,因此其Hash Code也不变。这使得不可变对象非常适于用作HashMap的Key。

Immutable Object模式实现时需要注意以下几个问题:

被建模对象的状态变更比较频繁:此时也不见得不能使用Immutable Object模式。只是这意味着频繁创建新的不可变对象,因此会增加GC(Garbage Collection)的负担和CPU消耗,我们需要综合考虑:被建模对象的规模、代码目标运行环境的JVM内存分配情况、系统对吞吐率和响应性的要求。若这几个方面因素综合考虑都能满足要求,那么使用不可变对象建模也未尝不可。

使用等效或者近似的不可变对象:有时创建严格意义上的不可变对象比较难,但是尽量向严格意义上的不可变对象靠拢也有利于发挥不可变对象的好处。

防御性拷贝:如果不可变对象本身包含一些状态需要对外暴露,而相应的字段本身又是可变的(如HashMap),那么在返回这些字段的方法还是需要做防御性拷贝,以避免外部代码修改了其内部状态。正如清单4的代码中的getRouteMap方法所展示的那样。


本文转载自http://www.infoq.com/cn/articles/java-multithreaded-programming-mode-immutable-object/



2.Two-phase Termination (两阶段终止)模式


Two-Phase Termination Pattern,指的就是当希望结束一个线程的时候,送出一个终止请求,但是不会马上停止,做一些刷新工作。进入“终止处理中”,在该状态下,不会进行该线程日常工作任务的操作,而是进行一些终止操作。

   这个方式所考虑的因素如下:

  1,必须要考虑到使得该线程能够安全的结束,Thread中的stop会有问题的,因为它会不管线程执行到哪里,都会马上停止,不能保证安全的结束。

  2,一定能够进行正常的终止处理,在java中,这点可以使用finally来实现

  3,能够高响应的终止,收到终止后,当线程在wait或者sleep或者join的时候,不用等到时间到才终止,而是马上中断线程的这些状态,进而进行终止操作。

   当一个线程正在执行周期性的工作时候,在“作业中”发了停止执行绪的请求,此时该线程不能马上离开停止,而应该先做完本次周期内部的工作,然后进入“善后阶段”完成一些善后的工作,所谓的两阶段终止,即中止“运作阶段”,并完成“善后阶段”,完整的完成执行绪的工作。









解释:
  1,利用Volatile的原因是,这个字段可能会被多个线程所使用,进行修改,为了保护该字段,则可以利用同步方法或者同步代码块来保护,或者利用Volatile。用Volatile修饰的字段,强制了该成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
  2,这里运用了标识和中断状态来终止线程,之所以不单独用一个。原因是如果仅仅利用标识,无法使得那些处于wait、sleep或者join中的线程马上停止,响应性就会很差。加入了interrupt后,就可以立刻使得这些状态下的线程中断。如果仅仅利用interrupt,由于interrupt仅仅对于wait,sleep或join处进行抛出异常,如果工作代码执行在catch里,捕获了InterruptedException后,则此时interrupt就不起作用了。
本文转载自http://computerdragon.blog.51cto.com/6235984/1206548




3.Producer-Consumer(生产者/消费者)模式


实际上,准确说应该是“生产者-消费者-仓储”模型,离开了仓储,生产者消费者模型就显得没有说服力了。
对于此模型,应该明确一下几点:
1、生产者仅仅在仓储未满时候生产,仓满则停止生产。
2、消费者仅仅在仓储有产品时候才能消费,仓空则等待。
3、当消费者发现仓储没产品可消费时候会通知生产者生产。
4、生产者在生产出可消费产品时候,应该通知等待的消费者去消费。




















  • 大小: 28.1 KB
  • 大小: 46 KB
  • 大小: 14.4 KB
  • 大小: 20.3 KB
  • 大小: 24.9 KB
  • 大小: 7.4 KB
  • 大小: 18.7 KB
  • 大小: 18.4 KB
  • 大小: 11.3 KB
分享到:
评论

相关推荐

    Java多线程设计模式上传文件

    Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式...

    java多线程设计模式详解(PDF及源码)

    (注意,本资源附带书中源代码可供参考) 多线程与并发处理是程序设计好坏优劣的重要课题,本书通过浅显易懂的文字与实例来介绍Java线程相关的设计模式概念,并且通过实际的Java程序范例和 UML图示来一一解说,书中...

    Java多线程设计模式_清晰完整PDF版 Java多线程设计模式源代码

    总之,Java多线程设计模式是每个Java开发者必备的技能之一。深入学习并熟练运用这些模式,将有助于你编写出更高效、稳定和易于扩展的多线程应用程序。这个PDF版教程和源代码集合是你学习多线程设计模式的理想资源,...

    JAVA多线程设计模式.pdf 下载

    标题和描述均指向了一个关于Java多线程设计模式的PDF文档的下载链接,这暗示了文档的主要内容将围绕Java中的多线程编程及其设计模式展开。在Java领域,多线程是一个核心概念,它允许程序执行多个任务同时进行,极大...

    java多线程设计模式_java_设计模式_多线程_多线程课题_

    Java多线程设计模式是Java开发中的核心概念,它涉及到如何高效、安全地在多个执行线程之间共享资源和协调任务。设计模式是解决特定问题的成熟方案,它们是编程经验的结晶,可以帮助开发者在面临多线程挑战时快速找到...

    JAVA多线程模式高清版+DEMO

    这个压缩包文件"JAVA多线程模式高清版+DEMO"显然是关于Java多线程设计模式的详细教程,很可能包含了理论讲解、代码示例以及实战DEMO。 在Java多线程编程中,了解和掌握以下知识点至关重要: 1. **线程的创建与启动...

    java 多线程编程实战指南(核心 + 设计模式 完整版)

    《Java多线程编程实战指南》这本书深入浅出地讲解了Java多线程的核心概念和实战技巧,分为核心篇和设计模式篇,旨在帮助开发者掌握并应用多线程技术。 1. **线程基础** - **线程的创建**:Java提供了两种创建线程...

    Java多线程设计模式(带源码)

    Java多线程设计模式是Java开发中的重要领域,它涉及到并发编程、系统性能优化以及程序的稳定性。在Java中,多线程允许程序同时执行多个任务,极大地提升了程序的执行效率。本资源提供了详细的Java多线程设计模式的...

    java多线程设计模式详解.pdf

    从描述中可以得知,这是一本个人收集的电子书,虽然信息部分重复,但其主要内容是关于Java多线程的设计模式,以及对设计模式进行详细的解说。这本书的使用目的限定为学习使用,并且明确指出不可用于商业用途,这也...

    java多线程设计模式详解(PDF及源码).zip

    Java多线程设计模式是Java开发中的重要领域,它涉及到如何高效、安全地利用系统资源进行并发处理。在这个主题中,我们将深入探讨单线程、生产者与消费者模型以及Java中实现多线程的各种方法。 首先,单线程是程序...

    汪文君JAVA多线程编程实战(完整不加密)

    《汪文君JAVA多线程编程实战》是一本专注于Java多线程编程的实战教程,由知名讲师汪文君倾力打造。这本书旨在帮助Java开发者深入理解和熟练掌握多线程编程技术,提升软件开发的效率和质量。在Java平台中,多线程是...

    java 多线程并发实例

    在Java编程中,多线程并发是...总之,Java的多线程并发实例可以帮助我们更好地理解和实践线程控制、同步机制以及经典的设计模式,提升我们的编程能力。通过不断学习和实践,我们可以编写出高效、安全的多线程并发程序。

    JAVA多线程设计模式_中国铁道出版社_源码

    JAVA多线程设计模式_中国铁道出版社 本书浅显易懂的介绍了JAVA线程相关的设计模式,通过程序范例和UML图示来一一解说,书中代码的重要部分加了标注以使读者更加容易理解,再加上图文并茂,对于初学者还是程序设计...

    Java多线程设计模式源代码

    Java多线程设计模式是Java编程中至关重要的一个领域,它涉及到如何在并发环境中高效、稳定地执行多个任务。在Java中,多线程可以提高应用程序的响应速度和整体性能,尤其是在处理I/O密集型或计算密集型任务时。本...

    Java多线程编程实战指南 设计模式篇.rar

    本实战指南将深入探讨如何在Java多线程环境中有效地运用设计模式。 一、Java多线程基础 1. 线程创建:Java提供了两种创建线程的方式,一是通过实现Runnable接口,二是继承Thread类。两者各有优缺点,Runnable适合...

    java多线程设计模式.pdf

    java多线程设计模式.pdf java多线程设计模式.pd

    java多线程设计模式详解(pdf版)

    java多线程设计模式,作者是:结城 浩,由 博硕文化 译。2005年4月,由中国铁道出版社出版。内附带部分源代码。

    java 多线程设计模式 进程详解

    《JAVA多线程设计模式》PDF 下载 《Java线程 高清晰中文第二版》中文第二版(PDF) 前言 第一章 线程简介 Java术语 线程概述 为什么要使用线程? 总结 第二章 Java线程API 通过Thread类创建线程 使用Runable接口...

    Java多线程编程

    Java多线程编程是Java开发中的重要组成部分,它允许程序同时执行多个任务,极大地提高了程序的效率和响应性。在Java中,多线程主要通过`Thread`类和并发工具来实现,接下来我们将深入探讨这些关键知识点。 1. **...

Global site tag (gtag.js) - Google Analytics