线程
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线 程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程, 或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是 一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个 指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输 入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划 分成的更小的运行单位。
线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为 同一进程中的线程极有可能会相互影响。从另一⻆度来说,进程属于操作系统的范畴,主要是同一段时 间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段
线程状态

线程状态转换过程

线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运 行) 状态。
操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图 源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两 个状态统称为 RUNNABLE(运行中) 状态
sleep vs wait
| wait | sleep | |
|---|---|---|
| 同步 | 只能在同步上下文中调用wait方法,否则或抛出IllegalMonitorStateException异常 | 不需要在同步方法或同步块中调用 |
| 作用对象 | wait方法定义在Object类中,作用于对象本身 | sleep方法定义在java.lang.Thread中,作用于当前线程 |
| 释放锁资源 | 是 | 否 |
| 唤醒条件 | 其他线程调用对象的notify()或者notifyAll()方法 | 超时或者调用interrupt()方法体 public static void sleep (long millis, int nanos) throws InterruptedException { |
| 方法属性 | wait是实例方法 | sleep是静态方法 |
Thread::yield()方法可以主动让出执行时间,但是如果想要主动获取执行时间,线程本身是没有什么办法的。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程甚至整个系统阻塞的问题。Java使用的线程调度方式就是抢占式调度。
Thread.join
Waits for this thread to die.
当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。
MainThread {
b.start()
b.join()
}
主线程会被挂起,直到 b结束执行后,主线程才会再运行,代码里面就判断 isAlive()
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) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
调用链:Parent.main() -> child.join() -> child.join(0) -> child.wait(0)(此时 Parent线程会获得 child 实例作为锁,其他线程可以进入 child.join() ,但不可以进入 child.join(0), 因为child.join(0)是同步方法)。
如果 child 线程是 Active,则调用 child.wait(0)(为了防止子线程 spurious wakeup, 需要将 wait(0) 放入 while(isAlive()) 循环中。
一旦 child 线程不为 Active (状态为 TERMINATED), child.notifyAll() 会被调用-> child.wait(0)返回 -> child.join(0)返回 -> child.join()返回 -> Parent.main()继续执行, 子线程会调用this.notify(),child.wait(0)会返回到child.join(0) ,child.join(0)会返回到 child.join(), child.join() 会返回到 Parent 父线程,Parent 父线程就可以继续运行下去了。
Thread.Interrupt
一、没有任何语言方面的需求一个被中断的线程应该终止。中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断。
二、对于处于sleep,join等操作的线程,如果被调用interrupt()后,会抛出InterruptedException,然后线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态。
三、不可中断的操作,包括进入synchronized段以及Lock.lock(),inputSteam.read()等,调用interrupt()对于这几个问题无效,因为它们都不抛出中断异常。如果拿不到资源,它们会无限期阻塞下去。
对于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中断的加锁操作,它可以抛出中断异常。等同于等待时间无限长的Lock.tryLock(long time, TimeUnit unit)。
对于inputStream等资源,有些(实现了interruptibleChannel接口)可以通过close()方法将资源关闭,对应的阻塞也会被放开
多线程内存区域

程序计数器为什么是私有的
程序计数器主要有下面两个作用:
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、 选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能 够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的 是 Java 代码时程序计数器记录的才是下一条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈为什么是私有的
虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常 量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈 和出栈的过程。 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚 拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
使用多线程可能带来什么问题
并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序 运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件 和软件的资源闲置问题
上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使 用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一 个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切 换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次 再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换 中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事 实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换 和模式切换的时间消耗非常少
线程死锁
线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线 程就会互相等待而进入死锁状态
破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资 源需要互斥访问)。
破坏请求与保持条件 :一次性申请所有的资源。
破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放
它占有的资源。
破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破
坏循环等待条件。
线程池
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int workerCountOf(int c) {
return c & CAPACITY;
}
private final BlockingQueue<Runnable> workQueue;
public void execute(Runnable command) { // 如果任务为null,则抛出异常。
if (command WX null)
throw new NullPointerException(); // ctl 中保存的线程池当前的一些状态信息 int c = ctl.get();
// 下面会涉及到3步操作
// 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
// 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线
程中;然后,启动该线程从而执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
// 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任 务,该任务才会被加入进去
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,
并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
if (!isRunning(recheck) && remove(command))
}
reject(command);
// 如果当前线程池为空就新创建一个线程并执行。 else if (workerCountOf(recheck) WX 0)
addWorker(null, false);
//3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然 后,启动该线程从而执行任务。
//如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 else if (!addWorker(command, false))
}
reject(command);

文档信息
- 本文作者:Jessica
- 本文链接:https://jessica0530.github.io/2020/08/26/Java-%E7%BA%BF%E7%A8%8B/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)