购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第3章
Java多线程基础

本章主要介绍了使用Java语言进行多线程编程的相关核心类,包括Thread类、Runnable接口、ThreadLocal类等的用法,线程的执行状态跟踪方法和转换流程,以及介绍Java提供的用于实现线程安全的几个关键字的使用,包括synchronized关键字、volatile关键字和final关键字的设计原理和用法。

对于高并发Java应用系统设计而言,最基础的是对Java多线程的使用。在介绍Java多线程之前,首先介绍一下Java应用程序的执行原理。Java应用程序通常使用一个静态的main方法作为执行入口。Java虚拟机在加载和执行Java应用程序时,首先需要创建一个主线程来执行该应用程序,然后在主线程中找到该静态main方法并执行main方法的代码,从而开启该应用程序的执行。

对于普通的Java应用程序而言,在默认情况下,所有代码都是在主线程中按照代码的定义顺序来执行的。如果前一段代码执行需要很长时间,那么后面的代码就需要等待前面的代码执行完成,所以整个过程是串行化的。

如果前后的代码没有直接联系,如两个基于不同数据集合的计算任务,就可以在主线程中创建两个子线程来分别执行这两个任务,从而实现任务的并行执行,缩短整体的执行时间。对于Java企业级应用系统而言,特别是在高并发场景中,一般都会通过多线程来处理不同的用户请求,实现请求的并发处理。

3.1 线程的使用

在企业级Java应用系统设计当中,一般很少直接使用Thread类来创建子线程,通常是基于Java并发包的线程池框架Executor提供的API和相关工具类来进行多线程编程。Executor线程池框架在内部封装了Java的线程类Thread,通过Thread类来映射操作系统的线程,在应用代码中通过实现Runnable接口来定义需要执行的业务逻辑,从而实现了业务逻辑的定义和线程执行的解耦。

3.1.1 Thread类的使用

Thread类是Java语言提供的线程类,每个Thread类对象实例在执行时会对应到一个操作系统线程,其中Thread类对象实例的执行是通过调用其start方法来启动的。在启动执行之后,对应的操作系统线程会执行该Thread类对象实例的run方法,默认情况下Thread类的run方法什么都不做,故需要继承Thread类并重写run方法来定义业务逻辑。

Java多线程最简单的使用方法就是创建一个Thread类的继承类,实现其run方法来定义业务逻辑。然后再创建该继承类的对象实例并调用其start方法来开始执行,具体例子如下,该例子的作用是分别打印主线程和子线程的名字。

执行结果如下:

1.线程的名字

由执行结果可以看出,Java主线程的名字为main,子线程的名字为Thread-线程编号,如果存在很多子线程时,也可以自定义子线程的名字,从而方便跟踪线程的执行,示例如下:

执行结果如下所示,可以看到子线程名字已经变为了mythread。

2.线程等待join

在主线程中可以创建多个子线程来实现多个任务的同时执行,此时一般需要在主线程等待这些子线程的执行结果,从而在主线程进一步处理和汇总。在主线程等待多个子线程执行完成的做法是,可以在主线程通过子线程的对象实例来调用其join方法,使得主线程阻塞等待,直到子线程执行完成才返回。

join方法的用法具体例子如下,两个子线程分别计算两个大的数据集合的和,在主线程等待这两个计算结果,然后在主线程计算二者的差异。该例子的应用场景可以是计算两天的订单收入差额。

执行结果如下,值为-9。

3.线程暂停sleep与yield

在线程执行过程中,如果run方法内部是在一个无限循环中检查某种状态,如消费某个队列的数据或者进行重试某个操作直到成功,为了避免该线程一直占用CPU资源,可以调用sleep方法来暂停指定时间,让出CPU资源给其他线程使用。线程在调用sleep休眠之后是可以被其他线程中断的,故需要通过try/catch来捕获中断异常InterruptedException,其中sleep是Thread类的一个静态方法。

除使用sleep方法来使线程暂停外,还可以调用yield方法。与sleep方法类似,yield方法也是Thread类的一个静态方法。与sleep方法不同的是,yield方法是使调用该方法的线程让出CPU资源,然后让同等优先级的线程去竞争获取CPU资源来执行。注意这里去竞争CPU资源的线程包括调用yield方法的线程本身,以及其他跟这个线程优先级相同的线程,所以可能还是该线程会竞争获取到CPU资源并继续运行。

除此之外,yield方法也不能指定暂停的时间长短,而sleep方法可以指定暂停多长时间。如下为sleep的使用示例,每次调用sleep暂停1秒。

打印结果如下所示,每次耗时1000毫秒左右,即1秒左右。

yield方法由于不能指定暂停的时间,只是给同等优先级的线程提供竞争CPU的机会,并且自身也会继续参与竞争,所以不存在中断终止的情况,不需要捕获中断异常。示例如下:

打印结果如下所示。可以看到耗时都是0毫秒且瞬间打印了大量日志,说明该线程每次继续运行,yield方法只有在存在大量同等优先级的线程时,才可能看到暂停的效果。

4.线程优先级

由操作系统线程的知识可知,线程一般存在优先级,优先级越高的线程越容易获取CPU资源。Thread类通过setPriority方法来设置当前线程的优先级,值的范围为1~10,默认值为5,值越大,优先级越高。优先级越高,被JVM调度执行的可能性就越高。如果超出这个范围,则会抛出IlegalException的异常。

注意

由于Java的线程是依赖于底层操作系统的线程的,Java线程的优先级也受到底层操作系统的线程优先级的影响,Java线程的优先级不一定与底层操作系统的线程优先级对等。所以在Java程序设计当中,不推荐去设置Thread线程的优先级,保持默认优先级即可。同时,如果要控制两个线程的执行顺序,不能依赖线程优先级来控制,因为底层操作系统不一定按照Java定义的线程优先级来工作。

5.线程协作wait/notify

多个子线程之间如果不是独立存在,而是需要相互协作,如构成生产者消费者模型,此时可以用基于Object类的wait和notify方法来实现线程之间的协作。

编程技巧:实现线程之间的协作看似基础操作,在设计企业级的应用程序时也是依赖这些操作来实现的,如Dubbo框架的请求发送、等待响应与获取到响应映射到原始请求就是基于Object的wait和notify实现的,后面章节将详细分析。

例如,消费者线程需要等待生产者线程执行完才能开始执行,二者通过调用一个Object类型的对象实例lock的wait和notify来进行协作。即消费者线程调用wait等待生产者线程执行完,生产者线程执行完时,调用notify来通知消费者线程可以继续往下执行了。具体的实现如下:

执行结果为:

主要逻辑为:消费者Consumer先开始一些准备工作,刚开始由于生产者Producer还没有开始工作,故Consumer等待wait,之后Producer开始工作,10秒后工作完成并通知Consumer可以继续工作了。

3.1.2 Runnable接口的使用

在通过Thread类来定义子线程时,需要创建Thread类的子类并重写run方法来定义业务逻辑。由于Java语言的单继承特性,所以无法在该子类中继续继承其他类来复用其他类的代码。除此之外,通过继承Thread类和重写其run方法来定义业务逻辑的这种做法,将业务逻辑和Thread类耦合在一起。

为了解决以上这两个问题,Java提供了Runnable接口来实现Thread类和业务逻辑的解耦。有了Runnable接口之后,不用再以通过创建Thread类的子类的方式来定义执行逻辑。与此同时,利用Java接口支持多继承的特性,可以在Runnable接口的实现类中继续继承其他类或接口来添加更多其他功能。

关于Runnable接口用法的具体例子如下。定义Runnable接口的实现类,该实现类同时实现了两个接口,分别为Runnable接口和自定义的OtherInterface接口。然后在主线程中创建该实现类的一个对象实例,并且将该对象实例作为Thread类的构造函数参数来创建Thread类对象实现,最后直接调用Thread类的start方法启动线程,所以整个过程直接创建Thread类的对象实例即可,具体业务逻辑在Runnable接口的实现类定义如下:

执行结果如下:

可能有读者会好奇Thread内部是如何调用这个Runnable接口实现类的run方法的。其实在源码实现层面,Thread类自身也是实现了Runnable接口并在内部定义了一个Runnable对象属性,然后可以通过Thread类的构造函数传递一个Runnable对象实例来对其进行赋值。

在Thread类的run方法默认实现当中,当该Runnable对象属性不为null时,则调用该Runnable对象实例的run方法,具体实现如下:

编程技巧:对设计模式熟悉的读者可能会发现这其实是使用了静态代理模式。静态代理的定义是与目标对象实现相同的接口并在内部包含一个目标对象的引用,通过构造函数传入实际对象并赋值。所以Thread类是Runnable接口的一个代理类,代理增强的功能是使用一个子线程而不是使用主线程来执行这个任务。 Q6kAGexqOHFagnjEavtT5HlyiXfWjysNPBGnbgS7ZGtAD/X+8GJ1wHioW99DqBTg

点击中间区域
呼出菜单
上一章
目录
下一章
×