前言
本文快速回顾了常考的的知识点,用作面试复习,事半功倍。
知乎阅读体验不好,请移步Csdn:https://blog.csdn.net/qqxx6661/article/details/86726537
面试知识点复习手册
已发布知识点复习手册
- Java基础知识点面试手册
- 快速梳理23种常用的设计模式
- Redis基础知识点面试手册
- Java容器(List、Set、Map)知识点快速复习手册
参考
本文内容参考自CyC2018的Github仓库:CS-Notes
https://github.com/CyC2018/CS-Notes/
有删减,修改,补充额外增加内容
本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。
线程状态转换
新建(New)
创建后尚未启动。
可运行(Runnable)
可能正在运行,也可能正在等待 CPU 时间片。
包含了操作系统线程状态中的 Running 和 Ready。
阻塞(Blocking)
等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
无限期等待(Waiting)
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
| 进入方法 | 退出方法 | | --- | --- | | 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() | | 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 | | LockSupport.park() 方法 | - |
限期等待(Timed Waiting)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁;
而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
| 进入方法 | 退出方法 | | --- | --- | | Thread.sleep() 方法 | 时间结束 | | 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() | | 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 | | LockSupport.parkNanos() 方法 | - | | LockSupport.parkUntil() 方法 | - |
死亡(Terminated)
可以是线程结束任务之后自己结束,或者产生了异常而结束。
使用线程
有三种使用线程的方法:
- 实现 Runnable 接口
- 实现 Callable 接口
- 继承 Thread 类
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
实现 Runnable 接口
需要实现 run() 方法。
通过 Thread 调用 start() 方法来启动线程。
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
实现 Callable 接口
Callable就是Runnable的扩展。
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
继承 Thread 类
同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
其他方法
严格说不能算方法,只能算实现方式:
- 匿名内部类
- 线程池
实现接口 VS 继承 Thread
实现接口会更好一些,因为:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
- 代码可以被多线程共享,数据独立,很容易实现资源共享
start和run有什么区别?
详细解释:https://blog.csdn.net/lai_li/article/details/53070141?locationNum=13&fps=1
start方法:
- 通过该方法启动线程的同时也创建了一个线程,真正实现了多线程。无需等待run()方法中的代码执行完毕,就可以接着执行下面的代码。
- 此时start()的这个线程处于就绪状态,当得到CPU的时间片后就会执行其中的run()方法。这个run()方法包含了要执行的这个线程的内容,run()方法运行结束,此线程也就终止了。
run方法: - 通过run方法启动线程其实就是调用一个类中的方法,当作普通的方法的方式调用。并没有创建一个线程,程序中依旧只有一个主线程,必须等到run()方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有达到写线程的目的。
线程代码示例
package cn.thread.test;
/*
* 设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。
*/
public class ThreadTest1 {
private int j;
public static void main(String[] args) {
ThreadTest1 tt = new ThreadTest1();
Inc inc = tt.new Inc();
Dec dec = tt.new Dec();
Thread t1 = new Thread(inc);
Thread t2 = new Thread(dec);
Thread t3 = new Thread(inc);
Thread t4 = new Thread(dec);
t1.start();
t2.start();
t3.start();
t4.start();
}
private synchronized void inc() {
j++;
System.out.println(Thread.currentThread().getName()+"inc:"+j);
}
private synchronized void dec() {
j--;
System.out.println(Thread.currentThread().getName()+"dec:"+j);
}
class Inc implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
inc();
}
}
}
class Dec extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
dec();
}
}
}
}
基础线程机制
Executor线程池
https://segmentfault.com/a/1190000014741369#articleHeader3
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。异步是指多个任务的执行互不干扰,不需要进行同步操作。
- 当前线程池大小 :表示线程池中实际工作者线程的数量;
- 最大线程池大小 (maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限;
- 核心线程大小 (corePoolSize ):表示一个不大于最大线程池大小的工作者线程数量上限。
如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队;
如果运行的线程等于或者多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不是添加新线程;
如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出 maxinumPoolSize, 在这种情况下,任务将被拒绝。
不用线程池的弊端
- 线程生命周期的开销非常高