Java 并发编程基础#
《并发编程的艺术》阅读笔记第四章,重点在于线程间的通信

一、线程简介#
首先对线程做一个简单的介绍
使用多线程的原因:
1.更多的处理器核心:一个线程在一个时刻只能运行在一个处理器核心上
2.更快的响应时间
3.更好的编程模型
JAVA 程序运行所需线程(jdk1.8)
线程优先级
操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配,线程分配到的时间片多少就决定了线程使用处理器资源的多少。
有些操作系统会忽略对线程优先级的设定
在 Java 线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程
线程的状态
6种,在给定的时刻只能处于一种状态
NEW:初始状态,线程被构建,但还没有调用 start 方法RUNNABLE:运行状态,java 线程将 == 就绪和运行 == 两种状态统称为运行状态- ``BLOCKED`:阻塞状态,表明线程阻塞于锁
WAITING:等待状态,等待其他线程的通知或中断TIME_WAITING:超时等待状态,可以在指定的时间自行返回TERMINATED:终止状态,表示当前线程已经执行完毕
线程状态变迁
Java 将操作系统中的运行和就绪两个状态合并称为运行状态。
阻塞状态是线程阻塞在进入
synchronized关键字修饰的方法或代码块(获取锁)时的状态但是阻塞在
java.concurrent包中 Lock 接口的线程状态却是等待状态,因为java.concurrent包中 Lock 接口对于阻塞的实现均使用了LockSupport类中的相关方法
Daemon 线程(守护线程)
指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程。不属于程序中不可或缺的部分。
当一个 java 虚拟机中不存在非守护线程时,虚拟机将会退出。可以通过设置Thread.setDaemon(true)将线程设置为 daemon 线程,必须在线程启动前设置
ps. 在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑,可能不执行
二、启动和终止线程#
一个新构造的线程对象是 == 由其父线程 == 来进行空间分配的,而子线程的各种属性 == 继承自父线程,同时还会分配一个唯一的 ID。==
看Thread.init()源码
jdk1.8的源码中添加了很多关于ThreadGroup的判断操作
估计是对线程的安全性和异常情况做了更完善的区分
** 启动线程:**start 方法:当前线程同步告知虚拟机:只要线程规划器空闲,应立即启动调用 start 方法的线程,== 最好在线程创建之前设置线程名称,有利于排查错误 ==
中断:
可以理解为线程的一个标识符属性,它标识一个运行中的线程是否被其他线程进行了中断操作。是一种简便的线程间交互方式。
线程通过isInterrupted()方法判断是否被中断,也可以调用静态方法Thread.interrupted()对当前的线程中断标识位进行复位。
从java的API可以看出,许多声明抛出InterruptedException的方法在抛出这个异常前,java虚拟机会先将该线程的中断标志位清除,然后再抛异常。此时调用isInterrupted返回false
- 当线程处于等待状态或者有超时的等待状态时(
TIMED_WAITING,WAITING)我们可以通过调用线程的·interrupt()方法来中断线程的等待,此时线程会抛InterruptedException异常。 - 但是当线程处于
BLOCKED状态或者RUNNABLE(RUNNING)状态时,调用线程的interrupt()方法也只能将线程的状态位设置为true。停止线程的逻辑需要我们自己去实现。
过期 API:suspend(),resume(),stop()
原因:占有资源的时候容易发生死锁问题,让线程共工作在不确定状态下
终止线程的做法: 一种方法可以使用interrupt()方法,或者利用一个 boolean 变量将线程终止,使线程在终止时有机会去清理资源
三、线程间通信(重点)#
1、volatile 和 synchronized 关键字#
volatile:告知程序任何对该变量的访问均需要从共享内存中获取,而对他的改变必须同步刷新回主内存,他能保证所有线程对变量访问的可见性。
synchronized:确保多个线程在同一个时刻,只能有一个线程处于方法或同步块中,保证了线程对变量访问的可见性和排他性。不能保证重排序
关于synchronized:本质是对一个对象的监视器(monitor)的获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到有synchronized所保护对象的监视器。任意一个对象都拥有自己的监视器。
任意线程对 Object 的访问,首先要获得 Object 的监视器。如果获取失败,线程进入同步队列,线程状态变为
BLOCKED。当访问 Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
2、等待 / 通知机制(生产者 - 消费者模型)#
等待 / 通知机制,是指一个线程 A 调用了对象 O 的
wait方法进入等待状态,而另一个线程 B 调用了对象 O 的notify或notifyall方法,线程 A 收到通知后从对象 O 的wait方法返回,进而执行后续操作。
此处的代码示例失效,wait线程没有正常被唤醒,也没有发生重排序
原因时因为获取的锁不是同一个锁,因此导致无法正常解锁和加锁,需要把同步的对象设置为public static确保获得对象是同一个
///示例不是这个问题,是因为一开始没有wait...、😂
注意:
(1)使用wait、notify、notifyAll时需要先对调用对象加锁
(2)调用 wait 方法后,线程状态由RUNNING变为WAITING,并将当前线程防止到对象的等待队列
(3)notify或notifyAll方法调用后,等待线程依旧不会从wait返回,需要调用notify或notifyAll的线程释放锁之后,等待线程才有机会从wait返回
(4)notify方法将等待队列中的等待线程从等待队列中移到同步队列中,被移动的线程从WAITING变为BLOCKED
(5)从wait方法返回的前提是获得了调用对象的锁
3、等待 / 通知经典范式#
等待方:1.获取对象的锁
2.如果条件不满足,那么调用对象的wait方法,被通知后仍要检查条件
3.条件满足则执行对应的逻辑
伪代码:
synchronized(对象){
while(条件不满足){
对象.wait();
}
对应的处理逻辑
}
通知方:1.获得对象的锁
2.改变条件
3.通知所有等待在对象上的线程
伪代码:
synchronized(对象){
改变条件
对象.notify();
}
4、管道输入输出流#
管道输入 / 输出流和 普通的文件输入 / 输出流 或者 网络输入 / 输出流 不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存
PipedOutputStream、PipedInputStream(面向字节数据)
PipedReader、PipedWriter(面向字符)
对于piped类型的流,使用时必须先调用connect方法进行绑定,否则会抛出异常
管道输入缓冲区大小默认 1024 字节
写示例代码的注意事项:
1. write写入 read读出
2. 打印的时候用的是System.out.print 而不是 System.out.prineln.后者会最后加回车
5、Thread.join()#
含义:当前线程等待 thread 线程终止之后才从thread.join返回。
另外还有两个join(long millis)和join(long millis,int nanos)具备 == 超时特性 == 的方法(如果线程在给定的时间内没有终止,那么将会从该超时方法返回)。
join()等价于wait(0),只要等指定线程锁释放就可以抢占锁
如果指定时间后,就需要指定时间之后才能重新拥有锁(wait加参数本质上还是定时调用notify()方法,只是这段代码在JVM里面,比较底层)
join如果加参数,第一次代表按照参数设置wait唤醒时间,之后循环检测线程活性,如果线程始终存活,才开始传入设置值和当前时间的差值
6、ThreadLocal#
ThreadLocal,即线程变量,是一个以 ThreadLocal 对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个 ThreadLocal 对象查询到绑定在这个线程上的一个值。
是一个线程安全的局部变量
四、应用实例#
一、等待超时模式#
调用一个方法时等待一段时间(一般来说是给定一个时间段),如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。
public synchronized Object get(long mills) throws InterruptedException {
long future = System.currentTimeMillis() + mills;
long remaining = mills;
// 当超时大于0并且result返回值不满足要求
while ((result == null) && remaining > 0) {
wait(remaining);
remaining = future - System.currentTimeMillis();
}
return result;
}
典型案例:数据库连接池模式
源码略
CountDownLatch:
- countDownLatch 这个类使一个线程等待其他线程各自执行完毕后再执行。
- 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就 - 1,当计数器的值为 0 时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
二、线程池技术#
客户端可以通过 execute (Job) 方法将 Job 提交入线程池执行,而客户端自身不用等待 Job 的执行完成。除了 execute (Job) 方法以外,线程池接口提供了增大 / 减少工作者线程以及关闭线程池的方法。
这里工作者线程代表着一个重复执行 Job 的线程,而每个由客户端提交的 Job 都将进入到一个工作队列中等待工作者线程的处理。
线程池的本质就是使用了一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放入工作队列后便返回,而工作者线程则不断地从工作队列上取出工作并执行
典型案例:实现一个 web 服务器
源码略