多线程基础
1. 线程&进程
1.1 什么是线程和进程?
线程(Thread):线程是操作系统能够运算调度的最小单位,一条线程指的是进程中一个单一顺序的控制流。
进程(Process):进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是操作系统进行资源分配和调度的基本单位,是操作系统结构的基础。
1.2 线程和进程的关系?
线程被包含在进程之中,是进程中的实际运作单位。
一个进程中可以并发多个线程,每个线程并行执行不同的任务。(什么是并发、并行会在下文解释
同一进程中的多条线程将共享该进程的全部系统资源。
2. 并发&并行
2.1 什么是并发和并行?
并发(Concurrent):一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都在同一个处理器上运行,但在任一时刻只有一个程序在处理器上运行。
当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。
并行(Parallel):一组程序按独立异步的速度执行。
当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行。
2.2 并发和并行的区别?
(单看字面上的意思可能会觉得很难以理解,所以下面引用了知乎上的一个高赞答案来进行解释:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
所以我认为它们最关键的点就是:是否是『同时』。
作者:知乎用户
链接:https://www.zhihu.com/question/33515481/answer/58849148
来源:知乎
3. 多线程
3.1 什么是多线程?
多线程(Multithreading)是指从软件或者硬件上实现多个线程并发执行的技术。
3.2 为什么要使用多线程?
使用多线程能够快速选择其中一个已就绪线程去运行,而不是一个一个运行而降低效率。
在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,多线程就比较有用了,在这种情况下可以释放一些珍贵的资源如内存占用等。
3.3 多线程可能会带来哪些问题?
如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。
更多的线程需要更多的内存空间。
线程可能会给程序带来更多“bug”,因此要小心使用。
线程的中止需要考虑其对程序运行的影响。
通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。
4. 创建线程的方式
4.1 继承Thread类
继承Thread类需要重写run()方法,在run()方法中定义需要执行的任务:
public MyThread extends Thread {
public MyThread(){
}
@Override
public void run(){
System.out.println("Create a thread.");
}
}
创建了自己的线程类之后,就可以创建线程对象,然后通过调用start()方法启动线程。
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
}
4.2 实现Runnable接口
实现Runnable接口也需要重写run()方法。
public MyRunnable implements Runnable {
public MyRunnable() {
}
@Override
public void run(){
System.out.println("Create a thread.");
}
}
public class Test {
public static void main(String[] args) {
Thread th = new Thread(new MyRunnable());
//或者可以写成:
//MyRunnable mr = new MyRunnable();
//Thread th = new Thread(mr);
th.start();
}
}
5. 线程的生命周期和状态
线程的生命周期中要经历一下五种基本状态:
5.1 创建(New)
当一个线程对象被创建后,该线程进入创建状态。
Thread th = new Thread();
5.2 就绪(Runnable)
当线程对象被创建后调用start()方法,该线程进入就绪状态。
处于就绪状态的线程会在就绪队列中排队等候CPU资源,等待CPU调度执行。
th.start();
5.3 运行(Running)
线程获得CPU资源,开始执行任务(调用run()方法),即进入运行状态。
就绪状态是进入到运行状态的唯一入口。
5.4 阻塞(Blocked)
由于某种原因,处于运行状态的线程让出CPU并暂停运行,进入阻塞状态。
可以用一下方法使线程进入阻塞状态:
- 运行状态的线程调用wait()方法,线程进入等待阻塞状态,同时释放自己占有的锁资源,调用notify()方法可以使线程回到就绪状态。
- 调用sleep(long millis)方法使线程在一定时间内进入阻塞状态,但是不会释放锁资源。睡眠时间结束后线程进入就绪状态。millis参数以毫秒为单位设定睡眠时间。
- 在线程A当中调用线程B的join()方法,线程A转为阻塞状态,直到线程B运行结束,线程A重新进入就绪状态。
- 调用yield()方法可以暂停当前正在执行的线程对象,使其放弃分配到的CPU时间,把执行机会让给优先级相同或者更高的线程。但是被暂停的线程不会阻塞,而是处于就绪状态,随时能够再次获得CPU时间。
其中sleep(),join()和yield()都是Thread类的方法,wait()是Object类的方法。
5.5 死亡(Dead)
当线程执行完毕、被其他线程杀死或者因异常退出了run()方法,线程进入死亡状态,结束生命周期。
6. 什么是上下文切换?
上下文切换(Context Switch):在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境。
进程上下文用进程的PCB(进程控制块,也称为PCB,即任务控制块)表示,它包括进程状态,CPU寄存器的值等。
通常通过执行一个状态保存来保存CPU当前状态,然后执行一个状态恢复重新开始运行。
在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换帧”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。
在三种情况下可能会发生上下文切换:中断处理,多任务处理,用户态切换。在中断处理中,其他程序”打断”了当前正在运行的程序。当CPU接收到中断请求时,会在正在运行的程序和发起中断请求的程序之间进行一次上下文切换。在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换。对于一些操作系统,当进行用户态切换时也会进行一次上下文切换,虽然这不是必须的。
操作系统或者计算机硬件都支持上下文切换。一些现代操作系统通过系统本身来控制上下文切换,整个切换过程中并不依赖于硬件的支持,这样做可以让操作系统保存更多的上下文切换信息
链接:https://baike.baidu.com/item/%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2/4842616
来源:百度百科
7. 死锁
7.1 什么是死锁?
死锁(Deadlock)是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
7.2 死锁的产生条件
死锁的发生必须具备一下四个必要条件:
- 互斥条件:指进程对所分配到的资源进行排它性使用,也就是说在一段时间内某资源只能由一个进程占用。如果这个时候有其他进程请求资源,那么只能等占有资源的进程释放资源。
- 请求和保持条件:指进程已经至少获得了一个资源,但仍提出了新的资源请求,所需要的资源已经被其他进程占用,发出请求的进程进入阻塞状态,但是又不释放自己所占有的资源。
- 不剥夺条件:指进程已经分配到的资源,在使用完之前,不能被剥夺,只能在用完之后自己释放。
- 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
7.3 如何避免死锁?
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。只要打破四个必要条件之一就能有效预防死锁的发生:
打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。
打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。
所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源,在系统运行过程中,对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,若分配后系统可能发生死锁,则不予分配,否则予以分配。因此,对资源的分配要给予合理的规划。
链接:https://baike.baidu.com/item/%E6%AD%BB%E9%94%81/2196938#reference-[3]-121723-wrap
来源:百度百科
避免死锁的基本思想:系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源;如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入死锁状态的动态策略。
其中,避免死锁的算法中,最有代表性的算法是银行家算法。
8. 为什么调用start()方法时会执行run()方法,而不能直接调用run()方法?
new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
作者:BUG_攻城狮
链接:https://blog.csdn.net/abc_123456___/article/details/100835113
来源:CSDN