目录
1、线程的概念
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
2、线程的分类
Java中的线程分为用户线程和守护线程两种类型:
用户线程
Java中的用户线程(User Thread)是指由Java应用程序自行创建、管理的线程,也称为非守护线程(non-daemon thread)。
Java中的用户线程由程序员手动创建和管理,可以控制线程的生命周期、状态等。通常情况下,Java应用程序中的线程都是用户线程,因为它们是由程序员创建和启动的。在Java中,通过Thread类或者实现Runnable接口来创建用户线程。
// 创建和启动线程的方式一
MyThread thread = new MyThread();
thread.start();
// 创建和启动线程的方式二
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
守护线程
Java中的守护线程是一种特殊类型的线程,它被设计成只有在其他非守护线程存在时才会运行。当Java虚拟机(JVM)检测到只有守护线程时,JVM将退出。守护线程通常用于执行一些后台任务,例如垃圾回收或I/O操作,它们在整个应用程序的生命周期内运行,并在不需要时自动终止。
//创建守护线程
Thread myThread = new Thread(new MyRunnable());
myThread.setDaemon(true);
myThread.start();
在使用守护线程时需要注意一下几点 :
(1) thread.setDaemon(true) 必须在 thread.start() 之前设置,否则会跑出一个
IllegalThreadStateException 异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在 Daemon 线程中产生的新线程也是 Daemon 的。
(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一
个操作的中间发生中断。
3、线程的生命周期
线程一共有五个状态,分别如下:
新建(new):当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。例如: Thread t1 = new Thread() 。
可运行(runnable):线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start 方法。该状态的线程 位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权。例如: t1.start() 。 有些文章,会称可运行(runnable)为就绪,意思是一样的。
运行(running):线程获得 CPU 资源正在执行任务( #run() 方法),此时除非此线程自动放弃 CPU 资源或 者有优先级更高的线程进入,线程将一直运行到结束。
死亡(dead):当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待 执行。
自然终止:正常运行完 #run() 方法,终止。
异常终止:调用 #stop() 方法,让一个线程终止运行。
堵塞(blocked):由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行,即进入堵塞状态。直到线程 进入可运行(runnable)状态,才有机会再次获得 CPU 资源,转到运行(running)状态。
阻塞的情况有三种:
正在睡眠:调用 #sleep(long t) 方法,可使线程进入睡眠方式。 一个睡眠着的线程在指定的时间过去可进入可运行(runnable)状态。
正在等待:调用 #wait() 方法。 调用 notify() 方法,回到就绪状态。
被另一个线程所阻塞:调用 #suspend() 方法。 调用 #resume() 方法,就可以恢复。
1> notify 方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程 等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管 理的实现。
2> notifyAll 会唤醒所有等待 ( 对象的 ) 线程,尽管哪一个线程将会第一个处理取决于操作系 统的实现。
3>yield() 方法是停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。 如果没有的话,那么 yield() 方法将不会起作用,并且由可执行状态后马上又被执行。
4>join 方法是用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执 行结束后,再继续执行当前线程。如:t.join(); // 主要用于等待 t 线程运行结束,若无此句, main 则会执行完毕,导致结果不可预测。
4、线程的创建方式
方式一:继承 Thread 类创建线程类。
public class MYThread extends Thread{
@Override
public void run() {
super.run();
}
}
方式二:通过 Runnable 接口创建线程类。
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
方式三:通过 Callable 和 Future 创建线程。
/**
* Java中创建线程方式三:Callable和FutureTask结合使用
*/
public class CallableTest implements Callable{
@Override
public Object call() throws Exception {
int i = 1000;
for ( ; i < 1010; i++) {
System.out.println(i);
}
return 1111;
}
public static void main(String[] args) {
CallableTest callableTest = new CallableTest();
FutureTask<Integer> futureTask = new FutureTask<Integer>(callableTest);
Thread thread = new Thread(futureTask);
thread.start();
try {
System.out.println("Result:"+futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
5、死锁问题
两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程都陷入了无限的等待中。
死锁产生的 4 个必要条件:
互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一 段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程 只能等待。
不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺 走,即只能 由获得该资源的进程自己来释放(只能是主动释放) 。
请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求, 而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不 放。
循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的 资源同时被 链中下一个进程所请求。
如何确保N个线程可以访问N个资源同时又不导致死锁?
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。
6、线程的调度算法是什么?它的作用是什么?
- 抢占式调度 (Preemptive Scheduling):操作系统可以强制剥夺当前正在执行的线程的CPU时间片,并分配给其他优先级更高的线程。常见的抢占式调度算法有 Round-Robin(轮询调度)和优先级调度。
- 协同式调度 (Cooperative Scheduling):线程自愿放弃 CPU 时间片,而不是被操作系统强制剥夺。线程通过显式地调用
yield()
或等待某些条件的发生来让出 CPU。
作用:
- 线程调度器是操作系统的一部分,负责决定哪个线程在给定的时间段内执行。
- 它的主要作用是根据特定的调度算法,将处理器时间片分配给不同的线程,以实现多线程的并发执行。
7、什么是线程上下文切换?如何减少线程上下文切换的开销?
线程上下文切换是指在多线程并发执行时,由于CPU需要切换线程执行,需要将当前线程的状态保存下来,然后加载另一个线程的状态。会涉及到CPU寄存器、线程堆栈、程序计数器等上下文信息的保存和恢复。线程上下文切换的开销比较大,可以采取以下措施减少上下文切换的开销:尽量避免线程间的互斥和同步;采用CAS算法等非阻塞算法;采用协程等轻量级线程模型;减少线程的数量;使用线程池;减少锁的持有时间等。