优雅设计封装基于Okhttp3的网络框架(五):多线程、单例模式优化 及 volatile、构建者模式使用解析
2017-08-01 11:45
746 查看
关于多线程下载功能,前四篇博文所讲解内容已经实现,接下来需要对代码进行优化。开发一个新功能并不复杂,难的是考虑到代码的扩展性和解耦性,后续需要进行的bug修复、完善功能等方面。此篇内容主要讲解代码优化,将从线程优化、单例优化、设计优化这三个方面进行讲解。
此篇内容将涉及到以下知识:
线程优化及Linux系统中线程调度介绍
Android中常用的5种单例模式解析
volatile关键字底层原理及注意事项
构建者模式介绍及使用
(建议阅读此篇文章之前,需理解前两篇文章的讲解,此系列文章是环环相扣,不可缺一,链接如下:)
优雅设计封装基于Okhttp3的网络框架(一):Http网络协议与Okhttp3解析
优雅设计封装基于Okhttp3的网络框架(二):多线程下载功能原理设计 及 简单实现
优雅设计封装基于Okhttp3的网络框架(三):多线程下载功能核心实现 及 线程池、队列机制解析
优雅设计封装基于Okhttp3的网络框架(四):多线程下载添加数据库支持(greenDao)及 进度更新
并非如此,实际上Java提供的一些线程优先级设置对于Android而言并非起太大作用,因为Android系统是基于Linux,而Linux系统对于线程调度管理有一套自己的法则。
nice的取值范围为-20到19。
通常情况下,nice的默认值为0。
nice的值越大,进程的优先级就越低,获得CPU调用的机会越少,nice值越小,进程的优先级则越高,获得CPU调用的机会越多。
一个nice值为-20的进程优先级最高,nice值为19的进程优先级最低。
以上便是 nice值的特点介绍,那么如何在代码中进行使用?首先来查看一个系统类AsyncTask,在它的内部实现中就使用到了相关代码:
此行代码作用相关于将该线程的优先级设置成THREAD_PRIORITY_BACKGROUND,继续查看其优先级别:
查看源码可知优先级别为10,将它设置为后台线程的好处是(查看注释):减少系统调度时间,UI线程会得到更多响应时间。
该模式算是设计模式中最常用、基本的一种,在目前已实现的网络框架编码中已多次使用。按照Java语言详细划分,单例模式可分为7种,但在Android中常用的只有以下5种。
实现方式
将该类的对象设置为静态成员变量;
类的构造方法设置为私有,意味着外界无法创建该对象;
直接创建对象赋值给静态成员变量。
创建时机
当此类加载进来的时候,该类的对象已经创建成功了。
(2)懒汉式
实现方式
懒汉式的单例模式算是对饿汉式的优化:
将该类的对象设置为静态成员变量;
类的构造方法设置为私有,意味着外界无法创建该对象;
在对外提供的public静态
创建时机
当需要该类的对象时,调用此类暴露出来的
(3)Double Check
问题解析
单例模式的每一种方式衍生可以看作是一次次的完善。在懒汉式中的
改善代码
最简单的修改方法就是给
于是,有了如下Double Check方式:
实现方式
将该类的对象设置为静态成员变量;
类的构造方法设置为私有,意味着外界无法创建该对象
在对外提供的public静态
创建时机
当需要该类的对象时,调用此类暴露出来的
优缺点
优点:只会在第一次创建对象的时候去加锁,之后无需加锁直接返回已存在对象,节省了不必要的性能消耗。
缺点:其实这种方法也不能保证程序的完整性,它在某些虚拟机上运行会出现空指针问题,背后原因比较复杂,简单而言是因为创建对象这行代码并非原子性操作,虚拟机在编译字节码时将这行代码分为几步操作。
举个例子,当A线程访问对象为空,获取到锁,在其中进行对象创建过程中,线程B访问该方法,判断对象不为空,获取到此时返回的对象进行其它操作。注意此时线程B获取的对象仅是不为空,但它还未初始化完成,所以会出现空指针问题。原因就是字节码在执行时并非是原子性操作。(这里稍作了解即可,后续volatile介绍会继续讲解)
虽然空指针异常的发生几率不大,但毕竟是一个显示问题,存在太大隐患,一旦发生异常,进行排除问题都难以想到此层面。此种方式不太推荐。
(4)静态内部类
问题解析
面对最后 Double Check而言的问题,又有一种方式出现来解决此问题 —— 静态内部类。它可以起到延迟加载的作用,并且能保证创建对象的完整性。
实现方式
编写访问权限为public 的静态内部类;
在其内部类中将对象设置为私有的静态成员变量;
在其内部类中对外提供 public 获取对象的静态方法
创建时机
调用该静态内部类的
原理分析
虽然它只是一个静态内部类,但虚拟机在编译class时会单独将它放到一个文件。看起来跟饿汉式似乎有点相像,但是当虚拟机加载此类时,并不会初始化该对象,只有调用该静态内部类的
此种方式较为推荐。
(5)枚举
其实枚举实现单例模式这种方式在Java语言中较为推崇,枚举在虚拟机层面已经保证了创建的唯一性,但是在Android系统中并不推荐,因为枚举会导致占用内存过多,可以尝试反编译枚举的Class,会发现随着枚举定义类型的增多,它所占用的内存是成倍增长,每一个枚举类型都生成相应的Class,它的每一个成员变量都是单独一份,所以此方式并不推荐!
博主声明: volatile 关键字向来是多线程并发中的重点,此点讲解涉及到大量虚拟机底层相关原理知识,若想真正了解透彻,仅以此点远远不够,推荐读者能够先查看以下文章链接,这是Java虚拟机中相关部分,学习之后再来理解Double Check单例模式中的空指针异常,会更加容易。
JVM高级特性与实践(十二):高效并发时的内外存交互、三大特征(原子、可见、有序性) 与 volatile型变量特殊规则
(1)异常定位—— 空指针异常
代码异常定义
出现异常来源于创建对象那行代码,看似只是一个对象创建并赋值操作,但是当虚拟机编译成字节码文件,这行代码的对象创建操作不是一个原子性操作,会生成多条字节码指令。
例子讲解
再来回顾这个例子:线程A进入到
对象创建代码 分解
那行代码的对象创建操作不是一个原子性操作,通过伪码的形式可分成以下几步:
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不会打乱字节码执行顺序,而出现步骤二在最后执行的情况。
指令重排序例子再论
为了更好的理解指令重排序,再举个例子来了解,例如在以下代码定义了这三个变量:
JVM在执行以上代码时会以字节码文件为准,即打乱顺序,可能先操作boolean变量赋值,然后再是int,最后long。代码原本顺序的确被打乱了,但是JVM并不会无故打乱而导致异常产生,例如以下示例:
JVM在进行重排序时绝对不会将
(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节)或博主写的记录博客。
Android : Builder模式 详解及学习使用
目前有个需求,在DownloadManager管理类中,想要提供一些灵活的参数来控制此类中的线程池、执行服务对象创建,例如核心、最大线程数这些参数等。如此而言就需要在此类中设计多个
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
作用:减少对象创建过程中引入的多个重载构造函数、可选参数以及
(2)组成
一般而言,Builder模式主要由四个部分组成:
Product :被构造的复杂对象,ConcreteBuilder 用来创建该对象的内部表示,并定义它的装配过程。
Builder :抽象接口,用来定义创建 Product 对象的各个组成部分的组件。
ConcreteBuilder : Builder接口的具体实现,可以定义多个,是实际构建Product 对象的地方,同时会提供一个返回 Product 的接口。
Director : Builder接口的构造者和使用者。
(3)实例讲解
这里举的例子应该算是构建者模式的进化版,精简了一些不必要的接口,若想要了解标准的模式编码,可以看第三点开头给出的链接,在此不多言。
从以上代码可以看出这几点:
User类的构造函数是私有的,这意味着调用者不可直接实例化这个类。
User类是不可变的,其中必选的属性值都是 final 的并且在构造函数中设置;同时对所有的属性取消 setters函数,只保留 getter函数。
UserBuilder 的构造函数只接收必选的属性值作为参数,并且只是将必选的属性设置为 fianl,来保证它们在构造函数中设置。
接下来,User类的使用方法如下:
以上通过 进化的Builder模式形象的体现在User的实例化。
在完成采用构建者模式的配置类,那么DownloadManager类中线程池的创建可直接调用配置类,需要在DownloadManager类中稍作修改。
(2)Application初始化
以上,这种动态的设置配置参数处理方式相较于构造方法,灵活得多,减少了大量不必要的冗杂代码,扩展性较强,可链式增添参数配置。
EasyOkhttp网络框架封装源码(对应第五篇博文优化后地代码)
httpHeader的接口定义和实现
http请求头和响应头访问编写
http状态码定义
http中的 response封装、request接口封装和实现
若有错误,虚心指教~
此篇内容将涉及到以下知识:
线程优化及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的网络框架(三):多线程下载功能核心实现 及 线程池、队列机制、终止线程解析
- 优雅设计封装基于Okhttp3的网络框架(一):Http网络协议与Okhttp3解析
- 优雅设计封装基于Okhttp3的网络框架(四):多线程下载添加数据库支持(greenDao)及 进度更新
- 优雅设计封装基于Okhttp3的网络框架(二):多线程下载功能原理设计 及 简单实现
- 优雅设计封装基于Okhttp3的网络框架(六):HttpHeader接口设计实现 及 Response、Request封装实现
- [置顶] 优雅设计封装基于Okhttp3的网络框架(完):原生HttpUrlConnction请求、多线程分发 及 数据转换
- 基于Retrofit、OkHttp、Gson封装通用网络框架
- Android网络请求框架----Okhttp3完全解析(2),封装框架
- 使用retrofit2和rxjava封装的网络框架RNet:(二)RNet的源码解析
- Okhttp3网络请求框架+MVP设计模式简单实战
- 基于retrofit的网络框架的终极封装(一):第一层(参数组装层)的API设计
- 安卓网络请求框架okHttp的使用与封装
- 基于Retrofit、OkHttp、Gson封装通用网络框架
- fastokhttp是基于okhttp-3.8.1.jar、okio-1.13.0.jar深度封装的OkHttp网络框架解读
- OkHttp框架从入门到放弃,解析图片使用Picasso裁剪,二次封装OkHttpUtils,Post提交表单数据
- Android网络请求框架----Okhttp3完全解析(1),使用篇
- OkHttp框架从入门到放弃,解析图片使用Picasso裁剪,二次封装OkHttpUtils,Post提交表单数据
- 基于OkHttp3封装网络请求框架
- 学习笔记 Tianmao 篇 OkHttp 网络的使用的简单封装 获取Json用GSON来解析
- 网络请求框架(二)----改善的okHttp封装库okhttputils的使用