线程基础知识(heima)
首先参考 b 站视频来入门,然后之后看 javaguide 文档来深入学习。
可以参考的资料
对于这里的飞书资料,也没必要所谓的去完全照抄下去,记住:是将精华部分给出!
线程和进程的区别?
二者对比
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
并行和并发有什么区别
现在都是多核 CPU,在多核 CPU 下
- 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个 CPU
- 并行是同一时间动手做多件事情的能力,4 核 CPU 同时执行 4 个线程
创建线程的方式(*)
共有四种方式可以创建线程,分别是:
- 继承
Thread
类 - 实现
runnable
接口 - 实现
Callable
接口 - 线程池创建线程
继承 Thread
类
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("Thread running...");
}
public static void main(String[] args) {
MyThread a1 = new MyThread();
MyThread a2 = new MyThread();
a1.start();
a2.start();
}
}
实现 runnable
接口
public class A implements Runnable{
@Override
public void run() {
System.out.println("Runnable running...");
}
public static void main(String[] args) {
A a = new A();
Thread t1 = new Thread(a);
Thread t2 = new Thread(a);
t1.start();
t2.start();
}
}
实现 Callable
接口
public class A implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName());
return "OK";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
A a = new A();
FutureTask<String> task = new FutureTask<>(a);
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
String res = task.get();
System.out.println(res);
// t2.start();
}
}
线程池创建线程
public class A implements Runnable {
@Override
public void run() {
System.out.println("Runnable running");
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.submit(new A());
executorService.shutdown();
}
}
在 java 中一共有四种常见的创建方式,分别是:继承 Thread 类、实现 runnable 接口、实现 Callable 接口、线程池创建线程。通常情况下,我们项目中都会采用线程池的方式创建线程。
runnable 和 callable 有什么区别
- Runnable 接口 run 方法没有返回值;Callable 接口 call 方法有返回值,是个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果
- Callalbe 接口支持返回执行结果,需要调用 FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
- Callable 接口的 call()方法允许抛出异常;而 Runnable 接口的 run()方法的异常只能在内部消化,不能继续上抛
线程的 run()和 start()有什么区别?
start()
: 用来启动线程,通过该线程调用 run 方法执行 run 方法中所定义的逻辑代码。start 方法只能被调用一次。run()
: 封装了要被线程执行的代码,可以被调用多次。
线程包括哪些状态,状态之间是如何变化的*
包括六种状态 :
- 新建(NEW)
- 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
- 此时未与操作系统底层线程关联
- 可运行(RUNNABLE)
- 调用了 start 方法,就会由新建进入可运行
- 此时与底层线程关联,由操作系统调度执行
- 终结(Terminated)
- 线程内代码已经执行完毕,由可运行进入终结
- 此时会取消与底层线程关联
- 阻塞
- 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
- 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
- 等待
- 当获取锁成功后,但由于条件不满足,调用了
wait()
方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间 - 当其它持锁线程调用
notify()
或notifyAll()
方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
- 当获取锁成功后,但由于条件不满足,调用了
- 有时限等待
- 当获取锁成功后,但由于条件不满足,调用了
wait(long)
方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间 - 当其它持锁线程调用
notify()
或notifyAll()
方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁 - 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
- 还有一种情况是调用
sleep(long)
方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态
- 当获取锁成功后,但由于条件不满足,调用了
在 JDK 中的 Thread 类中的枚举 State 里面定义了 6 中线程的状态分别是:新建、可运行、终结、阻塞、等待和有时限等待六种。
关于线程的状态切换情况比较多。我分别介绍一下
当一个线程对象被创建,但还未调用 start 方法时处于新建状态,调用了 start 方法,就会由新建进入可运行状态。如果线程内代码已经执行完毕,由可运行进入终结状态。当然这些是一个线程正常执行情况。
如果线程获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,只有当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
如果线程获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁等待状态,当其它持锁线程调用 notify() 或 notifyAll() 方法,会恢复为可运行状态
还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,不需要主动唤醒,超时时间到自然恢复为可运行状态
如何保证 T1,T2,T3 三个线程,如何保证顺序执行?
使用 join()
方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
T3 调用 T2,T2 调用 T1,这样就能确保 T1 就会先完成而 T3最后完成
总结:不够底层,所以不再总结。(弃)