1、进程与线程
- 进程是资源分配的最小单位,线程是任务执行的最小单位
- 运行一个 Java 服务,就是开启了一个 JVM 进程,在 JVM 进程中可以创建多个线程,由线程来执行具体的代码
2、构造方法
- Thread 有多种构造方法,本质都是执行 init 方法,方法会做一些判断和初始化一些变量
private void init(
// 线程组
ThreadGroup g,
// 执行的任务
Runnable target,
// 线程名称,默认"Thread-自增数"
String name,
// 当前线程的栈大小,默认0,有些 JVM 会忽略掉这个参数
long stackSize,
AccessControlContext acc,
// 是否从创建线程的线程中继承 ThreadLocal
boolean inheritThreadLocals) {
// 检查线程名称
if (name == null) {
throw new NullPointerException("name cannot be null");
}
// 设置线程名称
this.name = name;
// 获取创建线程的线程-父线程
Thread parent = currentThread();
// 获取安全管理器
SecurityManager security = System.getSecurityManager();
// 设置线程组,如果未指定线程组,会先从安全管理器中获取,如果安全管理器中获取不到,会从父线程中获取
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
// 记录线程组未启动线程数
g.addUnstarted();
// 设置线程组
this.group = g;
// 设置是否未守护线程,和父线程一样
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);
// 继承父线程的 ThreadLocal
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
// 设置栈大小
this.stackSize = stackSize;
/* Set thread ID */
// 设置id---自增
tid = nextThreadID();
}
3、常见方法
方法名 | 功能说明 | 注意 |
---|---|---|
start() | 启动一个新线程,在新的线程运行 run 方法中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException |
run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 |
join() | 等待线程运行结束 | |
join(long n) | 等待线程运行结束,最多等待 n 毫秒 | |
getId() | 获取线程长整型的 id | id 唯一 |
getName() | 获取线程名 | |
setName(String) | 修改线程名 | |
getPriority() | 获取线程优先级 | |
setPriority(int) | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 1、线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它 2、如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用 |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED |
isInterrupted() | 判断是否被打断 | 不会清除打断标记 |
isAlive() | 线程是否存活(还没有运行完毕) | |
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记 |
interrupted() | 判断当前线程是否被打断 | 会清除打断标记 |
currentThread() | 获取当前正在执行的线程 | |
sleep(long n) | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | 1、调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞) 2、其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException 3、睡眠结束后的线程未必会立刻得到执行(CPU 的时间片还没分给它) |
yield() | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 1、调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程 2、具体的实现依赖于操作系统的任务调度器 |
stop() | 停止线程运行 | 过时,不推荐 |
suspend() | 挂起(暂停)线程运行 | 过时,不推荐 |
resume() | 恢复线程运行 | 过时,不推荐 |
(1)start
- 启动线程,执行 run() 方法
- 一个线程只能被启动一次
- 该方法只是让线程进入可执行的状态,真正执行 run() 方法需要等待 cpu 分配时间片
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
// 检查线程状态
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
// 将当前线程加入线程组
group.add(this);
boolean started = false;
try {
// 启动线程
start0();
started = true;
} finally {
try {
if (!started) {
// 通知线程组启动失败
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
(2)join
- 等待线程运行结束,可以一直等待或带超时时间的等待,通过 wait / notify 实现
- 如果线程是存活状态(并且没有超过超时时间),会调用 wait 方法,等待被 notify,循环执行
- 如果其他线程打断了该线程,会抛出 InterruptedException
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");
}
if (millis == 0) {
// 如果线程还是存活状态,一直调用 wait 方法
while (isAlive()) {
wait(0);
}
} else {
// 如果线程还是存活状态,并且需要等待的时间未小于0,一直调用 wait 方法
while (isAlive()) {
// 计算还需要等待的时间
long delay = millis - now;
if (delay <= 0) {
// 等待时间小于0直接退出
break;
}
wait(delay);
// 更新已经等待时间
now = System.currentTimeMillis() - base;
}
}
}
(3)interrupt
- 打断线程,如果线程在执行可中断 IO 操作时,先设置打断标记再通知阻塞者中断,否则直接设置打断标记
- 若线程阻塞在 Object 类的 wait 方法、Thread 类自己的 join 和 sleep 方法调用上,那么中断状态会被清除,线程会收到 InterruptedException
- 若线程阻塞在 InterruptibleChannel 的 IO 操作上,那么通道会被关闭,中断状态会被置位,线程会收到 ClosedByInterruptException
- 若线程阻塞在 Selector 上,那么中断状态会被置位,select操作会立即返回,返回值可能是一个非零值
- 其他情况下中断状态都会被置位
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
// 获取阻塞者锁
synchronized (blockerLock) {
// 线程在执行可中断IO操作时阻塞该线程的对象
Interruptible b = blocker;
if (b != null) {
// 如果阻塞者不为空设置打断标记
interrupt0(); // Just to set the interrupt flag
// 调用阻塞者打断方法,中断IO
b.interrupt(this);
return;
}
}
// 设置打断标记
interrupt0();
}
4、线程状态
(1)操作系统层面
- 初始状态
- 仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 可运行状态
- (就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 运行状态
- 指获取了 CPU 时间片运行中的状态
- 当 CPU 时间片用完,会从运行状态转换至可运行状态,会导致线程的上下文切换
- 阻塞状态
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入阻塞状态
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态
- 与可运行状态的区别是,对阻塞状态的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 终止状态
- 表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
(2)Java层面
- NEW(尚未启动)
- 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE(可运行状态)
- 调用了 start() 方法之后
- Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的可运行状态、运行状态和阻塞状态(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED (阻塞状态)
- 等待 monitor 锁进入同步代码块/方法
- WAITING(等待状态)
- 处于等待状态的线程正在等待另一个线程执行特定操作
- Object 的 wait() 方法,没设置超时时间
- Thread 的 join() 方法,没设置超时时间
- LockSupport 的 park() 方法
- TIMED_WAITING (具有指定等待时间的等待状态)
- Thread 的 sleep() 方法
- Object 的 wait() 方法,设置了超时时间
- Thread 的 join() 方法,设置了超时时间
- LockSupport 的 parkNanos() 方法
- LockSupport 的 parkUntil() 方法
- TERMINATED (终止状态)
- 线程代码运行结束
(3)线程状态转换
5、用户线程与守护线程
-
默认情况下,JVM 需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要用户线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
-
平时创建的线程就是用户线程,可调用 setDaemon() 方法,参数传 true 设置为守护线程
6、线程运行
- 每个线程启动后,虚拟机就会为其分配一块栈内存和程序计数器。
- 栈
- 每个栈由多个栈帧(Frame)组成
- 栈针对应着每次方法的调用
- 栈针包括局部变量表,返回地址,锁记录,操作数栈
- 每个栈由多个栈帧(Frame)组成
- 程序计数器
- 选取下一条需要执行的字节码指令
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 栈
7、线程上下文切换
因为一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器,它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- 上下文切换频繁发生会影响性能
8、活跃性
(1)死锁
-
死锁是指多个进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象(互相挂起等待),若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
-
死锁产生的必要条件
- 互斥条件
- 该资源任意一个时刻只由一个线程占用
- 请求与保持条件
- 一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件
- 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
- 循环等待条件
- 若干进程之间形成一种头尾相接的循环等待资源关系
- 互斥条件
-
死锁定位
-
通过 jps 命令找到当前 JVM 的进程 id
-
admin@admindeMacBook-Pro fighting % jps 4880 RemoteMavenServer36 627 4887 RemoteMavenServer36 6776 Jps 4888 RemoteMavenServer36 6493 Launcher 6494 DeadlockDemo
-
-
通过 jstack <进程 id>,查看堆栈信息,分析死锁
-
Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00007fe951030aa8 (object 0x00000007957784c0, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00007fe95102e218 (object 0x00000007957784d0, a java.lang.Object), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at com.jelly.fighting.jdk.concurrent.DeadlockDemo.lambda$main$1(DeadlockDemo.java:28) - waiting to lock <0x00000007957784c0> (a java.lang.Object) - locked <0x00000007957784d0> (a java.lang.Object) at com.jelly.fighting.jdk.concurrent.DeadlockDemo$$Lambda$2/990368553.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) "Thread-0": at com.jelly.fighting.jdk.concurrent.DeadlockDemo.lambda$main$0(DeadlockDemo.java:18) - waiting to lock <0x00000007957784d0> (a java.lang.Object) - locked <0x00000007957784c0> (a java.lang.Object) at com.jelly.fighting.jdk.concurrent.DeadlockDemo$$Lambda$1/2003749087.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Found 1 deadlock.
-
-
-
避免死锁
- 固定顺序加锁
- 线程 1 和线程 2 都按照 lock1、lock2 的顺序加锁
- 增加超时时间
- 当过了超时时间之后自动释放锁资源,但是可能会出现逻辑未执行完,锁已经释放,发生并发冲突问题
- 释放锁资源
- 线程 1 尝试获取 lock1 成功后,再尝试获取 lock2 ,如果获取 lock2 失败,就释放 lock1,但是可能会导致活锁
- 固定顺序加锁
(2)活锁
- 多个线程互相改变对方的结束条件,虽然没有阻塞,但是也无法结束
- 避免活锁
- 交错执行,增加随机睡眠时间
(3)饥饿
- 因为某种原因,线程始终得不到 CPU 调度,无法执行
- 解决饥饿
- 合理安排任务线程的优先级
- 使用公平锁