优雅设计封装基于Okhttp3的网络框架:多线程单例模式优化 及 volatile构建者模式使用解析

Posted 鸽一门

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了优雅设计封装基于Okhttp3的网络框架:多线程单例模式优化 及 volatile构建者模式使用解析相关的知识,希望对你有一定的参考价值。

关于多线程下载功能,前四篇博文所讲解内容已经实现,接下来需要对代码进行优化。开发一个新功能并不复杂,难的是考虑到代码的扩展性和解耦性,后续需要进行的bug修复、完善功能等方面。此篇内容主要讲解代码优化,将从线程优化、单例优化、设计优化这三个方面进行讲解。

此篇内容将涉及到以下知识:

  • 线程优化及Linux系统中线程调度介绍
  • android中常用的5种单例模式解析
  • volatile关键字底层原理及注意事项
  • 构建者模式介绍及使用

(建议阅读此篇文章之前,需理解前两篇文章的讲解,此系列文章是环环相扣,不可缺一,链接如下:)
优雅设计封装基于Okhttp3的网络框架(一):Http网络协议与Okhttp3解析
优雅设计封装基于Okhttp3的网络框架(二):多线程下载功能原理设计 及 简单实现
优雅设计封装基于Okhttp3的网络框架(三):多线程下载功能核心实现 及 线程池、队列机制解析
优雅设计封装基于Okhttp3的网络框架(四):多线程下载添加数据库支持(greenDao)及 进度更新


一. 线程优化

1. 根本问题

在Java语言中本身存在线程的优先级,是否直接操作设置这些线程的优先级就可以控制Android程序中的线程?

并非如此,实际上Java提供的一些线程优先级设置对于Android而言并非起太大作用,因为Android系统是基于Linux,而Linux系统对于线程调度管理有一套自己的法则。


2. Linux的线程调度法则

在Linux中使用nice value(以下成为nice值)来设定一个进程的优先级,系统任务调度器根据nice值合理安排调度。在Android系统中,也是采用此值来进行优化,特点如下:

  • nice的取值范围为-20到19。
  • 通常情况下,nice的默认值为0。
  • nice的值越大,进程的优先级就越低,获得CPU调用的机会越少,nice值越小,进程的优先级则越高,获得CPU调用的机会越多。
  • 一个nice值为-20的进程优先级最高,nice值为19的进程优先级最低。

以上便是 nice值的特点介绍,那么如何在代码中进行使用?首先来查看一个系统类AsyncTask,在它的内部实现中就使用到了相关代码:

Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

此行代码作用相关于将该线程的优先级设置成THREAD_PRIORITY_BACKGROUND,继续查看其优先级别:

查看源码可知优先级别为10,将它设置为后台线程的好处是(查看注释):减少系统调度时间,UI线程会得到更多响应时间。


3. DownloadRunnable中的run方法优化

经过以上讲解后,将设置线程优先级至后台线程这行代码添加至run方法的第一行即可。(此行代码设置虽简单,但背后逻辑操作紧密联系Linux线程调度,有兴趣者后续可多了解)

    @Override
    public void run()    

//设置线程优先级别为后台线程,为了 减少系统调度时间,使UI线程会得到更多响应时间 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        ......
        



二. 单例模式优化

此点将对于Android中使用的单例模式进行优化,单例模式使用的场景往往是:程序中的某些对象的创建和消耗是比较耗费资源的,此时可以考虑将其对象设置为单例模式。

该模式算是设计模式中最常用、基本的一种,在目前已实现的网络框架编码中已多次使用。按照Java语言详细划分,单例模式可分为7种,但在Android中常用的只有以下5种。

1. 五大种类

(1)饿汉式

【饿汉式】

public class DownloadManager 
    private static DownloadManager sManager = new DownloadManager();

    private DownloadManager() 
    

    public static DownloadManager getInstance() 
        return sManager;
    

实现方式

  • 将该类的对象设置为静态成员变量;
  • 类的构造方法设置为私有,意味着外界无法创建该对象;
  • 直接创建对象赋值给静态成员变量。

创建时机

当此类加载进来的时候,该类的对象已经创建成功了。


(2)懒汉式

【懒汉式】

public class DownloadManager 
    private static DownloadManager sManager;

    private DownloadManager() 
    

    public static DownloadManager getInstance() 
        if (sManager == null) 
               sManager = new DownloadManager();
            
        
        return sManager;
    

实现方式

懒汉式的单例模式算是对饿汉式的优化:

  • 将该类的对象设置为静态成员变量;
  • 类的构造方法设置为私有,意味着外界无法创建该对象;
  • 在对外提供的public静态getInstance方法进行对象创建操作。

创建时机

当需要该类的对象时,调用此类暴露出来的getInstance方法,才会去创建对象。


(3)Double Check

问题解析

单例模式的每一种方式衍生可以看作是一次次的完善。在懒汉式中的getInstance方法创建类对象时,若遇到多线程的情况,便会出问题,即多个线程同时在操纵创建这行代码,这样对象并非是单例模式了。

改善代码

最简单的修改方法就是给getInstance方法加上synchronized 关键字,锁住此方法,但是锁住这整个方法消耗颇多,并非最佳,实际上只需锁住创建对象这行代码即可。

于是,有了如下Double Check方式:

【Double Check】

public class DownloadManager 
    private static DownloadManager sManager;

    private DownloadManager() 
    

    public static DownloadManager getInstance() 
        if (sManager == null) 
            synchronized (DownloadManager.class) 
                if (sManager == null) 
                    sManager = new DownloadManager();
                
            
        
        return sManager;
    

实现方式

  • 将该类的对象设置为静态成员变量;
  • 类的构造方法设置为私有,意味着外界无法创建该对象
  • 在对外提供的public静态getInstance方法中判断,当对象为空时,使用synchronized 关键字,锁住DownloadManager.class,在其代码块中再加一个判断:若对象为空时创建对象。

创建时机

当需要该类的对象时,调用此类暴露出来的getInstance方法,才会去创建对象。

优缺点

优点:只会在第一次创建对象的时候去加锁,之后无需加锁直接返回已存在对象,节省了不必要的性能消耗。

缺点:其实这种方法也不能保证程序的完整性,它在某些虚拟机上运行会出现空指针问题,背后原因比较复杂,简单而言是因为创建对象这行代码并非原子性操作,虚拟机在编译字节码时将这行代码分为几步操作。

举个例子,当A线程访问对象为空,获取到锁,在其中进行对象创建过程中,线程B访问该方法,判断对象不为空,获取到此时返回的对象进行其它操作。注意此时线程B获取的对象仅是不为空,但它还未初始化完成,所以会出现空指针问题。原因就是字节码在执行时并非是原子性操作。(这里稍作了解即可,后续volatile介绍会继续讲解)

虽然空指针异常的发生几率不大,但毕竟是一个显示问题,存在太大隐患,一旦发生异常,进行排除问题都难以想到此层面。此种方式不太推荐。


(4)静态内部类

问题解析

面对最后 Double Check而言的问题,又有一种方式出现来解决此问题 —— 静态内部类它可以起到延迟加载的作用,并且能保证创建对象的完整性。

【静态内部类】

    public static class Holder 

        private static DownloadManager sManager = new DownloadManager();

        public static DownloadManager getInstance() 
            return sManager;
        
    
//外界访问

DownloadManager.Holder.getInstance();

实现方式

  • 编写访问权限为public 的静态内部类;
  • 在其内部类中将对象设置为私有的静态成员变量;
  • 在其内部类中对外提供 public 获取对象的静态方法getInstance返回对象。

创建时机

调用该静态内部类的getInstance时才会初始化对象。

原理分析

虽然它只是一个静态内部类,但虚拟机在编译class时会单独将它放到一个文件。看起来跟饿汉式似乎有点相像,但是当虚拟机加载此类时,并不会初始化该对象,只有调用该静态内部类的getInstance时才会初始化对象,起到了延迟加载作用,保证了创建对象的操作原子性。

此种方式较为推荐。


(5)枚举

其实枚举实现单例模式这种方式在Java语言中较为推崇,枚举在虚拟机层面已经保证了创建的唯一性,但是在Android系统中并不推荐,因为枚举会导致占用内存过多,可以尝试反编译枚举的Class,会发现随着枚举定义类型的增多,它所占用的内存是成倍增长,每一个枚举类型都生成相应的Class,它的每一个成员变量都是单独一份,所以此方式并不推荐!



2. volatile 关键字解析

在上一点中介绍 Double Check的单例模式使用中,会出现空指针问题,根本原因上述已简单讲解,此点将结合 volatile 关键字,从虚拟机底层原理详细探究。

博主声明: volatile 关键字向来是多线程并发中的重点,此点讲解涉及到大量虚拟机底层相关原理知识,若想真正了解透彻,仅以此点远远不够,推荐读者能够先查看以下文章链接,这是Java虚拟机中相关部分,学习之后再来理解Double Check单例模式中的空指针异常,会更加容易。

JVM高级特性与实践(十二):高效并发时的内外存交互、三大特征(原子、可见、有序性) 与 volatile型变量特殊规则

(1)异常定位—— 空指针异常

【Double Check】

public class DownloadManager 
    private static DownloadManager sManager;

    public static DownloadManager getInstance() 
        if (sManager == null) 
            synchronized (DownloadManager.class) 
                if (sManager == null) 
                    //出错处
                    sManager = new DownloadManager();
                
            
        
        return sManager;
    

代码异常定义

出现异常来源于创建对象那行代码,看似只是一个对象创建并赋值操作,但是当虚拟机编译成字节码文件,这行代码的对象创建操作不是一个原子性操作,会生成多条字节码指令。

例子讲解

再来回顾这个例子:线程A进入到getInstance() 方法时首先判断对象为空,拿到DownloadManager的锁,准备对象创建操作。此时线程B进入该方法,该对象已经不为空了(线程A已创建该对象实例),线程B理所当然获取到对象,但是此时对象并不完整,它只是不为空,初始化阶段可能尚未完成,所以当线程B使用该对象操作时必然会出现空指针异常。

对象创建代码 分解

那行代码的对象创建操作不是一个原子性操作,通过伪码的形式可分成以下几步:

  • 1)sManager 分配内存
  • 2) sManager 调用构造方法进行初始化操作
  • 3)sManager 对象进行赋值操作,使它指向在第一步分配的内存区域

(2)根本原因——JVM中的字节码指令集的重排序

所以说一个简单的对象创建在Java虚拟机中会被划分为这3个步骤,其实这些步骤也很正常,需要注意的是Java虚拟机会对代码步骤进行重排序,即字节码指令集的重排序

例如以上步骤二可能被虚拟机重排序到最后,这样意味着步骤一结束后,对象确实不为空了,但是在步骤三初始化之前被线程B获取到对象实例,而导致空指针异常!

代码执行顺序

虚拟机执行代码的顺序并非是按照我们所写的,而是以字节码文件为准。而JVM会自动优化代码,打乱字节码执行顺序!注意:这里的顺序打乱它不会故意破坏而导致异常产生,例如代码上下之间有依赖关系,JVM不会进行重排序。


(3)问题解决 —— volatile关键字

其实以上问题在单线程中并不会出现,只会在多线程中出现,为了避免JVM中的字节码指令集重排序问题,JDK 1.5中引入了一个关键字 —— volatile,它有两个重要作用:

  • 禁止JVM进行重排序
  • 保证变量的可见性(可见性:指当一个线程修改了共享变量的值,其他能够立即得知这个修改

Java开发者应当知道当一个变量被volatile关键字修饰时,它拥有可见性,但是另外一个作用 —— 禁止JVM进行重排序,却鲜为人知。当次变量被修饰后,JVM不会打乱字节码执行顺序,而出现步骤二在最后执行的情况。

指令重排序例子再论

为了更好的理解指令重排序,再举个例子来了解,例如在以下代码定义了这三个变量:

int a = 12;
boolean flag = false;
long c = 23;

JVM在执行以上代码时会以字节码文件为准,即打乱顺序,可能先操作boolean变量赋值,然后再是int,最后long。代码原本顺序的确被打乱了,但是JVM并不会无故打乱而导致异常产生,例如以下示例:

int a = 12;
int b = 10;
int c = a+23;

JVM在进行重排序时绝对不会将int c = a+23;操作放到int a = 12;之前,在程序编译时JVM已经考虑到了这些变量之间的依赖(还有其它考虑原则),所以变量a的赋值一定在变量c之前完成,不过变量a、b的初始化顺序无法保证。


(4)先行发生原则(happens-before)

JVM在处理相关的字节码文件时,所考虑到的原则是规定好的,例如上述中变量之间的依赖,这些判断的依据就是先行发生原则,由以下几个规则组成:

  • 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作,准确地说,应该是控制流顺序。
  • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作;这里必须强调的是同一个锁,而后面是指时间上的先后顺序。
  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的后面是指时间上的先后顺序。
  • 线程启动规则(Thread Start Rule): Thread对象的start() 方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Temination Rule):线程中的所有操作都先行发生于对此线程的终止检测,可以通过Thread.join() 方法结束,Thread.isAlive() 的返回值等手段检测到线程已经终止运行。
  • 线程中断规则(Thread Interruption Rule):对线程interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrrupted() 方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成先行发生于它的finalize() 方法的开始。
  • 传递性(Transitivity):如果操作A 先行发生于操作B, 操作B 先行发生于操作C,那就可以得出操作A 先行发生于 操作C的结论。

如果编写的代码中满足以上规则,JVM不会对字节码指令集进行优化,即重排序。

最后,在《深入Java虚拟机》中从底层原理部分详细解析了volatile关键字,若要透彻了解,可查看此书(12章的3.3节)或博主写的记录博客。




三.设计优化——构建者(Build)模式

若读者对构建者模式的组成及使用不熟悉,建议先看以下博文,以下博文详细讲解了构建者模式的构造及使用,举例说明学习。

Android : Builder模式 详解及学习使用

目前有个需求,在DownloadManager管理类中,想要提供一些灵活的参数来控制此类中的线程池、执行服务对象创建,例如核心、最大线程数这些参数等。如此而言就需要在此类中设计多个set方法,而不同参数的组合可能导致set方法需求量的增加,代码冗杂,所以采用构建者模式来解决这种需求带来的问题。

1. 构建者(Build)模式

(1)定义及作用

定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
作用:减少对象创建过程中引入的多个重载构造函数、可选参数以及setter过度使用导致不必要的复杂性。

(2)组成

一般而言,Builder模式主要由四个部分组成:

  • Product :被构造的复杂对象,ConcreteBuilder 用来创建该对象的内部表示,并定义它的装配过程。
  • Builder :抽象接口,用来定义创建 Product 对象的各个组成部分的组件。
  • ConcreteBuilder : Builder接口的具体实现,可以定义多个,是实际构建Product 对象的地方,同时会提供一个返回 Product 的接口。
  • Director : Builder接口的构造者和使用者。

(3)实例讲解

这里举的例子应该算是构建者模式的进化版,精简了一些不必要的接口,若想要了解标准的模式编码,可以看第三点开头给出的链接,在此不多言。

public class User 
    private final String mName;     //必选
    private final String mGender;   //可选
    private final int mAge;         //可选
    private final String mPhone;    //可选

    public User(UserBuilder userBuilder) 
        this.mName = userBuilder.name;
        this.mGender = userBuilder.gender;
        this.mAge = userBuilder.age;
        this.mPhone = userBuilder.phone;
    

    public String getName() 
        return mName;
    

    public String getGender() 
        return mGender;
    

    public int getAge() 
        return mAge;
    

    public String getPhone() 
        return mPhone;
    


    public static class UserBuilder
        private final String name;     
        private String gender;   
        private int age;         
        private String phone;

        public UserBuilder(String name) 
            this.name = name;
        

        public UserBuilder gender(String gender)
            this.gender = gender;
            return this;
        

        public UserBuilder age(int age)
            this.age = age;
            return this;
        

        public UserBuilder phone(String phone)
            this.phone = phone;
            return this;
        

        public User build()
            return new User(this);
           
    


从以上代码可以看出这几点:

  • User类的构造函数是私有的,这意味着调用者不可直接实例化这个类。
  • User类是不可变的,其中必选的属性值都是 final 的并且在构造函数中设置;同时对所有的属性取消 setters函数,只保留 getter函数。
  • UserBuilder 的构造函数只接收必选的属性值作为参数,并且只是将必选的属性设置为 fianl来保证它们在构造函数中设置。

接下来,User类的使用方法如下:

    public User getUser()
        return new
                User.UserBuilder("gym")
                .gender("female")
                .age(20)
                .phone("12345678900")
                .build();
    

以上通过 进化的Builder模式形象的体现在User的实例化。



2. DownloadConfig(采用构建者模式)

创建一个配置类采用构建者模式对外提供参数配置:

public class DownloadConfig 
    private int coreThreadSize;
    private int maxThreadSize;
    private int localProgressThreadSize;

    private DownloadConfig(Builder builder) 
        coreThreadSize = builder.coreThreadSize == 0 ? DownloadManager.MAX_THREAD : builder.coreThreadSize;
        maxThreadSize = builder.maxThreadSize == 0 ? DownloadManager.MAX_THREAD : builder.coreThreadSize;
        localProgressThreadSize = builder.localProgressThreadSize == 0 ? DownloadManager.LOCAL_PROGRESS_SIZE : builder.localProgressThreadSize;
    

    public int getCoreThreadSize() 
        return coreThreadSize;
    

    public int getMaxThreadSize() 
        return maxThreadSize;
    

    public int getLocalProgressThreadSize() 
        return localProgressThreadSize;
    


    public static class Builder 
        private int coreThreadSize;
        private int maxThreadSize;
        private int localProgressThreadSize;

        public Builder setCoreThreadSize(int coreThreadSize) 
            this.coreThreadSize = coreThreadSize;
            return this;
        

        public Builder setMaxThreadSize(int maxThreadSize) 
            this.maxThreadSize = maxThreadSize;
            return this;
        

        public Builder setLocalProgressThreadSize(int localProgressThreadSize) 
            this.localProgressThreadSize = localProgressThreadSize;
            return this;
        


        public DownloadConfig builder() 
            return new DownloadConfig(this);
        
    


3. 代码整合

(1)DownloadManager

在完成采用构建者模式的配置类,那么DownloadManager类中线程池的创建可直接调用配置类,需要在DownloadManager类中稍作修改。

       private static ExecutorService sLocalProgressPool;

    private static ThreadPoolExecutor sThreadPool;

    public void init(DownloadConfig config) 
        sThreadPool = new ThreadPoolExecutor(config.getCoreThreadSize(), config.getMaxThreadSize(), 60, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(), new ThreadFactory() 
            private AtomicInteger mInteger = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable runnable) 
                Thread thread = new Thread(runnable, "download thread #" + mInteger.getAndIncrement());
                return thread;
            
        );

        sLocalProgressPool = Executors.newFixedThreadPool(config.getLocalProgressThreadSize());

    

(2)Application初始化

        DownloadConfig config = new DownloadConfig.Builder()
                .setCoreThreadSize(2)
                .setMaxThreadSize(4)
                .setLocalProgressThreadSize(1)
                .builder();
        DownloadManager.getInstance().init(config);

以上,这种动态的设置配置参数处理方式相较于构造方法,灵活得多,减少了大量不必要的冗杂代码,扩展性较强,可链式增添参数配置。





四. 总结

1. 本篇总结

本篇内容是对前四篇博文完成的编码工作进行的优化,需要修改的地方并不多,但是为了程序的扩展性、解耦性、线程安全性考虑,分别从线程优化、单例优化、设计优化这三个层面对代码进行优化。其实完成一个功能并不难,重要的是前期设计部分一定要思考清楚功能可行性、程序扩展性等问题,而优化工作更是必不可少,切勿全部编程完再来一次“重构”,这样开发周期会被无限拖长,代码质量并不会因为所谓的“重构”而提高,适时的时候停下来对已完成的功能进行优化。

EasyOkhttp网络框架封装源码(对应第五篇博文优化后地代码)


2. 下篇预告

到目前为止,多线程下载功能设计、编写、优化工作已经完成,但是网络框架功能并没有完成,下篇将编写的新功能还是围绕在http请求上:

  • httpHeader的接口定义和实现
  • http请求头和响应头访问编写
  • http状态码定义
  • http中的 response封装、request接口封装和实现


若有错误,虚心指教~

以上是关于优雅设计封装基于Okhttp3的网络框架:多线程单例模式优化 及 volatile构建者模式使用解析的主要内容,如果未能解决你的问题,请参考以下文章

优雅设计封装基于Okhttp3的网络框架:多线程下载添加数据库支持(greenDao)及 进度更新

优雅设计封装基于Okhttp3的网络框架:HttpHeader接口设计实现 及 ResponseRequest封装实现

Java EE 架构设计——基于okhttp3 的网络框架设计

基于Retrofit+RxJava 封装 Leopard 网络框架

游戏服务器框架分析

网络框架封装(retrofit2+rxjava2+okhttp3)