Java多线程编程核心技术(第2版)(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-07 21:27:38

点击下载

作者:高洪岩

出版社:机械工业出版社

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

Java多线程编程核心技术(第2版)

Java多线程编程核心技术(第2版)试读:

前言

本书是国内首本整本系统、完整地介绍Java多线程技术的书籍,作为笔者,我要感谢大家的支持与厚爱。

本书第1版在出版后获得了广大Java程序员与学习者的关注,技术论坛、博客、公众号等平台大量涌现出针对Java多线程技术的讨论与分享。能为国内IT知识的建设贡献微薄之力是让我最欣慰的。

有些读者在第一时间就根据书中的知识总结了学习笔记,并在博客中进行分享,笔者非常赞赏这种传播知识的精神。知识就要分享,知识就要传播,这样才能共同进步。第2版与第1版的区别

本书第1版上市后收到了大量的读者反馈,我对每一个建议都细心地进行整理,力求在第2版中得以完善。

第2版在第1版的基础上着重加强了8点更新:

1)大量知识点重排,更有利于阅读与理解;

2)更新了读者提出的共性问题并进行集中讲解;

3)丰富Thread.java类API的案例,使其更具有实用性;

4)对线程的信息进行监控实时采样;

5)强化了volatile语义、多线程核心synchronized的案例;

6)力求知识点连贯,方便深度学习与理解,增加原子与线程安全的内容;

7)深入浅出地介绍代码重排特性;

8)细化工具类ThrealLocal和InheritableThreadLocal的源代码分析与原理。

由于篇幅有限,有关线程池的知识请参考笔者的另一本书—《Java并发编程:核心方法与框架》,那本书中有针对Java并发编程技术的讲解。在向分布式领域进军时还需要用到NIO和Socket技术,故推荐笔者的拙作《NIO与Socket编程技术指南》,希望可以给读者带来一些帮助。

本书秉承大道至简的主导思想,只介绍Java多线程开发中最值得关注的内容,希望抛砖引玉,以个人的一些想法和见解,为读者拓展出更深入、更全面的思路。本书特色

在撰写本书的过程中,我尽量少用“啰唆”的文字,全部以Demo式案例来讲解技术点的实现,使读者看到代码及运行结果后就可以知道项目要解决的是什么问题,类似于网络中博客的风格,让读者用最短的时间学习知识点,明白知识点如何应用,以及在使用时要避免什么,使读者能够快速学习知识并解决问题。读者对象

·Java程序员;

·系统架构师;

·Java多线程开发者;

·Java并发开发者;

·大数据开发者;

·其他对多线程技术感兴趣的人员。如何阅读本书

本书本着实用、易懂的学习原则,利用7章来介绍Java多线程相关的技术。

第1章讲解了Java多线程的基础,包括Thread类的核心API的使用。

第2章讲解了在多线程中对并发访问的控制,主要是synchronized的使用。由于此关键字在使用上非常灵活,所以该章用很多案例来说明它的使用,为读者学习同步知识打好坚实的基础。

第3章讲解了线程之间的通信与交互细节。该章主要介绍wait()、notifyAll()和notify()方法的使用,使线程间能够互相通信,合作完成任务。该章还介绍了ThreadLocal类的使用。学习完该章,读者就能在Thread多线程中进行数据的传递了。

第4章讲解了Lock对象。因为synchronized关键字使用起来比较麻烦,所以Java 5提供了Lock对象,更好地实现了并发访问时的同步处理,包括读写锁等。

第5章讲解了Timer定时器类,其内部原理是使用多线程技术。定时器在执行计划任务时是很重要的,在进行Android开发时也会深入使用。

第6章讲解的单例模式虽然很简单,但如果遇到多线程将会变得非常麻烦。如何在多线程中解决这么棘手的问题呢?本章会全面给出解决方案。

第7章对前面章节遗漏的技术空白点进行补充,通过案例使多线程的知识体系更加完整,尽量做到不出现技术空白点。交流和支持

由于笔者水平有限,加上编写时间仓促,书中难免会出现一些疏漏或者不准确的地方,恳请读者批评指正,期待能够得到你们的真挚反馈,在技术之路上互勉共进。

联系笔者的邮箱是279377921@qq.com。致谢

在本书出版的过程中,感谢公司领导和同事的大力支持,感谢家人给予我充足的时间来撰写稿件,感谢出生3个多月的儿子高晟京,看到你,我有了更多动力,最后感谢在此稿件上耗费大量精力的高婧雅编辑与她的同事们,是你们的鼓励和帮助,引导我顺利完成了本书。高洪岩  第1章 Java多线程技能

作为本书的第1章,重点是让读者快速进入Java多线程的学习,所以本章主要介绍Thread类的核心方法。Thread类的核心方法较多,读者应该着重掌握如下技术点:

·线程的启动;

·如何使线程暂停;

·如何使线程停止;

·线程的优先级;

·线程安全相关的问题。

以上内容也是本章学习的重点与思路,掌握这些内容是进入Java多线程学习的必经之路。1.1 进程和多线程概述

本书主要介绍在Java语言中使用的多线程技术,但讲到多线程技术时不得不提及“进程”这个概念,“百度百科”对“进程”的解释如图1-1所示。图1-1 进程的定义

初看这段文字十分抽象,难以理解,那么再来看如图1-2所示的内容。图1-2 Windows 7系统中的进程列表

难道一个正在操作系统中运行的exe程序可以理解成一个“进程”?没错!

通过查看“Windows任务管理器”窗口中的列表,完全可以将运行在内存中的exe文件理解成进程——进程是受操作系统管理的基本运行单元。

程序是指令序列,这些指令可以让CPU完成指定的任务。*.java程序经编译后形成*.class文件,在Windows中启动一个JVM虚拟机相当于创建了一个进程,在虚拟机中加载class文件并运行,在class文件中通过执行创建新线程的代码来执行具体的任务。创建测试用的代码如下:public class Test1 { public static void main(String[] args) { try { Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } }}

在没有运行这个类之前,任务管理器中以“j”开头的进程列表如图1-3所示。图1-3 任务管理器中以“j”开头的进程

Test1类重复运行3次后的进程列表如图1-4所示。可以看到,在任务管理器中创建了3个javaw.exe进程,说明每执行一次main()方法就创建一个进程,其本质上就是JVM虚拟机进程。图1-4 创建了3个javaw.exe进程

那什么是线程呢?线程可以理解为在进程中独立运行的子任务,例如,QQ.exe运行时,很多的子任务也在同时运行,如好友视频线程、下载文件线程、传输数据线程、发送表情线程等,这些不同的任务或者说功能都可以同时运行,其中每一项任务完全可以理解成是“线程”在工作,传文件、听音乐、发送图片表情等这些功能都有对应的线程在后台默默地运行。

进程负责向操作系统申请资源。在一个进程中,多个线程可以共享进程中相同的内存或文件资源。

使用多线程有什么优点呢?其实如果有使用“多任务操作系统”的经验,如Windows系列,大家应该都有这样的体会:使用多任务操作系统Windows,可以大幅利用CPU的空闲时间来处理其他任务,例如,可以一边让操作系统处理正在用打印机打印的数据,一边使用Word编辑文档。CPU在这些任务中不停地进行切换,由于切换的速度非常快,给使用者的感受是这些任务在同时运行,所以使用多线程技术可以在同一时间内执行更多不同的任务。

为了更加有效地理解多线程的优势,下面先来看如图1-5所示的单任务运行环境。

在图1-5中,任务1和任务2是两个完全独立、不相关的任务。任务1在等待远程服务器返回数据,以便进行后期处理,这时CPU一直呈等待状态,一直在“空运行”。任务2在10s之后被运行,虽然执行完任务2所用时间非常短,仅仅是1s,但也必须等任务1运行结束后才可以运行任务2,本程序运行在单任务环境中,所以任务2有非常长的等待时间,系统运行效率大幅降低。单任务的特点就是排队执行,即同步,就像在cmd中输入一条命令后,必须等待这条命令执行完才可以执行下一条命令。在同一时间只能执行一个任务,CPU利用率大幅降低,这就是单任务运行环境的缺点。图1-5 单任务运行环境

多任务运行环境如图1-6所示。图1-6 多任务运行环境

在图1-6中,CPU完全可以在任务1和任务2之间来回切换,使任务2不必等到10s之后再运行,系统和CPU的运行效率大大提升,这就是为什么要使用多线程技术、为什么要学习多线程。多任务的特点是在同一时间可以执行多个任务,这也是多线程技术的优点。使用多线程也就是在使用异步。

在通常情况下,单任务和多任务的实现与操作系统有关。例如,在一台计算机上使用同一个CPU,安装DOS磁盘操作系统只能实现单任务运行环境,而安装Windows操作系统则可以实现多任务运行环境。

在什么场景下使用多线程技术?笔者总结了两点。

1)阻塞。一旦系统中出现了阻塞现象,则可以根据实际情况来使用多线程技术提高运行效率。

2)依赖。业务分为两个执行过程,分别是A和B。当A业务发生阻塞情况时,B业务的执行不依赖A业务的执行结果,这时可以使用多线程技术来提高运行效率;如果B业务的执行依赖A业务的执行结果,则可以不使用多线程技术,按顺序进行业务的执行。

在实际的开发应用中,不要为了使用多线程而使用多线程,要根据实际场景决定。

注意

多线程是异步的,所以千万不要把Eclipse代码的顺序当作线程执行的顺序,线程被调用的时机是随机的。1.2 使用多线程

想学习一个技术就要“接近”它,所以本节首先通过一个示例来接触一下线程。

一个进程正在运行时至少会有一个线程在运行,这种情况在Java中也是存在的,这些线程在后台默默地执行,例如,调用public static void main()方法的线程就是这样的,而且它由JVM创建。

创建示例项目callMainMethodMainThread,并创建Test.java类,代码如下:package test;public class Test { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); }}

程序运行结果如图1-7所示。图1-7 程序运行结果(主线程main出现)

在控制台输出的main其实就是一个名称为main的线程在执行main()方法中的代码。另外,需要说明一下,在控制台输出的main和main方法没有任何关系,它们仅仅是名字相同而已。1.2.1 继承Thread类

Java的JDK开发包已经自带了对多线程技术的支持,通过它可以方便地进行多线程编程。实现多线程编程主要有两种方式:一种是继承Thread类,另一种是实现Runnable接口。

在学习如何创建新的线程前,先来看看Thread类的声明结构:public class Thread implements Runnable

从上面的源代码中可以发现,Thread类实现了Runnable接口,它们之间具有多态关系,多态结构的示例代码如下:Runnable run1 = new Thread();Runnable run2 = new MyThread();Thread t1 = new MyThread();

其实使用继承Thread类的方式创建新线程时,最大的局限是不支持多继承,因为Java语言的特点是单根继承,所以为了支持多继承,完全可以实现Runnable接口,即一边实现一边继承,但这两种方式创建线程的功能是一样的,没有本质的区别。

本节主要介绍第一种方式。创建名称为t1的Java项目,创建一个自定义的线程类MyThread.java,此类继承自Thread,并且重写run()方法。在run()方法中添加线程要执行的任务代码如下:package com.mythread.www;public class MyThread extends Thread {@Override public void run() { super.run(); System.out.println("MyThread"); }}

运行类代码如下:package test;import com.mythread.www.MyThread;public class Run { public static void main(String[] args) { MyThread mythread = new MyThread(); mythread.start();//耗时大 System.out.println("运行结束!");//耗时小 }}

上面代码使用start()方法来启动一个线程,线程启动后会自动调用线程对象中的run()方法,run()方法里面的代码就是线程对象要执行的任务,是线程执行任务的入口。

程序运行结果如图1-8所示。

从图1-8的程序运行结果来看,MyThread.java类中的run()方法的执行时间相对于输出“运行结束!”的执行时间晚,因为start()方法的执行比较耗时,这也增加了先输出“运行结束!”字符串的概率。start()方法耗时的原因是执行了多个步骤,步骤如下。图1-8 程序运行结果

1)通过JVM告诉操作系统创建Thread。

2)操作系统开辟内存并使用Windows SDK中的createThread()函数创建Thread线程对象。

3)操作系统对Thread对象进行调度,以确定执行时机。

4)Thread在操作系统中被成功执行。

以上4步完整地执行后所消耗的时间一定大于输出“运行结束!”字符串的时间。另外,main线程执行start()方法时不必等待4步都执行完毕,而是立即继续执行start()方法后面的代码,这4步会与输出“运行结束!”的代码一同执行,由于输出“运行结束!”耗时比较少,所以在大多数的情况下,先输出“运行结束!”,后输出“MyThread”。

但在这里,还是有非常非常小的、非常渺茫的机会能输出如下运行结果:MyThread运行结束!

输出上面的结果说明执行完整的start()方法的4步后,才执行输出“运行结束!”字符串的代码,这也说明线程执行的顺序具有随机性。然而由于输出这种结果的机会很小,使用手动的方式来重复执行“Run as”->“Java Application”难以重现,这时可以人为地制造这种输出结果,即在执行输出“运行结束!”代码之前先执行代码Thread.sleep(300),让run()方法有充足的时间来先输出“MyThread”,后输出“运行结束!”,示例代码如下:package test;import com.mythread.www.MyThread;public class Run2 { public static void main(String[] args) throws InterruptedException { MyThread mythread = new MyThread(); mythread.start(); Thread.sleep(200); System.out.println("运行结束!"); }}

在使用多线程技术时,代码的运行结果与代码的执行顺序或调用顺序是无关的。另外,线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run()方法,所以先输出“运行结束!”和先输出“MyThread”具有不确定性。

注意

如果多次调用start()方法,则出现异常Exception in thread“main”java.lang.Illegal-ThreadStateException。1.2.2 使用常见命令分析线程的信息

可以在运行的进程中创建线程,如果想查看这些线程的状态与信息,则可采用3种常见命令,它们分别是jps+jstack.exe、jmc.exe和jvisualvm.exe,它们在jdk\bin文件夹中。

创建测试用的程序并运行,代码如下:package test.run;public class Run3 { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 5; i++) { new Thread() { public void run() { try { Thread.sleep(500000); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); } }}

1)采用第1种方式查看线程的状态——使用jps+jstack.exe命令。在cmd中输入jps命令查看Java进程,其中进程id是13824的进程就是当前运行类Run3对应的Java虚拟机进程,然后使用jstack命令查看该进程下线程的状态,命令如下:C:\>cd jdk1.8.0_161C:\jdk1.8.0_161>cd binC:\jdk1.8.0_161\bin>jps13824 Run38328 JpsC:\jdk1.8.0_161\bin>jstack -l 13824

按Enter键后就可以看到线程的状态。

2)采用第2种方式查看线程的状态——使用jmc.exe命令。双击jmc.exe命令出现如图1-9所示的欢迎界面。

关闭欢迎界面后双击Run3进程,再双击“MBean服务器”,然后单击“线程”选项卡,出现如图1-10所示的界面。图1-9 jmc.exe命令的欢迎界面图1-10 线程列表

在线程列表中可以看到5个线程的名称与状态。

3)采用第3种方式查看线程的状态——使用jvisualvm.exe命令。双击jvisualvm.exe命令,出现如图1-11所示的界面。图1-11 jvisualvm.exe命令的主界面

双击Run3进程,再单击“线程”选项卡就看到了5个线程,如图1-12所示。图1-12 可以看到5个线程

但采用jvisualvm.exe命令看不到线程运行的状态,所以推荐使用jmc.exe命令来分析线程对象的相关信息。1.2.3 线程随机性的展现

前面介绍过线程的调用是随机的,但这一点并没有在代码中得以体现,都是理论的内容,所以本节将在名称为randomThread的Java项目中演示线程的随机性。

创建自定义线程类MyThread.java的代码如下:package mythread;public class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 10000; i++) { System.out.println("run=" + Thread.currentThread().getName()); } }}

再创建运行类Test.java代码:package test;import mythread.MyThread;public class Test { public static void main(String[] args) { MyThread thread = new MyThread(); thread.setName("myThread"); thread.start(); for (int i = 0; i < 10000; i++) { System.out.println("main=" + Thread.currentThread().getName()); } }}

Thread.java类中的start()方法通知“线程规划器”——此线程已经准备就绪,准备调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法,即让线程执行具体的任务,具有随机顺序执行的效果。

如果调用代码“thread.run();”而不是“thread.start();”,其实就不是异步执行了,而是同步执行,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代码执行完毕后才可以执行后面的代码。

以异步方式运行的效果如图1-13所示。图1-13 随机被执行的线程

多线程随机输出的原因是CPU将时间片分给不同的线程,线程获得时间片后就执行任务,所以这些线程在交替地执行并输出,导致输出结果呈现乱序的效果。时间片即CPU分配给各个程序的时间。每个线程被分配一个时间片,在当前的时间片内CPU去执行线程中的任务。需要注意的是,CPU在不同的线程上进行切换是需要耗时的,所以并不是创建的线程越多,软件运行效率就越高,相反,线程数过多反而会降低软件的执行效率。1.2.4 执行start()的顺序不代表执行run()的顺序

注意,执行start()方法的顺序不代表线程启动的顺序。创建测试用的项目名称为z,MyThread.java类代码如下:package extthread;public class MyThread extends Thread { private int i; public MyThread(int i) { super(); this.i = i; } @Override public void run() { System.out.println(i); }}

运行类Test.java代码如下:package test;import extthread.MyThread;public class Test { public static void main(String[] args) { MyThread t11 = new MyThread(1); MyThread t12 = new MyThread(2); MyThread t13 = new MyThread(3); MyThread t14 = new MyThread(4); MyThread t15 = new MyThread(5); MyThread t16 = new MyThread(6); MyThread t17 = new MyThread(7); MyThread t18 = new MyThread(8); MyThread t19 = new MyThread(9); MyThread t110 = new MyThread(10); MyThread t111 = new MyThread(11); MyThread t112 = new MyThread(12); MyThread t113 = new MyThread(13); t11.start(); t12.start(); t13.start(); t14.start(); t15.start(); t16.start(); t17.start(); t18.start(); t19.start(); t110.start(); t111.start(); t112.start(); t113.start(); }}

程序运行结果如图1-14所示。图1-14 线程启动顺序与start()执行顺序无关

使用代码: MyThread thread = new MyThread(); thread.start();

启动一个线程后,JVM直接调用MyThread.java类中的run()方法。1.2.5 实现Runnable接口

如果想创建的线程类已经有一个父类了,就不能再继承自Thread类,因为Java不支持多继承,所以需要实现Runnable接口来解决这样的情况。

创建项目t2,继续创建一个实现Runnable接口的MyRunnable类,代码如下:package myrunnable;public class MyRunnable implements Runnable {@Override public void run() { System.out.println("运行中!"); }}

如何使用这个MyRunnable.java类呢?这就要看一下Thread.java的构造函数了,如图1-15所示。

在Thread.java类的8个构造函数中,有5个可以传递Runnable接口。为了说明构造函数支持传入一个Runnable接口的对象,运行如下类代码:public class Run { public static void main(String[] args) { Runnable runnable=new MyRunnable(); Thread thread=new Thread(runnable); thread.start(); System.out.println("运行结束!"); }}图1-15 Thread.Java的构造函数

程序运行结果如图1-16所示。

图1-16所示的输出结果没有什么特殊之处。图1-16 程序运行结果1.2.6 使用Runnable接口实现多线程的优点

使用继承Thread类的方式来开发多线程应用程序在设计上是有局限性的,因为Java是单根继承,不支持多继承,所以为了改变这种限制,可以使用实现Runnable接口的方式来实现多线程技术。下面来看使用Runnable接口必要性的演示代码。

创建测试用的项目moreExtends,首先来看业务A类,代码如下:package service;public class AServer { public void a_save_method() { System.out.println("a中的保存数据方法被执行"); }}

再来看业务B类,代码如下:package service;public class BServer1 extends AServer,Thread{ public void b_save_method() { System.out.println("b中的保存数据方法被执行"); }}

BServer1.java类不支持在extends关键字后写多个类名,即Java并不支持多继承的写法,所以在代码“public class BServer1 extends AServer,Thread”处出现如下异常信息:Syntax error on token "extends", delete this token

这时就有使用Runnable接口的必要性了,创建新的业务B类,代码如下:package service;public class BServer2 extends AServer implements Runnable { public void b_save_method() { System.out.println("b中的保存数据方法被执行"); } @Override public void run() { b_save_method(); }}

程序不再出现异常,通过实现Runnable接口,可间接地实现“多继承”的效果。

另外,需要说明的是Thread.java类也实现了Runnable接口,如图1-17所示。图1-17 Thread.Java类实现Runnable接口

这意味着构造函数Thread(Runnable target)不仅可以传入Runnable接口的对象,而且可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他线程进行调用,示例代码如下:public class Test { public static void main(String[] args) { MyThread thread = new MyThread(); //MyThread是Thread的子类,而Thread是Runnable实现类 //所以MyThread也相当于Runnable的实现类 Thread t = new Thread(thread); t.start(); }}

在非多继承的情况下,使用继承Thread类和实现Runnable接口两种方式在取得程序运行的结果上并没有什么太大的区别,一旦出现“多继承”的情况,则采用实现Runnable接口的方式来处理多线程的问题是很有必要的。1.2.7 实现Runnable接口与继承Thread类的内部流程

使用如下代码以实现Runnable接口法启动一个线程的执行过程和在前面章节使用继承Thread类启动一个线程时的执行过程是不一样的:MyRunnable run = new MyRunnable();Thread t = new Thread(run);t.start();

JVM直接调用的是Thread.java类的run()方法,该方法源代码如下:@Overridepublic void run() { if (target != null) { target.run(); }}

在方法中判断target变量是否为null,不为null则执行target对象的run()方法,target存储的对象就是前面声明的MyRunnable run对象,对Thread构造方法传入Runnable对象,再结合if判断就可以执行Runnable对象的run()方法了。变量target是在init()方法中进行赋值初始化的,核心源代码如下:private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { …… this.target = target; ……}

而方法init()是在Thread.java构造方法中被调用的,源代码如下:public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0);}

通过分析JDK源代码可以发现,实现Runnable接口法在执行过程上相比继承Thread类法稍微复杂一些。1.2.8 实例变量共享造成的非线程安全问题与解决方案

自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多个线程之间交互时是很重要的技术点。

1.不共享数据的情况

不共享数据的情况如图1-18所示。图1-18 不共享数据的情况

下面通过一个示例来看一下不共享数据的情况。

创建实验用的Java项目,名称为t3,MyThread.java类代码如下:public class MyThread extends Thread { private int count = 5; public MyThread(String name) { super(); this.setName(name);//设置线程名称 } @Override public void run() { super.run(); while (count > 0) { count--; System.out.println("由 " + this.currentThread().getName() + " 计算,count=" + count); } }}

运行类Run.java代码如下:public class Run { public static void main(String[] args) { MyThread a=new MyThread("A"); MyThread b=new MyThread("B"); MyThread c=new MyThread("C"); a.start(); b.start(); c.start(); }}

程序运行结果如图1-19所示。

由图1-19可以看到,该程序一共创建了3个线程,每个线程都有各自的count变量,自己减少自己的count变量的值,这样的情况就是变量不共享,此示例并不存在多个线程访问同一个实例变量的情况。

如果想实现3个线程共同去对一个count变量进行减法操作,则代码该如何设计呢?

2.共享数据的情况

共享数据的情况如图1-20所示。图1-19 不共享数据情况的示例图1-20 共享数据的情况

共享数据的情况就是多个线程可以访问同一个变量,例如,在实现投票功能的软件时,多个线程同时处理同一个人的票数。

下面通过一个示例来看下共享数据的情况。

创建t4测试项目,MyThread.java类代码如下:public class MyThread extends Thread { private int count=5; @Override public void run() { super.run(); count--; //此示例不要用while语句,会造成其他线程得不到运行的机会 //因为第一个执行while语句的线程会将count值减到0 //一直由一个线程进行减法运算 System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count); }}

运行类Run.java代码如下:public class Run { public static void main(String[] args) { MyThread mythread=new MyThread(); Thread a=new Thread(mythread,"A"); Thread b=new Thread(mythread,"B"); Thread c=new Thread(mythread,"C"); Thread d=new Thread(mythread,"D"); Thread e=new Thread(mythread,"E"); a.start(); b.start(); c.start(); d.start(); e.start(); }}

程序运行结果如图1-21所示。

从图1-21可以看到,A线程和B线程输出的count值都是3,说明A和B同时对count进行处理,产生了“非线程安全”问题。而我们想要得到输出的结果却不是重复的,应该是依次递减的。

出现非线程安全的情况是因为在某些JVM中,count--的操作要分解成如下3步,(执行这3个步骤的过程中会被其他线程所打断):

1)取得原有count值。

2)计算count-1。

3)对count进行赋值。

在这3个步骤中,如果有多个线程同时访问,那么很大概率出现非线程安全问题,得出重复值的步骤如图1-22所示。图1-21 共享变量值重复了,出现线程安全问题图1-22 得出重复值的步骤

A线程和B线程对count执行减1计算后得出相同值4的过程如下。

1)在时间单位为1处,A线程取得count变量的值5。

2)在时间单位为2处,B线程取得count变量的值5。

3)在时间单位为3处,A线程执行count--计算,将计算后的值4存储到临时变量中。

4)在时间单位为4处,B线程执行count--计算,将计算后的值4也存储到临时变量中。

5)在时间单位为5处,A线程将临时变量中的值4赋值给count。

6)在时间单位为6处,B线程将临时变量中的值4也赋值给count。

7)最终结果就是A线程和B线程得到相同的计算结果4,非线程安全问题出现了。

其实这个示例就是典型的销售场景,5个销售员,每个销售员卖出一个货品后不可以得出相同的剩余数量,必须在每个销售员卖完一个货品后,其他销售员才可以在新的剩余物品数上继续减1操作,这时就需要使多个线程之间进行同步操作,即用按顺序排队的方式进行减1操作,更改代码如下:public class MyThread extends Thread { private int count=5; @Override synchronized public void run() { super.run(); count--; System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count); }}

重新运行程序后,便不会出现值一样的情况了,如图1-23所示。图1-23 方法调用被同步

通过在run()方法前加入synchronized关键字,使多个线程在执行run()方法时,以排队的方式进行处理。一个线程调用run()方法前,先判断run()方法有没有被上锁,如果run()方法被上锁,则说明其他线程正在调用run()方法,必须等其他线程对run()方法调用结束后,该线程才可以执行run()方法,这样也就实现了排队调用run()方法的目的,从而实现按顺序对count变量减1的效果。synchronized可以对任意对象及方法加锁,而加锁的这段代码称为“互斥区”或“临界区”。

当一个线程想要执行同步方法里面的代码时,线程会首先尝试去申请这把锁,如果能够申请到这把锁,那么这个线程就会执行synchronized里面的代码。如果不能申请到这把锁,那么这个线程就会不断尝试去申请这把锁,直到申请到为止,而且多个线程会同时去争抢这把锁。1.2.9 Servlet技术造成的非线程安全问题与解决方案

非线程安全问题主要指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序执行流程。下面通过一个示例来学习如何解决因Servlet技术造成的非线程安全问题。

创建t4_threadsafe项目,以实现非线程安全的环境。LoginServlet.java代码如下:package controller;//本类模拟成一个Servlet组件public class LoginServlet { private static String usernameRef; private static String passwordRef; public static void doPost(String username, String password) { try { usernameRef = username; if (username.equals("a")) { Thread.sleep(5000); } passwordRef = password; System.out.println("username=" + usernameRef + " password=" + password); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }}

线程ALogin.java代码如下:package extthread;import controller.LoginServlet;public class ALogin extends Thread { @Override public void run() { LoginServlet.doPost("a", "aa"); }}

线程BLogin.java代码如下:package extthread;import controller.LoginServlet;public class BLogin extends Thread { @Override public void run() { LoginServlet.doPost("b", "bb"); }}

运行类Run.java代码如下:public class Run { public static void main(String[] args) { ALogin a = new ALogin(); a.start(); BLogin b = new BLogin(); b.start(); }}

程序运行结果如图1-24所示。图1-24 线程非安全

运行结果是错误的,在研究问题的原因之前,首先要知道两个线程向同一个对象的public static void doPost(String username,String password)方法传递参数时,方法的参数值不会被覆盖,方法的参数值是绑定到当前执行线程上的。

执行错误结果的过程如下。

1)在执行main()方法时,执行的结构顺序如下:ALogin a = new ALogin();a.start();BLogin b = new BLogin();b.start();

这样的代码被顺序执行时,很大概率是ALogin线程先执行,BLogin线程后执行,因为ALogin线程是首先执行start()方法的,并且在执行a.start()之后又执行BLogin b=new BLogin(),实例化代码需要耗时,更增大了ALogin线程先执行的概率。

但如果BLogin线程的确得到了先执行的机会,那么运行结果有可能出现两种:

运行结果1:b bba aa

运行结果2:a bba aa

2)ALogin线程执行了public static void doPost(String username,String password)方法,对username和password传入值a和aa。

3)ALogin线程执行usernameRef=username语句,将值a赋值给usernameRef。

4)ALogin线程执行if(username.equals("a"))代码符合条件,执行Thread.sleep(5000)停止运行5s。

5)BLogin线程也执行public static void doPost(String username,String password)方法,对username和password传入值b和bb。

6)由于LoginServlet.java是单例的,只存在1份usernameRef和passwordRef变量,所以ALogin线程对usernameRef赋的a值被BLogin线程的值b所覆盖,usernameRef值变成b。

7)BLogin线程执行if(username.equals("a"))代码不符合条件,不执行Thread.sleep(5000),而继续执行后面的赋值语句,将passwordRef值变成bb。

8)BLogin线程执行输出语句,输出b和bb的值。

9)5s之后,ALogin线程继续向下运行,参数password的值aa是绑定到当前线程的,所以不会被BLogin线程的bb值所覆盖,将ALogin线程password的值aa赋值给变量passwordRef,而usernameRef还是BLogin线程赋的值b。

10)ALogin线程执行输出语句,输出b和aa的值。

以上就是对运行过程的分析。

需要注意的是,如果代码改成:ALogin a = new ALogin();BLogin b = new BLogin();a.start();b.start();

则BLogin线程先执行的比重会加大,并且输出如下两种结果的概率较大。

运行结果1:a bba aa

运行结果2:b bbb aa

解决“非线程安全”问题可以使用synchronized关键字,更改代码如下: synchronized public static void doPost(String username, String password) { try { usernameRef = username; if (username.equals("a")) { Thread.sleep(5000); } passwordRef = password; System.out.println("username=" + usernameRef + " password=" + password); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); }}

程序运行结果如图1-25所示。图1-25 排队进入方法线程安全了

在Web开发中,Servlet对象本身就是单例的,所以为了不出现非线程安全问题,建议不要在Servlet中出现实例变量。1.2.10 留意i--与System.out.println()出现的非线程安全问题

在前面章节中,解决非线程安全问题使用的是synchronized关键字,本节将通过案例去细化println()方法与i--联合使用时“有可能”出现的另外一种异常情况,并说明其原因。

创建名称为sameNum的项目,自定义线程MyThread.java代码如下:package extthread;public class MyThread extends Thread { private int i = 5; @Override public void run() { System.out.println("i=" + (i--) + " threadName=" + Thread.currentThread().getName()); //注意:代码i--;单独一行运行 //被改成在当前项目中的println()方法中直接进行输出 }}

运行类Run.java代码如下:package test;import extthread.MyThread;public class Run { public static void main(String[] args) { MyThread run = new MyThread(); Thread t1 = new Thread(run); Thread t2 = new Thread(run); Thread t3 = new Thread(run); Thread t4 = new Thread(run); Thread t5 = new Thread(run); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); }}

程序运行后还是会出现非线程安全问题,如图1-26所示。图1-26 非线程安全问题继续出现

本实验的测试目的是要说明:虽然println()方法在内部是同步的,但i--的操作是在进入println()之前发生的,所以发生非线程安全问题仍有一定的概率,如图1-27所示。所以,为了防止发生非线程安全问题,推荐使用同步方法。图1-27 println()方法在内部是同步的1.3 currentThread()方法

currentThread()方法可返回代码段正在被哪个线程调用。下面通过一个示例进行说明。

创建t6项目,创建Run1.java类代码如下:public class Run1 { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); }}

程序运行结果如图1-28所示。图1-28 Run1.java运行结果

该运行结果说明main()方法是被名为main的线程调用的。

继续实验,创建MyThread.java类,代码如下:public class MyThread extends Thread { public MyThread() { System.out.println("构造方法的打印:" + Thread.currentThread().getName()); } @Override public void run() { System.out.println("run方法的打印:" + Thread.currentThread().getName()); }}

运行类Run2.java代码如下:public class Run2 { public static void main(String[] args) { MyThread mythread = new MyThread(); mythread.start(); // mythread.run(); }}

程序运行结果如图1-29所示。

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

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载