并发编程的艺术--第四章-java并发编程基础

线程简介

启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light

Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,

并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

使用多线程的原因主要有以下几点:

(1)更多的处理器核心;

(2)更快的响应时间;

(3)更好的编程模型;

在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线
程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分

配时间片的数量要多于优先级低的线程。

注意:线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java

线程对于优先级的设定。

在给定的一个时刻,线程只能处于其中的一个状态。


线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换。


注意:Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程
阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在
java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于

阻塞的实现均使用了LockSupport类中的相关方法。

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这
意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调
用Thread.setDaemon(true)将线程设置为Daemon线程。

注意:Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。

Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行。

注意:在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

启动和终止线程

在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要

的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。

线程对象在初始化完成之后,调用start()方法就可以启动这个线程。

线程start()方法的含义:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用

start()方法的线程。

注意:启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程

序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起个名字。

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行
了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()
方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否
被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该
线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返

回false。

suspend()、resume()和stop()方法完成了线程的暂停、恢复和终

止工作,而且非常“人性化”。但是这些API是过期的,也就是不建议使用的。

不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资
源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结
一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,

因此会导致程序可能工作在不确定状态下。

注意:正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建

议使用的过期方法,而暂停和恢复操作可以用后面提到的等待/通知机制来替代。

中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互
方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变

量来控制是否需要停止任务并终止该线程。

这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地

将线程停止,因此这种终止线程的做法显得更加安全和优雅。

线程间通信

一个线程看到的变量并不一定是最新的?

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个
变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是
可以拥有一份拷贝,这样做的目的是加速程序的执行。

关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要
从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问

的可见性。

过多地使用volatile是不必要的,因为它会降低程序执行的效率。

关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程
在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性

和排他性。

本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个
线程获取到由synchronized所保护对象的监视器。

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用
时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获
取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED
状态。


等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类

java.lang.Object上。


等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B
调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而
执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的
关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

调用wait()、notify()以及notifyAll()时需要注意的细节,如下:

1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的
等待队列。
3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或
notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。

4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()
方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为
BLOCKED。

5)从wait()方法返回的前提是获得了调用对象的锁。

从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从

wait()方法返回时能够感知到通知线程对变量做出的修改。


等待/通知的经典范式:该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。

等待方遵循如下原则:
1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
对应的伪代码如下。


通知方遵循如下原则:
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
对应的伪代码如下。


管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要
用于线程之间的数据传输,而传输的媒介内存

管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、
PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。

对于Piped类型的流,必须先要进行绑定,也就是调用connect()方法,如果没有将输入/输

出流绑定起来,对于该流的访问将会抛出异常。


如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才
从thread.join()返回。
线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long
millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时
时间里没有终止,那么将会从该超时方法中返回。

每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程
终止后,才从join()方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结
束通知)。

JDK中Thread.join()方法的源码(进行了部分调整)。


ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这
个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个

线程上的一个值。

可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。

线程应用实例

等待超时模式

开发人员经常会遇到这样的方法调用场景:调用一个方法时等待一段时间(一般来说是给
定一个时间段),如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,
超时返回默认结果。

等待/通知的经典范式,即加锁、条件循环和处理逻辑3个步骤,而这种范式无法做到超时等待。

超时等待的加入,只需要对经典范式做出非常小的改动,改动内容如下所示:

假设超时时间段是T,那么可以推断出在当前时间now+T之后就会超时。
定义如下变量。
·等待持续时间:REMAINING=T。
·超时时间:FUTURE=now+T。
这时仅需要wait(REMAINING)即可,在wait(REMAINING)返回之后会将执行:
REMAINING=FUTURE–now。如果REMAINING小于等于0,表示已经超时,直接退出,否则将
继续执行wait(REMAINING)。
上述描述等待超时模式的伪代码如下。



一个简单的数据库连接池示例

连接池的定义:它通过构造函数初始化连接的最大上限,通过一个双向队列
来维护连接,调用方需要先调用fetchConnection(long)方法来指定在多少毫秒内超时获取连接,
当连接使用完成后,需要调用releaseConnection(Connection)方法将连接放回线程池。

线程池技术:它预先创建了若干数量的线程,并且不能由用户
直接对线程的创建进行控制,在这个前提下重复使用固定或较为固定数目的线程来完成任务
的执行。这样做的好处是,一方面,消除了频繁创建和消亡线程的系统资源开销,另一方面,
面对过量任务的提交能够平缓的劣化。

一个简单的线程池接口定义:


线程池的本质就是使用了一个线程安全的工作队列连接工作者线程和客户端
线程,客户端线程将任务放入工作队列后便返回,而工作者线程则不断地从工作队列上取出
工作并执行。当工作队列为空时,所有的工作者线程均等待在工作队列上,当有客户端提交了
一个任务之后会通知任意一个工作者线程,随着大量的任务被提交,更多的工作者线程会被

唤醒。

一个基于线程池技术的简单Web服务器

大部分Web服务器都是支持并发访问的。

随着线程池中线程数量的增加,SimpleHttpServer的吞吐量不断增大,响应时间

不断变小,线程池的作用非常明显。

但是,线程池中线程数量并不是越多越好,具体的数量需要评估每个任务的处理时间,以
及当前计算机的处理器能力和数量。使用的线程过少,无法发挥处理器的性能;使用的线程过
多,将会增加系统的无故开销,起到相反的作用。




















猜你喜欢

转载自blog.csdn.net/qq_40722284/article/details/80470539