第4章 Java并发编程基础

前言

本章主要讲解Java的线程Thread,线程的6个状态,Daemon守护线程,线程的生存周期操作(构造、启动、中断、终止),以及线程的几种通信方式:volatile和synchronized提供的内存可见性保障,等待/通知机制wait/notify,管道流,Thread.join() 以及 ThreadLocal变量。

最后还示例了线程池、数据库连接池的简单实现,篇幅较大,暂时未补充,需要自己阅读书本。

第4章 Java并发编程基础

一、线程

线程是现代操作系统调度的最小单位,拥有各自的计数器、堆栈和局部变量等属性,能够访问共享的内存变量。

实际上Java程序天生就是多线程程序,执行main()会启用一个主线程,同时还会启用其他的一些辅助线程。

1.1. 线程优先级(priority)

现代操作系统基本采用时分调度线程,会分出多个时间片,一个线程会分配到若干时间片,时间片用完了就会发生线程调度,等待下次分配。

线程优先级决定分配到的时间片和处理器资源多少的线程属性。优先级高的线程分配的时间片数量更多。

Java线程中使用一个整型的变量priority来控制优先级,范围是1~10,线程构建的时候使用setPriority(int)修改优先级,优先级默认是5。

设置线程优先级的原则:

  • 针对频繁阻塞(休眠或I/O操作)的线程需要设置较高优先级。
  • 偏重计算(需要较多CPU时间)的线程设置较低优先级,从而避免处理器被独占。

但实际上有些JVM或者操作系统会无视Priority的设定,所以线程优先级不能作为程序正确性的依赖

1.2. 线程的状态

表4-1 Java线程的状态(6种)

状态名称 状态 说明
NEW 初始态 线程被构建,但是还没调用start()方法
RUNNABLE 运行态 Java线程将操作系统中的就绪态和运行态统称为“运行中”
BLOCKED 阻塞态 线程阻塞于锁(调用同步方法时没获取到锁)(注意:阻塞在current包的Lock接口中的线程是等待态而不是阻塞态)
WAITING 等待态 线程wait()后,当前线程需要等待其他线程做出一些特定动作(通知或中断)才能返回到运行态
TIME_WAITING 超时等待态 可以在指定时间内自行返回的特殊WAITING
(其他线程在规定时间内未回复的话则自行返回进行下一步操作)
TERMINGATED 终止态 表示当前线程已经执行完毕

图4-1 Java线程状态变迁

1.3. Daemon线程(守护线程)

Daemon线程是一种支持型线程,主要被用作程序的后台调度和支持性工作。

使用threadA.setDaemon(true)将线程设置为Daemon线程(Daemon需要在启动线程前设置)

  • 当JVM中不存在非Daemon线程的时候,JVM会退出。此时不一定会执行Daemon线程中的finally块,因此不能依靠Daemon线程中的finally来确保逻辑正确。

二、启动和终止线程

2.1. 构造线程

运行线程之前首先要构造一个线程对象,并且初始化线程属性。如线程所属的线程组、线程优先级、是否Daemon线程等信息。

在Thread.init()的源代码中,新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent的属性是否为Daemon、优先级和加载资源的contextClassLoader以及科技城ThreadLocal,同时分配一个唯一的线程ID。

2.2. 启动线程

  • 调用start()方法即可启动。

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

2.3. 中断(interrupt)

中断可以理解为线程的一个标志位属性。它表示一个运行中的线程是否被其他的线程通知进行了中断操作(不不会终止线程)。

interrupt():其他线程通过调用该线程的interrupt()方法对其进行中断操作。

isInterrupted():线程通过检查自身是否被中断来响应,通过调用isInterrupted() 来判断是否被中断。

Thread.interrupted():复位当前线程的中断标志位(重置为false)。

如果线程已经处于结束态,则即使该线程被中断过,其中断标志位依旧是false。

InterruptedException:当线程处于阻塞状态(因被调用了wait(),join(),sleep()而进入阻塞)时,调用interrupt() ,因为没有占用CPU运行的线程是不可能给自己中断状态置位的,JVM会先清楚中断标志位,然后产生InterruptedException异常(不会终止线程)。(可以利用接收这个异常终止阻塞的线程)

2.4. 安全地终止线程

2.4.1. 终止处于运行状态的线程

  • 利用中断状态标志位,做中断操作来取消或停止任务。

  • 使用boolean变量控制是否需要停止任务并终止该线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private class Runner implments Runnable {
private long i;
privatge volatile boolean on = true; //volatile的boolean控制标志

@Override
public void run() {
// boolean变量为真时继续执行
// 线程没被中断(Isterrupted()返回false)之前继续执行
// 当外部执行cancel()或interrupt()时终止线程
while (on && !Thread.currentThread().isInterrupted() ) {
i++;
}
}

// boolean取消操作,供外部终止线程使用
public void cancel() {
on = false;
}
}

2.4.2. 终止处于“阻塞状态”的线程

  • 同样用interrupt():

    当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常。

1
2
3
4
5
6
7
8
9
10
@Override
public void run() {
try {
while (true) {
// 执行任务...
}
} catch (InterruptedException ie) {
// 由于产生InterruptedException异常,退出while(true)循环,线程终止!
}
}

2.5. 废弃的suspend()、resume()、和stop()

suspend:暂停

resume:恢复

stop:终止

由于强制操作导致的资源不完全释放的副作用(死锁等),所以废弃。建议用回以上的等待/通知的机制替代。

三、线程间通信

让线程之间能后互相配合的完成工作。

3.1. volatile和synchronized关键字

在程序执行过程中,每个线程可以拥有变量的拷贝,而且看到的不一定是最新的变量值,这时候就需要volatile和synchronized关键字。

volatile可以告知程序任何对该变量的访问必须要从共享内存中获取,并且其修改必须同步刷新到共享内存,以保证该变量在线程之间的可见性。(不保证原子性)

synchronized确保同一时刻只能有一个线程处于方法或者同步块中,保证了线程对变量访问的可见性和排他性。

每个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进去同步块或者同步方法,没有获取到监视器的线程将会被阻塞在入口,进入BLOCKED状态和同步队列(释放操作会唤醒同步队列中的线程,使其重新尝试获取监视器)。

3.2.等待/通知机制(wait/notify)

简单的生产者/消费者模式:一个线程修改了一个对象的值,而另一个线程感知到了变化,进而进行相应操作(开始于一个生产者线程,执行于另一个消费者线程)。隔离了做什么(What)和怎么做(How),在功能上解耦。

相关监视器方法:(等待/通知的相关方法都定义在java.lang.Object类上,执行这些方法前,需要配合synchronzied对调用的对象加锁实现等待/通知机制,这些锁是针对同一个Object对象进行操作的,)

方法名 描述 备注
notify() 通知一个在对象上等待的线程,使其从wait()方法返回,前提是本线程已获得该对象的锁 notify不代表被通知线程获得锁,需要等notify的主动线程释放锁后,才有机会获得锁;
而从wait()返回的前提就是获取到了锁
notifyAll() 通知所有等待在该对象上的锁 通知后,所有在该对象上wait()的线程进入同步队列(BLOCKED状态)依次等待获取锁的机会
wait() 本线程进入WAITING状态,只有等待别的线程通知或被中断才可返回 调用wait后,该线程会释放锁,进入等待队列
需要try/catch检查InterruptException异常
wait(long) 超时等待。n毫秒后若没有通知的话就自主返回
wait(long, int) 纳秒级超时等待

等待/通知机制的经典范式

等待方(生产者):

1
2
3
4
5
6
synchronized(Object 对象) {	// 1. 获得对象锁
while (条件不满足) {
对象.wait(); // 2. 如果条件不满足,则调用wait(),被通知后仍然要检查条件
}
对应的处理逻辑 // 3. 直到条件满足后才执行相应逻辑
}

通知方(消费者):

1
2
3
4
synchronized(Object 对象) {	// 1. 获取对象锁
改变等待方的条件; // 2. 改变等待方条件,使其能退出自旋检查
对象.notifyAll(); // 3. 通知等待方,使其退出Waiting态
}

3.3. 管道.输入/输出流(Piped)

管道输入/输出流用于线程间数据传输,媒介为内存(区别于文件IO)

字节流:PipedOutputStreamPipedInputStream

字符流:PipedReaderPipedWriter

  • 在使用时一定要先将Output和Input绑定connect起来,否则会抛出IO异常:
1
2
3
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
out.connect(in); // 将输出流和输入流进行连接,否则在使用时会抛出IOException

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Piped {
public static void main(String[] args) throws Exception {

PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();

// 将输出流和输入流进行连接,否则在使用时会抛出IOException
out.connect(in);
Thread printThread = new Thread(new Print(in), "PrintThread");
printThread.start();
int receive = 0;
try {
while ((receive = System.in.read()) != -1) {
out.write(receive);
}
} finally {
out.close();
}
}

static class Print implements Runnable {
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
public void run() {
int receive = 0;
try {
while ((receive = in.read()) != -1) { // 从PipedReader里读取)
System.out.print((char) receive);
}
} catch (IOException ex) {}
}
}
}

3.4. Thread.join()

当属Thread类的方法Thread.join()

涵义:当线程A执行了threadB.join(),其意为:当前线程A等待threadB线程终止后才从thread.join()返回也就是确保当前线程必须在threadB后继续执行

此外也提供了Thread.join(long millis)Thread.join(long millis, int nanos)超时等待方法

使用时要try/catch检测InterruptException异常

Java源码的Thread.join()大致如下:

1
2
3
4
5
6
7
public final synchronized void join() throw InterruptException {
// 条件不满足,继续等待
while (isAlive()) {
wait(0);
}
// 条件符合,返回
}
  • 可以发现 join() 其实也是等待/通知机制的一种线程终止时会调用自身的notifyAll()方法,唤醒等待在本线程对象的所有线程。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Join {
public static void main(String[] args) throws Exception {
Thread previous = Thread.currentThread();
for (int i = 0; i < 10; i++) {
// 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + " terminate.");
}

static class Domino implements Runnable {
private Thread thread;
public Domino(Thread thread) { //在此处引入别的进程
this.thread = thread;
}
public void run() {
try {
thread.join(); // 此处调用Thread.join()
} catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}

3.5. ThreadLocal 线程变量

ThreadLocal线程变量:是一个以 \ 的key-value结构(Entry),存放在线程自己的ThreadLocalMap中。(一个线程可以拥有多个ThreadLocal变量)

这个Map被附带绑定在线程上,也就是说一个线程可以通过一个ThreadLocal对象查询到绑定在该线程上的一个值。(每个线程的static ThreadLocal都不一样)

构造后,需先使用set(T)设置值,再使用get()方法获取值,若不调用set(T),会抛出空指针异常。 如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法:这样如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回。

注意:ThreadLocal的内存泄漏问题

ThreadLocalMap的ThreadLocal的key用的是弱引用,这样子的话,在GC的时候就一定会被回收掉,导致只有value还被ThreadLocal强引用,导致内存泄漏。所以使用过后务必要进行remove()

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Profiler {

private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
//重写initialValue()方法,第一次get()方法调用时会进行初始化(前提是若set方法没有调用),每个线程会调用一次
// 可以不重写,那么声明ThreadLocal变量写作不用括号重写:XX = new ThreadLocal<Long>();
// 并且不重写时,不调用set就get的话,会抛出NullPointerException
protected Long initialValue() {
return System.currentTimeMillis();
}
};

public static final void begin() {
TIME_THREADLOCAL.set(System.currentTimeMillis()); // set(T)
}

public static final long end() {
return System.currentTimeMillis() - TIME_THREADLOCAL.get(); //get()
}

public static void main(String[] args) throws Exception {
Profiler.begin(); // set(T)
TimeUnit.SECONDS.sleep(1);
System.out.println("Cost: " + Profiler.end() + " mills"); //get()
}
}

四、补充

书P106-P117说明了:基本的数据库连接池实现、线程池的基本实现、基于线程池的Web项目实现。注意查看

4.1. 数据库连接池

4.2. 线程池

4.3. 基于线程池技术的简单Web服务器

本文标题:第4章 Java并发编程基础

文章作者:Aaron.H

发布时间:2018年05月01日 - 20:05

最后更新:2018年09月18日 - 13:09

原始链接:https://uncleaaron.github.io/Blog/Java/Java并发编程艺术/第4章-Java并发编程基础/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。