Java高并发编程详解:多线程与架构设计(txt+pdf+epub+mobi电子书下载)


发布时间:2020-08-07 16:21:44

点击下载

作者:汪文君

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Java高并发编程详解:多线程与架构设计

Java高并发编程详解:多线程与架构设计试读:

前言

为什么写这本书

从大学毕业到现在已有11年的时间,在这11年中,我一直保持着一个习惯,将工作中用到的技术梳理成系统性的文档。在项目的开发过程中,由于时间的紧迫性,我们对某个技术领域知识的掌握往往都是比较碎片化的,系统化地串联知识碎片,不仅能加深对该技术的掌握,还能方便日后快速查阅,帮助记忆。截至目前,我已经在互联网上发布了大约12本电子书,主要是围绕着项目构建工具、Java开发的相关技术、NoSQL、J2EE等内容展开的。

2017年年初,很多人看过我写的《Java 8之Stream》电子书之后,给我发邮件,希望我写一本能够涵盖Java 8所有新特性的电子书。最开始一两个人这样提议的时候,我并没有在意,后来越来越多的朋友都有类似的需求,由于写电子书需要花费很长的时间,于是我尝试着将Java 8新特性录制成视频教程,大概花了一个月的业余时间我录制了40集《汪文君Java8实战视频》,视频一经推出收获了非常多的好评,所幸大家都比较喜欢我的授课风格,在过去的2017年,我一口气录制了11套视频教程,超过400集(每集都在30分钟左右),当然也包括Java高并发相关的内容。

在我的计划中,关于Java高并发的内容将会发布4套视频教程,分别是:第一阶段(Java多线程基础),第二阶段(Java内存模型,高并发设计模式),第三阶段(Java并发包JUC),第四阶段(Java并发包源码剖析AQS)。其中三个阶段都已经发布了,在今日头条《心蓝说Java》中累计播放时长超过20万分钟,百度云盘下载量也超过了30万余次。由于内容太多,本书只涵盖了前两个阶段的内容,经过了数以万计读者对视频教程问题的指正,本书的写作相对比较顺利,本书内容不仅修复了视频讲解中无法修复的缺陷,而且还加入了我对Java高并发更深一层的领悟和体会。

本书是我第一本正式出版的书稿,关于本书的写作可以说是一次偶然的机缘巧合,在2017年9月初,机械工业出版社的策划编辑Lisa找到了我,她觉得我的视频内容比较系统,非常适合以书稿的形式发表,我们简单交流之后就快速敲定了本书内容的主体结构,围绕着高并发视频教程的前两个阶段展开,在今年我也会努力将高并发后两个阶段的内容编著成书,使之尽快与读者见面。

读者对象

·计算机相关专业的在校学生。

·从事Java语言的开发者。

·从事Java系统架构的架构师。

·使用Java作为开发语言的公司与集体。

·开设Java课程的专业院校。

·开设Java课程的培训机构。

如何阅读本书

本书主要分为四部分,其中,第一部分主要阐述Thread的基础知识,详细介绍线程的API使用、线程安全、线程间数据通信以及如何保护共享资源等内容,它是深入学习多线程内容的基础。

在第二部分中之所以引入ClassLoader,是因为ClassLoader与线程不无关系,我们可以通过synchronized关键字,或者Lock等显式锁的方式在代码的编写阶段对共享资源进行数据一致性保护,那么一个Class在完成初始化的整个过程到最后在方法区(JDK8以后在元数据空间)其数据结构是怎样确保数据一致性的呢?这就需要对ClassLoader有一个比较全面的认识和了解。

在本书的第三部分中,我用了三章的篇幅来详细、深入地介绍volatile关键字的语义,volatile关键字在Java中非常重要,可以说它奠定了Java核心并发包的高效运行,在这一部分中,我们通过实例展示了如何使用volatile关键字并非常详细地介绍了Java内存模型等知识。

本书的最后一部分也就是第四部分,站在程序架构设计的角度讲解如何设计高效灵活的多线程应用程序,第四部分应该是内容最多的一部分,总共包含了15章。

勘误和支持

由于作者的水平有限,编写的时间也很仓促,书中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果在阅读的过程中发现任何问题都欢迎将您宝贵的意见发送到我的个人邮箱532500648@qq.com,我会专门在我的今日头条《心蓝说Java》开设专栏,用于修订书中出现的错误和不妥的地方,我真挚地期待着您的建议和反馈。

致谢

首先要感谢我的父亲,在我很小的时候,他就教育我做任何事情都要脚踏实地,一步一个脚印,做人不能浮躁,任何事情都不是一蹴而就的,这也致使我在遇到发展瓶颈的时候总能够耐得住性子寻求突破。

在本书最后一部分编写的过程中,我的妻子经历了十月怀胎为我生下了一对龙凤胎汪子敬、汪子兮兄妹,他俩的到来让我感觉到了初为人父的激动与喜悦,更加体会到了为人父母的不容易,感谢我的妻子,多谢你的支持和理解,本书的出版应该有一半你的功劳。

我还要感谢在我一路成长过程中带给我很多帮助的同事及朋友——徐景辉、Andrew Davidson、Bonson、Winne、Wilson、龙含等,在本书还是草稿阶段的时候,你们就给了我很多建设性的意见和建议。

当然也不能忘了感谢本书的策划编辑Lisa老师,是你直接促成了本书的诞生,在过去的半年多里,你反复不断地帮我审稿,修改错别字,调整不通顺的语句,你的专业水准和敬业精神帮助我最终顺利完稿。

最后一定要感谢我所在的研发团队——GBDS的Jack、Eachur、Jenny、Sebastian、Yuki、Kiki、Dillon、Gavin、Wendy、Josson、Echo、Ivy、Lik、Leo、Allen、Adrian、Kevin、Ken、Terrence,以及VADM的Jeffrey、Robert、Amy、Randy等,多谢你们在工作中对我的帮助。

谨以此书,献给我最亲爱的家人,以及众多热爱Java开发的朋友们。汪文君(Alex Wang)中国,广州,2018年3月▶▶第一部分多线程基础

第一部分的内容是整本书的基础,其围绕着Thread API及ThreadGroup API等进行讲解,在编写第一部分内容的过程中,笔者大量参考了虚拟机规范以及JDK官方文档,并深入源码分析每一个方法的详细信息。

第1章“快速认识线程”:本章主要介绍线程的概念,以及线程在Java中的主要作用,并且详细讲解了线程的生命周期,以及生命周期每个状态之间的切换方法。

第2章“深入理解Thread构造函数”:本章主要介绍了所有与Thread有关的构造函数,线程的父子关系(并非继承关系,而是一种包含关系),Thread和ThreadGroup之间的关系,Thread与虚拟机栈的关系(学习这部分内容需要读者有JVM的相关基础,尤其是对栈内存要有深入的理解),最后还介绍了守护线程的概念、特点和使用场景。

第3章“Thread API的详细介绍”:本章深入分析了Thread的所有API,熟练掌握Thread的API是学好Thread的前提。

第4章“线程安全与数据同步”:本章首先从一个简单的例子入手,讲解了数据同步的概念,以及会引发数据不一致性问题的情况,然后非常详细地介绍了synchronized关键字以及与其对应的JVM指令。本章的最后还分析了几种可能引起程序进入死锁的原因,以及如何使用工具进行诊断,线程安全与数据同步是线程中最重要也是最复杂的知识点之一,掌握好本章的内容可以使得程序在多线程的情况下既高效又安全的运行。

第5章“线程间通信”:我们在开发多线程程序的时候,往往不会只存在一个独立的线程,相反大多数情况下是需要多个线程之间进行协同工作的,如何在多个线程之间进行通信,是本章学习的重点。另外,本章的最后部分将会分析synchronized关键字的缺陷,我们手动实现了一个显式锁(BooleanLock)可以解决synchronized所不具备的功能,其中也需要用到线程间通信的知识。

第6章“ThreadGroup详细讲解”:如果在创建线程时并未指定所属的Group,线程会默认和父线程加入同一个Group之中,ThreadGroup为线程提供了一定的结构组织能力。通过本章的学习,读者将会深入掌握ThreadGroup所有API的使用以及Thread和ThreadGroup之间的关系。

第7章“Hook线程以及捕获线程执行异常”:不管是Runnable接口的run方法还是Thread本身的run方法,都是不允许抛出checked异常的,这样启动线程就会无法捕获到异常信息,JDK为我们提供了UncaughtExceptionHandler接口。本章将通过对UncaughtExceptionHandler的源码进行分析,使读者能够清晰地了解当Thread执行任务时出现异常应如何进行Handler的回调。本章的最后,我们还将学习如何向一个JVM进程注入Hook线程,当JVM进程收到中断信号时Hook线程将被触发执行。

第8章“线程池原理以及自定义线程池”:本章首先从线程池的原理入手,详细讲解了一个功能完善的线程池应该具备哪些要素,其中包括任务队列、线程数量管理、拒绝策略、线程工厂等,后文中也会经常使用我们创建的线程池。第1章 快速认识线程

在计算机的世界里,当我们探讨并行的时候,实际上是指,一系列的任务在计算机中同时运行,比如在浏览网页的时候还能打开音乐播放器,在撰写邮件的时候,收件箱还能接收新的邮件。在单CPU的计算机中,其实并没有真正的并行,它只不过是CPU时间钟快速轮转调度带给你的错觉,而这种错觉让你产生了它们真的在同一时刻同时运行。当然如果是多核CPU,那么并行运行还是真实存在的。1.1 线程的介绍

现在几乎百分之百的操作系统都支持多任务的执行,对计算机来说每一个任务就是一个进程(Process),在每一个进程内部至少要有一个线程(Thread)是在运行中,有时线程也称为轻量级的进程。

线程是程序执行的一个路径,每一个线程都有自己的局部变量表、程序计数器(指向正在执行的指令指针)以及各自的生命周期,现代操作系统中一般不止一个线程在运行,当启动了一个Java虚拟机(JVM)时,从操作系统开始就会创建一个新的进程(JVM进程),JVM进程中将会派生或者创建很多线程。1.2 快速创建并启动一个线程

本节中,我们将快速认识Thread Class的用法,并将尝试说明如何创建并启动一个线程。

这里还是以我们日常生活中的例子进行举例,假设你想在浏览网页看新闻的同时听听音乐,下面我们就来尝试着用Java的代码来实现这一功能。

1.2.1 尝试并行运行

代码清单1-1 TryConcurrency.javapackage com.wangwenjun.concurrent.chapter01;import java.util.concurrent.TimeUnit;public class TryConcurrency{ public static void main(String[] args) { browseNews(); enjoyMusic(); } /** * Browse the latest news. */ private static void browseNews() { for (; ; ) { System.out.println("Uh-huh, the good news."); sleep(1); } } /** * Listening and enjoy the music. */ private static void enjoyMusic() { for (; ; ) { System.out.println("Uh-huh, the nice music."); sleep(1); } } /** * Simulate the wait and ignore exception. * @param seconds */ private static void sleep(int seconds) { try { TimeUnit.SECONDS.sleep(seconds); } catch (InterruptedException e) { e.printStackTrace(); } }}

代码清单1-1试图让听音乐和看新闻两个任务同时执行(在控制台输出表现为交替输出),不过很可惜,程序的输出永远都是在看新闻,而听音乐的任务永远都得不到执行,TryConcurrency代码的输出如下:Uh-huh, the good news.U-h-huh, the good news.Uhhuh, the good news....

1.2.2 并发运行交替输出

如果想让听音乐和看新闻两个事件并发执行,也就是在Console中看到它们彼此交替输出,就必须借助Java提供的Thread这个class(关于Thread的用法和详解,后文中会有详细的讲解)。

只需要将代码清单1-1的main方法中的任意一个方法交给Thread即可,下面是加入了Thread之后的代码:public static void main(String[] args){ //通过匿名内部类的方式创建线程,并且重写其中的run方法 new Thread(){ //① @Override public void run() { enjoyMusic(); } }.start(); //② browseNews();}

输出如下:Uh-huh, the good news.Uh-huh, the nice music.Uh-huh, the good news.Uh-huh, the nice music.........

代码修改之后会发现听音乐和看新闻两个任务可以并行运行,并且在控制台交替输出。注意

上面的代码中,线程启动必须在其中一个任务之前,否则线程将永远得不到启动,因为前一个任务永远不会结束。

①创建一个线程,并且重写其run方法,将enjoyMusic交给它执行。

②启动新的线程,只有调用了Thread的start方法,才代表派生了一个新的线程,否则Thread和其他普通的Java对象没有什么区别,start方法是一个立即返回方法,并不会让程序陷入阻塞。

如果用Java 8 Lambda改造上面的代码,那么代码看起来将会更加简洁,如下所示:public static void main(String[] args){ new Thread(TryConcurrency::enjoyMusic).start(); browseNews();}

1.2.3 使用Jconsole观察线程

1.2.2节创建了一个Thread并且启动,那么此时JVM中有多少个线程呢?除了我们创建的线程以外,还有哪些线程可以借助Jconsole或者Jstack命令来查看,如图1-1所示,这两个JVM工具都是由JDK自身提供的。图1-1 使用Jconsole观察JVM线程

在图1-1中,我用线框勾出了两个线程,其中一个是main,另一个是Thread-0,之前说过在操作系统启动一个Java虚拟机(JVM)的时候,其实是启动了一个进程,而在该进程里面启动了一个以上的线程,其中Thread-0这个线程就是1.2.2节中创建的,main线程是由JVM启动时创建的,我们都知道J2SE程序的入口就是main函数,虽然我们在1.2.2节中显式地创建了一个线程,事实上还有一个main线程,当然还有一些其他的守护线程,比如垃圾回收线程、RMI线程等。1.3 线程的生命周期详解

前面提到过,每一个线程都有自己的局部变量表、程序计数器,以及生命周期等,本节就来分析一下线程的生命周期,如图1-2所示。

在开始解释线程的生命周期之前,请大家思考一个问题:执行了Thread的start方法就代表该线程已经开始执行了吗?图1-2 线程生命周期状态图

通过图1-2的展示可知,线程的生命周期大体可以分为如下5个主要的阶段。

·NEW

·RUNNABLE

·RUNNING

·BLOCKED

·TERMINATED

1.3.1 线程的NEW状态

当我们用关键字new创建一个Thread对象时,此时它并不处于执行状态,因为没有调用start方法启动该线程,那么线程的状态为NEW状态,准确地说,它只是Thread对象的状态,因为在没有start之前,该线程根本不存在,与你用关键字new创建一个普通的Java对象没什么区别。

NEW状态通过start方法进入RUNNABLE状态。

1.3.2 线程的RUNNABLE状态

线程对象进入RUNNABLE状态必须调用start方法,那么此时才是真正地在JVM进程中创建了一个线程,线程一经启动就可以立即得到执行吗?答案是否定的,线程的运行与否和进程一样都要听令于CPU的调度,那么我们把这个中间状态称为可执行状态(RUNNABLE),也就是说它具备执行的资格,但是并没有真正地执行起来而是在等待CPU的调度。

由于存在Running状态,所以不会直接进入BLOCKED状态和TERMINATED状态,即使是在线程的执行逻辑中调用wait、sleep或者其他block的IO操作等,也必须先获得CPU的调度执行权才可以,严格来讲,RUNNABLE的线程只能意外终止或者进入RUNNING状态。

1.3.3 线程的RUNNING状态

一旦CPU通过轮询或者其他方式从任务可执行队列中选中了线程,那么此时它才能真正地执行自己的逻辑代码,需要说明的一点是一个正在RUNNING状态的线程事实上也是RUNNABLE的,但是反过来则不成立。

在该状态中,线程的状态可以发生如下的状态转换。

·直接进入TERMINATED状态,比如调用JDK已经不推荐使用的stop方法或者判断某个逻辑标识。

·进入BLOCKED状态,比如调用了sleep,或者wait方法而加入了waitSet中。

·进行某个阻塞的IO操作,比如因网络数据的读写而进入了BLOCKED状态。

·获取某个锁资源,从而加入到该锁的阻塞队列中而进入了BLOCKED状态。

·由于CPU的调度器轮询使该线程放弃执行,进入RUNNABLE状态。

·线程主动调用yield方法,放弃CPU执行权,进入RUNNABLE状态。

1.3.4 线程的BLOCKED状态

1.3.3节中已经列举了线程进入BLCOKED状态的原因,此处就不再赘述了,线程在BLOCKED状态中可以切换至如下几个状态。

·直接进入TERMINATED状态,比如调用JDK已经不推荐使用的stop方法或者意外死亡(JVM Crash)。

·线程阻塞的操作结束,比如读取了想要的数据字节进入到RUNNABLE状态。

·线程完成了指定时间的休眠,进入到了RUNNABLE状态。

·Wait中的线程被其他线程notify/notifyall唤醒,进入RUNNABLE状态。

·线程获取到了某个锁资源,进入RUNNABLE状态。

·线程在阻塞过程中被打断,比如其他线程调用了interrupt方法,进入RUNNABLE状态。

1.3.5 线程的TERMINATED状态

TERMINATED是一个线程的最终状态,在该状态中线程将不会切换到其他任何状态,线程进入TERMINATED状态,意味着该线程的整个生命周期都结束了,下列这些情况将会使线程进入TERMINATED状态。

·线程运行正常结束,结束生命周期。

·线程运行出错意外结束。

·JVM Crash,导致所有的线程都结束。

通过本节关于线程生命周期的分析,相信读者已经能够独立回答我们开始时提的问题了,线程的生命周期非常关键,很多工作多年的程序员,由于不重视这部分的内容,在编写程序的时候也经常会出现一些错误。1.4 线程的start方法剖析:模板设计模式在Thread中的应用

在本节中,我们将分析Thread的start方法,在调用了start方法之后到底进行了什么操作,通过1.3节的内容讲解,相信大家已经明白了,start方法启动了一个线程,并且该线程进入了可执行状态(RUNNABLE),在“代码清单1-1 TryConcurrency”中,我们重写了Thread的run方法,但却调用了start方法,那么run方法和start方法有什么关系呢?带着诸多的疑问,我们一起在本节中寻找答案吧!

1.4.1 Thread start方法源码分析以及注意事项

先来看一下Thread start方法的源码,如下所示:public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } }}

start方法的源码足够简单,其实最核心的部分是start0这个本地方法,也就是JNI方法:private native void start0();

也就是说在start方法中会调用start0方法,那么重写的那个run方法何时被调用了呢?单从上面是看不出来任何端倪的,但是打开JDK的官方文档,在start方法中有如下的注释说明:※ Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread.

上面这句话的意思是:在开始执行这个线程时,JVM将会调用该线程的run方法,换言之,run方法是被JNI方法start0()调用的,仔细阅读start的源码将会总结出如下几个知识要点。

·Thread被构造后的NEW状态,事实上threadStatus这个内部属性为0。

·不能两次启动Thread,否则就会出现IllegalThreadStateException异常。

·线程启动后将会被加入到一个ThreadGroup中,后文中我们将详细介绍ThreadGroup。

·一个线程生命周期结束,也就是到了TERMINATED状态,再次调用start方法是不允许的,也就是说TERMINATED状态是没有办法回到RUNNABLE/RUNNING状态的。Thread thread = new Thread(){ @Override public void run() { try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } }};thread.start();//启动线程thread.start();//再次启动

执行上面的代码将会抛出IllegalThreadStateException异常,而我们将代码稍作改动,模拟一个线程生命周期的结束,再次启动看看会发生什么:Thread thread = new Thread(){ @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }};thread.start();TimeUnit.SECONDS.sleep(2);//休眠主要是确保thread结束生命周期thread.start();//企图重新激活该线程注意

程序同样会抛出IllegalThreadStateException异常,但是这两个异常的抛出却有本质上的区别,第一个是重复启动,只是第二次启动是不允许的,但是此时该线程是处于运行状态的,而第二次企图重新激活也抛出了非法状态的异常,但是此时没有线程,因为该线程的生命周期已经被终结。

1.4.2 模板设计模式在Thread中的应用

通过1.4.1节的分析,我们不难看出,线程的真正的执行逻辑是在run方法中,通常我们会把run方法称为线程的执行单元,这也就回答了我们最开始提出的疑问,重写run方法,用start方法启动线程。Thread中run方法的代码如下,如果我们没有使用Runnable接口对其进行构造,则可以认为Thread的run方法本身就是一个空的实现:@Overridepublic void run() { if (target != null) {//我们并没有使用runnable构造Thread target.run(); }}

其实Thread的run和start就是一个比较典型的模板设计模式,父类编写算法结构代码,子类实现逻辑细节,下面通过一个简单的例子来看一下模板设计模式,然后读者可以参考该模式在Thread中的使用,示例代码如清单1-2所示。

代码清单1-2 TemplateMethod.javapackage com.wangwenjun.concurrent.chapter01;public class TemplateMethod { public final void print(String message) { System.out.println("################"); wrapPrint(message); System.out.println("################"); } protected void wrapPrint(String message) { } public static void main(String[] args) { TemplateMethod t1 = new TemplateMethod(){ @Override protected void wrapPrint(String message) { System.out.println("*"+message+"*"); } }; t1.print("Hello Thread"); TemplateMethod t2 = new TemplateMethod(){ @Override protected void wrapPrint(String message) { System.out.println("+"+message+"+"); } }; t2.print("Hello Thread"); }}

print方法类似于Thread的start方法,而wrapPrint则类似于run方法,这样做的好处是,程序结构由父类控制,并且是final修饰的,不允许被重写,子类只需要实现想要的逻辑任务即可,输出如下:################*Hello Thread*################################+Hello Thread+################

1.4.3 Thread模拟营业大厅叫号机程序

相信很多人都去过银行、医院、移动营业厅、公积金中心等,在这些机构的营业大厅都有排队等号的机制,这种机制的主要作用就是限流,减轻业务受理人员的压力。当你走进营业大厅后,需要先领取一张流水号纸票,然后拿着纸票坐在休息区等待你的号码显示在业务办理的橱窗显示器上面,如图1-3所示。图1-3 银行营业大厅

如图1-3所示,假设大厅共有四台出号机,这就意味着有四个线程在工作,下面我们用程序模拟一下叫号的过程,约定当天最多受理50笔业务,也就是说号码最多可以出到50。

TicketWindow代表大厅里的出号机器,代码如清单1-3所示。

代码清单1-3 TicketWindow.javapackage com.wangwenjun.concurrent.chapter01;public class TicketWindow extends Thread { //柜台名称 private final String name; //最多受理50笔业务 private static final int MAX = 50; private int index = 1; public TicketWindow(String name) { this.name = name; } @Override public void run() { while (index <= MAX) { System.out.println("柜台:" + name + "当前的号码是:" + (index++)); } }}

接下来,写一个main函数,对其进行测试,定义了四个TicketWindow线程,并且分别启动:public static void main(String[] args) { TicketWindow ticketWindow1 = new TicketWindow("一号出号机"); ticketWindow1.start(); TicketWindow ticketWindow2 = new TicketWindow("二号出号机"); ticketWindow2.start(); TicketWindow ticketWindow3 = new TicketWindow("三号出号机"); ticketWindow3.start(); TicketWindow ticketWindow4 = new TicketWindow("四号出号机"); ticketWindow4.start(); }

运行之后的输出似乎令人大失所望,为何每一个TickWindow所出的号码都是从1到50呢?柜台:一号柜台当前的号码是:1柜台:三号柜台当前的号码是:1柜台:二号柜台当前的号码是:1柜台:三号柜台当前的号码是:2柜台:三号柜台当前的号码是:3柜台:三号柜台当前的号码是:4柜台:三号柜台当前的号码是:5柜台:三号柜台当前的号码是:6...

之所以出现这个问题,根本原因是因为每一个线程的逻辑执行单元都不一样,我们新建了四个Ticket Window线程,它们的票号都是从0开始到50结束,四个线程并没有像图1-3所描述的那样均从客席号服务器进行交互,获取一个唯一的递增的号码,那么应该如何改进呢?无论TicketWindow被实例化多少次,只需要保证index是唯一的即可,我们会立即会想到使用static去修饰index以达到目的,改进后的代码如清单1-4所示:

代码清单1-4 修改后的TicketWindow.javapublic class TicketWindow extends Thread { private final String name; private static final int MAX = 50; private static int index = 1; public TicketWindow(String name) { this.name = name; } @Override public void run() { while (index <= MAX) { System.out.println("柜台:" + name + "当前的号码是:" + (index++)); } }}

再次运行上面的main函数,会发现情况似乎有些改善,四个出号机交替着输出不同的号码,输出如下:柜台:一号出号机当前的号码是:1柜台:一号出号机当前的号码是:3柜台:三号出号机当前的号码是:2柜台:一号出号机当前的号码是:4柜台:三号出号机当前的号码是:5柜台:一号出号机当前的号码是:6柜台:三号出号机当前的号码是:7柜台:一号出号机当前的号码是:8柜台:三号出号机当前的号码是:9柜台:一号出号机当前的号码是:10...

通过对index进行static修饰,做到了多线程下共享资源的唯一性,看起来似乎满足了我们的需求(事实上,如果将最大号码调整到500、1000等稍微大一些的数字就会出现线程安全的问题,关于这点将在后面的章节中详细介绍),但是只有一个index共享资源,如果共享资源很多呢?共享资源要经过一些比较复杂的计算呢?不可能都使用static修饰,而且static修饰的变量生命周期很长,所以Java提供了一个接口Runnable专门用于解决该问题,将线程的控制和业务逻辑的运行彻底分离开来。1.5 Runnable接口的引入以及策略模式在Thread中的使用

1.5.1 Runnable的职责

Runnable接口非常简单,只定义了一个无参数无返回值的run方法,具体如代码清单1-5所示。

代码清单1-5 Runnable接口package java.lang;public interface Runnable { void run();}

在很多软文以及一些书籍中,经常会提到,创建线程有两种方式,第一种是构造一个Thread,第二种是实现Runnable接口,这种说法是错误的,最起码是不严谨的,在JDK中代表线程的就只有Thread这个类,我们在前面分析过,线程的执行单元就是run方法,你可以通过继承Thread然后重写run方法实现自己的业务逻辑,也可以实现Runnable接口实现自己的业务逻辑,代码如下:@Overridepublic void run() {//如果构造Thread时传递了Runnable,则会执行runnable的run方法 if (target != null) { target.run(); }//否则需要重写Thread类的run方法}

上面的代码段是Thread run方法的源码,我在其中加了两行注释更加清晰地说明了实现执行单元的两种方式,所以说创建线程有两种方式,一种是创建一个Thread,一种是实现Runnable接口,这种说法是不严谨的。准确地讲,创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元则有两种方式,第一种是重写Thread的run方法,第二种是实现Runnable接口的run方法,并且将Runnable实例用作构造Thread的参数。

1.5.2 策略模式在Thread中的应用

前面说过了,无论是Runnable的run方法,还是Thread类本身的run方法(事实上Thread类也是实现了Runnable接口)都是想将线程的控制本身和业务逻辑的运行分离开来,达到职责分明、功能单一的原则,这一点与GoF设计模式中的策略设计模式很相似,在本节中,我们一起来看看什么是策略模式,然后再来对比Thread和Runnable两者之间的关系。

相信很多人都做过关于JDBC的开发,下面我们在这里做一个简单的查询操作,只不过是把数据的封装部分抽取成一个策略接口,代码如清单1-6所示。

代码清单1-6 RowHandler.javapackage com.wangwenjun.concurrent.chapter01;import java.sql.ResultSet;public interface RowHandler{ T handle(ResultSet rs);}

RowHandler接口只负责对从数据库中查询出来的结果集进行操作,至于最终返回成什么样的数据结构,那就需要你自己去实现,类似于Runnable接口,示例代码如清单1-7所示。

代码清单1-7 RecordQuery.javapackage com.wangwenjun.concurrent.chapter01;import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;public class RecordQuery{ private final Connection connection; public RecordQuery(Connection connection) { this.connection = connection; } public T query(RowHandler handler, String sql, Object... params) throws SQLException { try (PreparedStatement stmt = connection.prepareStatement(sql)) { int index = 1; for (Object param : params) { stmt.setObject(index++, param); } ResultSet resultSet = stmt.executeQuery(); return handler.handle(resultSet);//①调用RowHandler } }}

RecordQuery中的query只负责将数据查询出来,然后调用RowHandler进行数据封装,至于将其封装成什么数据结构,那就得看你自己怎么处理了,下面我们来看看这样做有什么好处?

上面这段代码的好处是可以用query方法应对任何数据库的查询,返回结果的不同只会因为你传入RowHandler的不同而不同,同样RecordQuery只负责数据的获取,而RowHandler则负责数据的加工,职责分明,每个类均功能单一,相信通过这个简单的示例大家应该能够清楚Thread和Runnable之间的关系了。注意

重写Thread类的run方法和实现Runnable接口的run方法还有一个很重要的不同,那就是Thread类的run方法是不能共享的,也就是说A线程不能把B线程的run方法当作自己的执行单元,而使用Runnable接口则很容易就能实现这一点,使用同一个Runnable的实例构造不同的Thread实例。

1.5.3 模拟营业大厅叫号机程序

既然我们说使用static修饰index这个共享资源不是一种好的方式,那么我们在本节中使用Runnable接口来实现逻辑执行单元重构一下1.4节中的营业大厅叫号机程序。

首先我们将Thread的run方法抽取成一个Runnable接口的实现,代码如清单1-8所示。

代码清单1-8 TicketWindowRunnable.javapackage com.wangwenjun.concurrent.chapter01;public class TicketWindowRunnable implements Runnable { private int index = 1;//不做static修饰 private final static int MAX = 50; @Override public void run() { while (index <= MAX) { System.out.println(Thread.currentThread() + " 的号码是:" + (index++)); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }}

可以看到上面的代码中并没有对index进行static的修饰,并且我们也将Thread中run的代码逻辑抽取到了Runnable的一个实现中,下面的代码构造了四个叫号机的线程,并且开始工作:public static void main(String[] args) { final TicketWindowRunnable task = new TicketWindowRunnable(); Thread windowThread1 = new Thread(task, "一号窗口"); Thread windowThread2 = new Thread(task, "二号窗口"); Thread windowThread3 = new Thread(task, "三号窗口"); Thread windowThread4 = new Thread(task, "四号窗口"); windowThread1.start();

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载