进程和线程
进程: 程序在数据集合上的一次运行活动,是操作系统进行资源分配和调度的基本单位。
线程: 进程的一个执行路径,一个进程中有多个线程,进程中的多个线程共享进程的资源,线程是 CPU 分配的基本单位。
在 Java 中,启动 main 函数就是启动了一个 jvm 进程, main 函数所在的线程就是 这个进程中的一个线程,称为主线程。
在 Java 虚拟机中,所有线程共享方法区和堆中的资源,每个线程有私有的程序计数器和栈区域。
上下文切换
多线程编程中,线程的个数一般大于 CPU 的个数,而 CPU 同一时刻只能执行一个线程。
CPU 资源的分配采用了了时间片轮转策略,也就是给每个线程分配个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU ,让其他线程占用这就是上下文切换

上下文切换的时机举例:
- 当前线程的 CPU 时间片使用完处于就绪状态时
- 当前线程被其他线程中断时
多线程一定比单线程快吗?
不一定,当任务量不多时,单线程比多线程更快,因为多线程上下文切换回带来额外的开销
线程的状态
java语言中定义了 6 种线程状态,可以通过不同的方法在6种不同的线程之间转换
- New (初始) : 线程创建后且未启动时
- Runnable (运行) :包括 Running (当前线程正在执行) 和 Ready (还没有执行,处于就绪状态,等着 cpu 调度为其分配时间片)
- Waiting (无限期等待):处于这种状态的线程不会 CPU 调度分配时间片,它们要等待被其他线程显式唤醒
- Timed Waiting (限期等待):处于这种状态的线程不会 CPU 调度分配时间片,一段时间后会自动唤醒,无需其他线程显示唤醒
- BLOCKED (阻塞状态):当获取对象资源监视器锁时,该对象锁已被其他线程占用,线程会进入阻塞状态
- Terminated : 线程终止状态

这张图很重要,上面的方法会在后面一一出现
java内存模型
工作内存与主内存
Java 内存模型规定了所有的变量(指实例变量、常量、静态变量)都在主内存中,每个线程还有自己的工作内存。
线程的工作内存中,保存了该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间的传值要通过主内存来完成。

这里的工作内存,是抽象的概念,实际上并不存在。从硬件层次可以认为,主内存位于主存储器上, 工作内存涵盖了 高速缓存寄存器,控制器,运算器和其他硬件等 (学过计组的都懂)

内存间交互操作
java内存模型定义了 8 种内存间交互操作,每一种操作都是原子性的。
- lock : 作用与主内存中的变量,把一个变量标示为一条线程独占的状态
- unlock : 作用与主内存中的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read : 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中
- load : 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
- use : 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- assign : 作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用
- write: 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
把一个变量从主内存拷贝到工作内存,需要执行: read 和 load 操作。
把变量从工作内存同步到主内存,需要执行 store 和 write 操作
需要知道的规则限定:
-
对一个变量进行 lock 操作,会清空工作内存中此变量的值,执行引擎使用这个变量时,需要重新从主内存中读取
-
对一个变量进行 unlock 操作之前,必须先把变量同步到主内存。
java中创建线程的三种方式
继承Thread
public class ThreadTest1 {
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println("my first thread");
}
// 线程的run方法执行完毕后,线程处于Terminated状态。
}
public static void main(String[] args) {
MyThread myThread = new MyThread(); //线程处于new状态
myThread.start(); // 调用start方法后,线程处于Ready状态,当CPU调度该线程时,线程处于Running状态
}
}
好处:
获取当前线程直接用 this
就可以,不需要使用 Thread.currentThread()
方法
坏处:
Java 只支持单继承,继承了这个类就不能继承别的类了
实现Runnable接口
public class ThreadTest2 {
public static class RunableTask implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 2020-5-24,学习多线程第一天");
}
}
public static void main(String[] args) {
RunableTask task = new RunableTask();
new Thread(task,"勤能补拙").start(); // 给thread起名字
new Thread(task,"笨鸟先飞").start();
}
}

好处: 解决了继承问题
实现Callable接口
直接继承 Thread 和实现 Runnable 有个共同的缺点,无法获取返回值
public class ThreadTest3 {
public static class MyTask implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("第三种创建线程方式");
Thread.sleep(2000); //线程休眠2000ms
return "77777"; // 返回一个String
}
}
public static void main(String[] args) {
// 创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new MyTask());
new Thread(futureTask).start(); //启动线程
try {
String res = futureTask.get(); //当线程执行完毕后才会得到结果,否则线程处于WAIT状态
System.out.println(res); // 打印返回值
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("执行结束");
}
}

线程的通知与等待
Object类 包含线程的通知与等待函数
wait函数
wait 函数在使用前必须获取对象的监视器锁,否则会抛出 IllegalMonitorStateException
如何获得对象的监视器锁呢? 使用 synchronized (后面再详解)关键字
synchronized 修饰代码块,用共享变量作为参数
// 当对象的监视器锁被线程持有时,其他尝试获取对象监视器锁的线程会被阻塞
synchronized(obj){
// 同一时间只有一个线程能获取对象的监视器锁,只有当前线程释放对象锁后,其他线程才能尝试获取锁
}
wait 函数能让当前线程进入 WAITNG 状态,并释放对象的监视器锁(其他尝试获取该锁的线程会有一个获得锁),直到发生以下事件才返回:
- 其他线程调用了该共享对象的 notify 或者 notifyAll 方法
- 其他线程调用了该线程的 interrupt 方法,该线程抛出 InterruptedException 异常后返回
notify¬ifyAll
notify 函数在使用前也必须获取对象的监视器锁,否则会抛出 IllegalMonitorStateException。
同一对象上调用 notify/notifyAll 方法,就可以唤醒对应对象 monitor(监视器锁) 上等待的线程了。notify和notifyAll 的区别在于前者只随机唤醒 monitor 上的一个线程,而 notifyAll 则唤醒所有的线程。
例子1
private static Object resource = new Object(); //定义一个共享对象
public static class NotifyTest implements Runnable{
@Override
public void run() {
synchronized (resource) { // 使用wait和notify前必须获取对象的监视器锁
String name = Thread.currentThread().getName(); // 获取当前线程名称
System.out.println(name + "进入wait状态,释放锁资源");
try {
resource.wait();
System.out.println(name + "被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "执行结束,释放锁资源");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new NotifyTest(),"t1");
Thread t2 = new Thread(new NotifyTest(),"t2");
Thread t3 = new Thread(new NotifyTest(),"t3");
t1.start();
t2.start();
t3.start();
Thread.sleep(1000); //等待前面的线程执行完
// 此时线程t1、t2、t3 都处于wait状态
synchronized (resource){
// resource.notify(); //随机随机唤醒一个线程
resource.notifyAll(); // 唤醒所有线程
}
}

这里 t2 比 t3 先start,但是 t3 比 t2 先进入 Running 状态: 先进入就绪状态不一定先执行,调度程序没有挑选到你,你就永远是就绪状态。这段程序每次运行的结果顺序可能都不一样
大家可以自己试一下 notify ,多运行几次看看结果。
例子2
当前线程调用共享变量的 wait 方法只会释放当前共享变量上的锁,如果当前线程还持有其他贡献变量的锁,则不会释放
private static Object resourceA = new Object();
private static Object resourceB = new Object(); // 定义两个共享变量
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
String name = Thread.currentThread().getName();
synchronized (resourceA){
System.out.println(name + " get resourceA lock");
synchronized (resourceB){
System.out.println(name + " get resourceB lock");
System.out.println(name + " release resourceA lock");
try {
resourceA.wait(); //释放resourceA的监视器锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t1").start(); // 线程名为t1
Thread.sleep(1000); // 保证t1先进入Running状态
new Thread(()->{
String name = Thread.currentThread().getName();
synchronized (resourceA){
System.out.println(name + " get resourceA lock");
System.out.println(name + " try get resourceB lock");
synchronized (resourceB){
System.out.println(name + " get resourceB lock");
System.out.println(name + " release resourceA lock");
try {
resourceA.wait(); //释放resourceA的监视器锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t2").start(); // 线程名为t2
}

t1 先获取 rescouceA 的对象锁,然后获取 recouceB 的对象锁(此时线程 t2 还未启动),然后线程 t1 释放对象 A 的锁资源,进入 wati 状态。 线程 t2 启动后获取 rescouceA 的对象锁,但不能获取 resourceB 的对象锁,因为线程 t1 并没有释放 resourceA 的对象锁。
join
在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行。比如多个线程加载资源,需要等待多个线程全部加载完毕再向下处理。
Thread类 有一个 join 方法可以完成这个功能
t.join,会让当前线程进入 wait 状态,直到 t线程 执行完毕再向下执行。
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 over");
});
Thread t2 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 over");
});
t1.start();
t1.join(); // 当前线程(主线程)进入wait,必须等线程t1执行完后才向下执行
t2.start();
t2.join(); // 当前线程(主线程)进入wait,必须等线程t2执行完后才向下执行
System.out.println("main over");
}
}
sleep
Thread 类有个静态方法 sleep ,当一个执行中的线程调用了 Thread 的 sleep 方 法后,调用线程会暂时让出指定时间内 CPU 的执行权,也就是在这期间不参与 CPU 的调度 , 但是锁资源并不释放。
public class SleepTest {
private static Object resource = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1= new Thread(()->{
synchronized (resource) {
try {
System.out.println("t1 get lock");
Thread.sleep(10000); // 进入wait状态,但不会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 over");
}
});
Thread t2= new Thread(()->{
synchronized (resource) {
try {
System.out.println("t2 get lock");
Thread.sleep(10000); // 进入wait状态,但不会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 over");
}
});
t1.start();
t2.start();
}
}
注释写的很清楚了,就不帖结果图了
yield
Thread类有个静态方法 yield ,能让当前正在执行的线程让出时间片,重新处于就绪状态,让 CPU 进行下一轮的 CPU 调度。不是很常用
public class YieldTest {
static class MyTask implements Runnable{
@Override
public void run() {
for (int i=0;i<3;i++){
String name = Thread.currentThread().getName();
if (i==0){
Thread.yield(); // 当i=0时,当前线程放弃时间片,cpu进行下一轮调度
}
System.out.println(name + " start " + i);
}
}
}
public static void main(String[] args) {
new Thread(new MyTask(),"t1").start();
new Thread(new MyTask(),"t2").start();
new Thread(new MyTask(),"t3").start();
}
}
调用 yield 方法时,只是让出当前线程剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时仍可能调度到当前线程执行 。
interrupt
interrupt 是中断的意思,但并不是真的中断线程,只是将线程的中断标志位设为true,线程仍然会继续运行
Thread 类有几个与 interrupt 相关的函数
// 中断线程
public void interrupt() {
...
}
// 判断当前线程是否被中断,不清除中断标志位
public boolean isInterrupted() {
return isInterrupted(false);
}
// 静态方法,判断当前线程是否被中断,并清除中断标志位
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
// 判断当前线程是否被中断,根据ClearInterrupted判断是否清除中断标志位,true代表清除
private native boolean isInterrupted(boolean ClearInterrupted);
清除中断标志位就是将中断标志位设为 false 。下面来看个例子
public class InterrputTest {
static class MyThread extends Thread {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) { //换成这个试试 Thread.interrupted()
System.out.println("线程被中断");
} else {
System.out.println("线程在运行");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
Thread.sleep(1000);
myThread.interrupt();
}
}
先输出 1s 的 '线程在运行',然后一直输出'线程被中断',大家把if中的判断条件换成 Thread.interrupted()
试试看结果有什么不一样。
ps : 线程在 sleep()
中或者 wait()
中如果被中断,会抛出 InterruptedException
异常。
守护线程和用户线程
Java 中线程分为两类, daemon 线程(守护线程) 和 user 线程
main 函数所在的线程就是一个用户线程,其实在 JVM 内部同时还启动了许多守护线程,比如垃圾回收线程。
区别是当最后一个用户线程结束时, JVM 就会正常退出,不管守护线程是否结束。
public class DaemonTest {
public static void main(String[] args) {
Thread t = new Thread(()->{
for (;;){} // 无限循环
});
t.setDaemon(true); // 设置线程为守护线程,注释掉该行看看有啥区别
t.start();
}
}
虽然有一个无限循环线程,但是他是守护线程,不影响 JVM 的退出,

原创不易,求点赞