第四章 JAVA并发编程基础
4.1 线程简介
线程(轻量级进程): 现代操作系统调度的最小单位.
线程共享的存储: 堆
线程独占的存储: 栈(局部变量,方法参数),PC,堆的ThreadLocal
区
JAVA程序天生多线程: 执行main
方法的是一个名字为main
的线程.
天生的线程:
Signal Dispatcher
: 分发处理发送给JVM信号的线程;Finalizer
: 调用对象finallize
方法的线程;Reference Handler
: 清除Reference
的线程;main
:main
线程,用户程序入口.
要查看上述线程,可以用JMX打印出来:
1 | ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); |
如果懒得写上述代码,也可以使用IDE的debug功能,例如在Intellij idea
中打个断点,就直接可以在debug
标签页看到1-4号线程的栈帧了.(5号看不见,原因未知.TODO)
4.1.3 线程优先级(很可能被操作系统忽略)
线程优先级: 整型变量priority
优先级范围: 1~10
默认优先级: 5
设定策略:
- cpu密集线程=>设定较低优先级;(需要cpu时间长,防止它独占太久)
- io密集线程=>设定较高优先级(相当于cpu占用时间不长的线程,反而可以优先给它,一种短作业优先的逻辑).
// 短作业优先可能导致长作业饿死,因此上述策略下,如果IO密集线程特别多,就不好了.
4.1.4 线程的状态
Runnable
Java把操作系统中的就绪
和运行中
统称为Runnable
.
线程状态转化大致如下:
New
=>Runnable
=>Terminated
// 理想状态Runnable
=>Blocked
/Waiting
/Time_Waited
=>Runnable
// 可能误入的歧途
状态 | 说明 |
---|---|
New | 创建线程后,调用start() 前. |
Runnable | 就绪和运行中. |
Terminated | 终止. 执行完毕 |
Blocked | 阻塞. 阻塞于锁. (synchronized ) |
Waiting | 等待. 等待其他线程的中断或者通知.(Lock 类) |
Time_Waiting | 超时等待. 比Waiting多一个超时返回功能.(Lock 类) |
查看某个java程序目前各线程状态:
- 先用jps查看该进程的进程id;
- 运行命令jstack
即可.
或者用kill -3 <id>
命令让进程把threadDump
信息输出到标准输出.(可以之前让进程把标准输出重定向到日志文件中)
或者用IDE的threadDump
按钮也可以.
Runnable与其他状态的转化
1.WaitingRunnable
=>Waiting
: // 主动等待某个对象
1 | obj.wait() |
Waiting
=>Runnable
: // 被别人中断或通知
1 | obj.notify() // 必须在wait之后调用才有效 |
2.Time_WaitingRunnable
=>Time_Waiting
: // 基本就是比Waiting多个时长
1 | obj.wait(long) |
Time_Waiting
=>Runnable
: // 与Waiting完全一样
1 | obj.notify() |
3.BlockedRunnable
=>Blocked
:
1 | synchronized(xx)// 没获取到锁 |
Runnable
=>Blocked
:
1 | synchronized(xx)// 获取到了锁 |
4.1.4 Daemon线程
守护线程,用作后台调度以及支持性工作.
换句话说,是为普通线程服务的,如果普通线程不存在了(运行结束了),Daemon
线程也就没有存在的意义了,因此会被立即终止.
立即终止发生得非常突然,以至于Daemon线程的
finally
方法都可能来不及执行.
设定线程为Daemon
的方法:
1 | thread.setDaemon(true); |
4.2 启动和终止线程
4.2.1 构造线程
线程的构造内容包括:
- 父线程; (创建它的线程) // 下面的属性默认值均与父线程一致:
- 线程组;
- 是否守护线程;
- 名字;
- ThreadLocal内容. (复制一份父线程的可继承部分)
构造完成后,在堆内存中等待运行.
4.2.2 启动线程
- start方法的含义:
当前线程(父线程)同步通知JVM虚拟机,在线程规划期空闲时,启动线程.
4.2.3 中断
每个线程的中断标识位:
true
: 被中断. (收到了中断信号)false
(初始值): 没中断,或已经运行结束.
容易混淆的几个方法:
1 | obj.interrupt();// 中断某线程.把它的中断标志改为`true`. |
除了Thread.interrupted()
,还有一些方法抛出interruptedException
前也会清除中断标志(置为false
),以表示自己已经处理了这个中断.(如sleep
方法.)
4.2.4 废弃方法: suspend(),resume(),stop()
- suspend(): 挂起(暂停), 不释放锁.
- resume(): 恢复(继续)
- stop(): 停止,太突然,可能没释放资源.
4.2.5 安全的终止/暂停的方法
使用中断.
例如:
1 | public class TestCancel2 { |
这部分在<并发编程实战>第七章有详细讨论.
根据具体情况的不同,有多种解决方案.
- 简单情况: 直接轮询标志位;
- while中操作可能阻塞: (1)检查while中每一行;(2)while中只提交任务,起另外的线程执行任务;
- 能中断但不能取消的任务: 保持中断状态,直到收到继续信号;
- 其他….
详见:
https://github.com/xiaoyue26/scala-gradle-demo/tree/master/src/main/java/practice/chapter7
4.3 线程间通信
4.3.1 使用volatile和synchronized
首先,本质上是使用共享内存进行通信,同步则是使用volatile
附带的内存屏障和sychronized
带来的内置锁。(排他锁)
下面分别介绍volatile
和synchronized
:
volatile
volatile
主要作用就是让写入能够尽快从cpu缓存刷新到内存;
而读则尽量读内存。(最新数据)
应用场景:
一个线程写,其他线程只读的场景。
出错场景:(这种场景应改用AtomicInteger
等原子类)
多个线程写:
- 线程A进行自增操作,从1增加到2;
- 线程B进行自增操作,从1增加到2;
- A,B分别先读后写,最后都写入2,因此出错。(还有其他次序及结果)
底层内存屏障:
- volatile写:
1
2
3StoreStore屏障
volatile写
StoreLoad屏障 - volatile读:
1
2
3volatile读
LoadLoad屏障
LoadStore屏障
用volatile
模拟锁,辅助线程同步(通信):
1 | volatile boolean flag=false; |
还有其他库里的同步类,原子类也是在volatile
的基础上,加上CAS操作实现的。
Synchronized
synchronized
用于线程同步时,使用的是对象的内置锁。
- Java代码层面:
synchronized
- class字节码层面:
monitorenter
,monitorexit
,ACC_SYNCHRONIZED
指令 - 执行层面:多个线程竞争某个对象的内置锁,这个内置锁是排他的,一次只有一个线程能够成功获得内置锁。
线程获取内置锁有成功失败两种情况:
(1) Thread==Monitor enter
=>失败=>进入同步队列(Blocked
状态);
(2)Thread==Monitor enter
=>成功=>结束后释放锁,唤醒同步队列的线程.
相关字节码实验:
源代码:
1
2
3
4
5
6
7
8
9
10
11public class SynchronizedTest{
public static void main(String[]args){
synchronized(SynchronizedTest.class){
// do something
}
}
public static synchronized void m(){
// do something
}
}反编译class文件
1
javap -v <xxx.class>
结果大致如下:
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
36// 省略几行
Constant pool:
// 省略此处的#1~#27常量.(包括符号引用)
{
// 省略一些
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC // 访问修饰符
Code:
stack=2, locals=3, args_size=1
0: ldc #2// class practice/art/chapter4/SynchronizedTest
2: dup
3: astore_1
4: monitorenter // 获取锁
5: aload_1
6: monitorexit // 释放锁
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: invokestatic #3 // Method m:()V
18: return
// 省略很多
public static synchronized void m();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
//注意这里的ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 16: 0
}
SourceFile: "SynchronizedTest.java"
4.3.2 等待/通知机制
相关Java方法:
方法 | 描述 |
---|---|
obj.wait() | 在某个对象上等待 |
obj.notify() | 通知1个在该对象上等待的线程,使其从wait()方法返回。 |
obj.notifyAll() | 通知所有在该对象上等待的线程。 |
示例代码:
1 | static Object obj=new Object(); |
4.3.4 管道输入/输出流
依然是使用共享内存进行通信的一种方法。具体实现有2种:
- 面向字节:
PipedOutputStream
/PipedInputStream
; - 面向字符:
PipedReader
/PipedWriter
。
示例代码:
1 | PipedWriter out=new PipedWriter(); |
threadA.join()用于线程同步
假如threadB中调用threadA.join()
,意思就是等待threadA
线程对象退出。
本质上join
是一个sychronized
方法,调用了线程对象的wait
方法:
1 | public final synchronized void join(long millis) |
因此join
方法的特性大致与wait
方法相同:线程状态变成waiting
,能接受中断(IDE会提示受检异常),接受notify
,等待时会释放锁。
而threadA
线程对象退出的时候,会调用notifyAll
方法,通知所有等待它退出的线程。
4.4 线程应用实例
这节主要写了一个简单的线程池构造、使用示例,Web服务器示例。
实现中:
- 线程通信使用了原子变量
AtomicInteger
进行数据记录,记录多个线程成功及失败的线程数; - 连接池的线程安全委托给了
LinkedList
,但是用Collections.synchronizedList
包装了一下;另一个地方的实现则是用synchronized
对所有相关容器的访问进行保护; - 实验相关的代码,为了加大线程的冲突,用
countDownLatch
同步了线程的启动和结束; - 使用了wait/notify机制,尽量使用了
notify
而不是notifyAll
,避免唤醒太多;(感觉可以考虑使用unpark
)