《java 并发编程的艺术》第4章 Java 并发编程基础
线程简介
什么是线程
什么是线程?现代操作系统最小的调度单元是线程,也叫轻量级进程。在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。
线程和进程的区别是什么呢?
- 地址空间:线程是进程内的一个执行单元,进程内的线程共享进程的地址空间,而不同进程都有自己独立的地址空间。
- 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源。
- 线程是处理器调度的基本单位,但进程不是。
使用多线程有什么好处呢?
- 更好的利用多核处理器。
- 业务并行处理,缩短响应时间。
- 更好的编程模型。
线程状态
Java 线程在生命周期中可能处于下表所示的 6 种不同的状态:
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被创建,但还没有调用 start() 方法 |
RUNNABLE | 运行状态,Java 将操作系统里的就绪和运行都成为“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程需要等待其他线程做成一些特定动作(通知或中断) |
TIME_WAITING | 带超时功能的等待状态 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程不同状态之间的变化图如下所示:
在实际中,可以使用 jstack 命令查看线程状态。
构造线程
运行线程之前要先构造一个线程对象,线程对象在构造时需要提供线程所需要的属性:线程组、优先级、名称等。下面的代码是 Thread.java 中对线程进行初始化的部分。
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
// 当前线程就是该线程的父线程
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
// 省略一些安全检查
this.group = g;
// 继承父线程的 daemon、priority 属性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
// 继承父线程的 inheritableThreadLocals
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
// 分配线程 ID
tid = nextThreadID();
}
启动线程
线程对象在初始化完成之后,调用 start() 就可以启动这个线程。start() 方法的含义是,当前线程告知 Java 虚拟机,只有线程规划器空闲,应立即启动调用 start() 方法的线程。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0();
start 方法主要是调用了 native 方法 start0,在 start0 主要是创建了操作系统线程,详细的内容可以看看Java线程源码解析之start。
理解中断
中断本质上是线程的一个标志位属性,表示运行中的线程是否被其他线程进行了中断操作。中断相关的方法有三个:
方法 | 说明 |
---|---|
public void interrupt() | 将中断状态设置为true的方法,比如线程t1可以通过调用interrupt方法将线程t2的中断状态置为true |
public static boolean interrupted() | 测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外) |
public boolean isInterrupted() | 测试线程是否已经中断。线程的中断状态不受该方法的影响 |
通常,中断的使用场景有以下几个:
- 点击某个桌面应用中的取消按钮时;
- 某个操作超过了一定的执行时间限制需要中止时;
- 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;
- 一组线程中的一个或多个出现错误导致整组都无法继续时;
- 当一个应用或服务需要停止时。
注意事项
- 线程优先级的设定是不靠谱的,很多操作系统完全忽略了 Java 线程对优先级的设定。
- Daemon 线程是一种支持性线程,当虚拟机中不存在非 Daemon 线程时,虚拟机就会退出。在构建 Daemon 线程时,不能依靠 finally 块来确保执行关闭或清理资源逻辑。
- 不推荐使用过期了的 suspend、resume、stop 方法,以 suspend 方法为例,在调用后线程不会释放已经占有的资源(比如锁)便进入睡眠状态,容易引发死锁问题。stop 方法在终结时不能保证线程的资源正常释放,因此会导致程序工作在不确定状态下。
如何正确地终止一个线程呢?使用中断操作或者标志位,中断也可以理解为线程的一个标志位属性。
线程间通信
volatile 和 synchronized
关键字 volatile 用来修饰变量,确保任何对该变量的读均需要从主内存获取,对变量的写都需要立即写回主内存,保证了线程对变量访问的可见性。
synchronized 可以用来修饰方法或同步块,确保多个线程在同一时刻只有一个线程处于方法或同步块中,保证了线程对变量访问的可见性和排他性。
我们先看一段代码:
public class LockTest {
public synchronized void testSync() {
System.out.println("testSync");
}
public void testSync2() {
synchronized(this) {
System.out.println("testSync2");
}
}
}
在这段代码中,分别使用 synchronized 对方法和语句块进行了同步,接下来我们使用 javac 编译后,再用 javap 命令查看其汇编代码:
public synchronized void testSync();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String testSync
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/qunar/fresh2017/LockTest;
public void testSync2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #5 // String testSync2
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 13: 0
line 14: 4
line 15: 12
line 16: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 this Lcom/qunar/fresh2017/LockTest;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class com/qunar/fresh2017/LockTest, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
这里我们省略了关键方法之外对常量池、构造函数等部分,对于 synchronized 方法,仅仅在其 class 文件的 access_flags 字段中设置了 ACC_SYNCHRONIZED 标志。对于 synchronized 语句块,分布在同步块的入口和出口插入了 monitorenter 和 monitorexit 字节码指令。
等待通知机制
在生产者消费者场景中,消费者在队列为空时,要等待生产者往队列里放数据后再处理,如何实现这样的功能呢?
while (queue is empty) {
Thread.sleep(1000);
}
doSomeThing();
上述这段代码的缺点是:当睡眠时间设的长了,难以保证及时性;当睡眠时间短了,线程切换的开销会很高。怎样才能做的更好呢?想办法让生产者在往队列中存数据之后,通知消费者去处理即可。这种等待通知的机制,可以通过 Java 内置的 wait/notify 机制来实现。
在 wait/notify 模式中,等待方遵循如下原则:
- 获取对象的锁。
- 如果条件不满足,那么调用对象的 wait 方法,被通知后仍然要检查条件。
- 条件满足则执行对应的逻辑。
对应的伪代码如下:
synchronized(对象) {
while (条件不满足) {
对象.wait();
}
对应的处理逻辑。
}
通知方遵循如下原则:
- 获得对象的锁。
- 改变条件。
- 通知所有等待在对象上的线程。
对应伪代码如下:
synchronized(对象) {
改变条件
对象.notifyAll();
}
管道输入输出流
管道输入输出流和普通的文件输入输出流或者网络输入输出流不同之处在于,它主要用于线程之间的数据传输,传输的媒介为内存。
管道输入输出流主要包括了以下四种具体实现:PipedOutputStream、PipedInputStream 和 PipedReader、PipedWriter,前两种面向字节,后两种面向字符。
public class Piped {
public static void main(String[] args) throws IOException {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
out.connect(in);
Thread thread = new Thread(new Print(in), "PrintThread");
thread.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;
}
@Override
public void run() {
int receive = 0;
try {
while (-1 != (receive = in.read())) {
System.out.println((char) receive);
}
} catch (Exception e) {
}
}
}
}
对于 Piped 类型的流,必须先进行绑定,也就是调用 connect() 方法。如果没有将输入输出流绑定,对于该流的访问将会抛异常。
Thread.join()
如果一个线程 A 执行了 Thread.join() 语句,其含义是:当前线程 A 等待 thread 线程终止之后才从 Thread.join() 返回。线程 Thread 除了提供 join() 方法之外,还提供了带超时的 join(long millis) 和 join(long millis, int nanos) 方法。
public class JoinLearn {
public static void main(String[] args) {
joinThread();
runInMain();
}
private static void joinThread(){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("i: " + i);
}
}
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void runInMain(){
for (int i = 0; i < 100; i++) {
System.out.println("runInMain: " + i);
}
}
}
上面的示例程序,输入结果如下所示:
i: 84
i: 85
i: 86
i: 87
i: 88
i: 89
i: 90
i: 91
i: 92
i: 93
i: 94
i: 95
i: 96
i: 97
i: 98
i: 99
runInMain: 0
runInMain: 1
runInMain: 2
runInMain: 3
runInMain: 4
runInMain: 5
runInMain: 6
runInMain: 7
runInMain: 8
runInMain: 9
runInMain: 10
runInMain: 11
runInMain: 12
runInMain: 13
runInMain: 14
Thread.join() 底层是如何实现的呢?
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//参数为0,调用Object.wait(0)等待
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);//参数非0,调用Object.wait(time)等待
now = System.currentTimeMillis() - base;
}
}
}
从上面的源码可以看到,逻辑实现比较简单,通过while循环查看线程状态 isAlive(),如果线程活着,调用 Object.wait() 方法等待;那么如何判断线程是否活着呢?wait() 方法是在什么时候被唤醒的呢?
isAlive 是一个 native 方法,具体实现在 在 javaClasses.cpp 文件中:
bool java_lang_Thread::is_alive(oop java_thread) {
JavaThread* thr = java_lang_Thread::thread(java_thread);
return (thr != NULL);
}
可以看到是通过底层的 JavaThread 来判断, 如果为 NULL,则表示线程已死。
第一个问题解决了,那么再看看第二个问题。在 run 方法执行结束之后,会调研 JavaThread::exit 方法清理资源,会将 native 线程对象设为 null,并且通过 notifyAll 方法,让等待在 Thread 对象锁上的wait方法返回。
//由于原方法较长,删除不相关部分
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
ensure_join(this);
assert(!this->has_pending_exception(), "ensure_join should have cleared");
// Remove from list of active threads list, and notify VM thread if we are the last non-daemon thread
Threads::remove(this);
}
static void ensure_join(JavaThread* thread) {
// 获取Threads_lock
Handle threadObj(thread, thread->threadObj());
assert(threadObj.not_null(), "java thread object must exist");
ObjectLocker lock(threadObj, thread);
// 忽略pending exception (ThreadDeath)
thread->clear_pending_exception();
//设置java.lang.Thread的threadStatus为 TERMINATED.
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
//清除native线程,这将会导致isAlive()方法返回false
java_lang_Thread::set_thread(threadObj(), NULL);
//通知所有等待thread锁的线程, join的wait方法将返回,由于isActive返回false,join方法将执行结束并返回
lock.notify_all(thread);
}
ThreadLocal
ThreadLocal 是线程本地变量,线程可以通过其 set 方法在 ThreadLocal 中设置线程私有的值,不和其他线程共享该值。ThreadLocal 最常用的是其 get、set、remove方法,具体实现可以看看其源码:
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
}
在 set 方法中,首先获取该线程对应的 ThreadLocal 集合,然后向 map 集合中添加键值对,key 为该 ThreadLocal 对象,value 为 set 方法的入参。get 方法操作也是类似的,首先获取线程对应的 ThreadLocal 集合,再查询 ThreadLocal 对象对应的 value。
注意:需要在线程执行完成之后,要通过 remove 方法来删除 ThreadLocal 里保存的值,否则会造成内存泄漏。