小明谈谈你对多线程的理解建议收藏
Posted 小明の学习心得
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小明谈谈你对多线程的理解建议收藏相关的知识,希望对你有一定的参考价值。
一、【基础】首先谈谈什么是进程什么是线程,为什么要用多线程
【背诵直接答,*亮点】
*在任何一个操作系统和cpu中,执行的时候必须执行程序的指令,不管什么语言最后都要转为指令,保存指令的程序,在Linux中保存程序有两种方式,在windows是.exe,我们在执行程序时,需要把二进制代码拿到内存中,开辟的空间就是进程的资源,进程可以读写数据执行代码,进程和线程 在 操作系统 层面 的控制块都是 task start,但是线程是需要共享进程的空间,所以两个进程中共享了部分资源就叫线程*。
进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等,进程是系统进行资源分配和调度的一个独立单位。线程是CPU调度的最小单位,必须依赖于进程而存在,是CPU调度和分派的基本单位。线程拥有程序计数器,一组寄存器和栈。
使用多线程的意义(好处)主要有(1)可以发挥多处理器的最大能力(2)异步事件的简化处理(3)响应更灵敏
例如在我的项目中:下订单并给用户推送短信、邮件就可以进行拆分,将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。
二、【基础】再谈线程的生命周期和线程的状态
【线程的生命周期】首先是NEW 新建,就是刚使用new() 方法,new出来的线程;第二步是就绪,就是调用的线程的start() 方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;第三运行 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run() 方法定义了线程的操作和功能;第四步,阻塞 在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait() 之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;第五步 销毁 如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源
【线程的基本状态】由于jvm采用liunx线程模拟java线程,本质上是进程的就绪(Ready-对应new)、运行(Running-对应RUNNABLE)、阻塞状态(Blocked-对应 BLOCKED和 WAITING)
按JDK的源码分析来看,Thread 的状态分为:
NEW : 尚未启动的线程的线程状态
RUNNABLE: 处于可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统(例如处理器)的其他资源
BLOCKED: 线程的线程状态被阻塞,等待监视器锁定。处于阻塞状态的线程正在等待监视器锁定以输入同步的块方法或在调用后重新输入同步的块方法,通过 Object#wait()
进入阻塞
WAITING:处于等待状态的线程正在等待另一个线程执行特定操作;例如: 在对象上调用了Object.wait() 方法的线程正在等待另一个线程调用Object.notify() 方法或者Object.notifyAll(), 调用了 Thread.join() 方法的线程正在等待指定的线程终止;TIMED_WAITING : 具有指定等待时间的等待线程的线程状态。由于以指定的正等待时间调用以下方法之一,因此线程处于定时等待状态:
三、【基础】再谈多线程使用
【*启动线程有几种方式——1种2种还是4种】
JDK源码注释说2种:There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread。 The other way to create a thread is to declare a class that implements the Runnable interface. 意思是有两种方法可以创建新的执行线程一种是将类声明为Thread的子类(继承Thread重写run() 方法),另一种方法是声明一个继承Runnable 接口的类(继承Runnable 重写run()方法,把run()方法的实例传到Thread类中)。
但是我们知道还有,有返回值的 Callable 创建线程、线程池 Futuretask 创建线程这两种
但是我们看源码可以得到,创建线程只有一种方式,那就是构造一个Thread 类在 start() 中调用系统函数start0();创建线程。
【如何优雅的关闭线程?】
线程Thread的API就是暂停suspend() 、恢复resume() 和停止stop() 。但是这些API是过期的
正确的做法是,调用了 interrupted() 方法,中断标志被设置为true
java里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true来进行响应,线程通过方法isInterrupted() 来进行判断是否被中断,也可以调用Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
调用线程类的interrupted方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupted方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程 。* 处于死锁状态的线程无法被中断
【线程间如何协作的,线程的状态如何切换】
wait() 调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁;尽可能用notifyall()通知所有等待在该对象上的线程,谨慎使用notify(),因为notify()只会唤醒一个线程
join() 方法把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。(此处为常见面试考点)
yield() 方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行yield( )的线程不一定就会持有锁,我们完全可以在释放锁后再调用yield方法。所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
sleep() 方法被调用以后,持有的锁是不释放的
【调用yield() 、sleep()、 wait()、notify() 等方法对锁有何影响?】
yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。调用notify()系列方法后,对锁无影响,线程只有在syn同步代码执行完后才会自然而然的释放锁,所以notify() 系列方法一般都是syn同步代码的最后一行。
四、【基础】聊聊线程安全、活跃态问题,竞态条件?
线程安全的活跃性问题可以分为 死锁、活锁、饥饿
活锁 就是有时线程虽然没有发生阻塞,但是仍然会存在执行不下去的情况,活锁不会阻塞线程,线程会一直重复执行某个相同的操作,并且一直失败重试1. 我们开发中使用的异步消息队列有可能造成活锁的问题,在消息队列的消费端如果没有正确的ack消息,并且执行过程中报错了,就会再次放回消息头,然后再拿出来执行,一直循环往复的失败。这个问题除了正确的ack之外,往往是通过将失败的消息放入到延时队列中,等到一定的延时再进行重试来解决。
解决活锁的方案很简单,尝试等待一个随机的时间就可以,会按时间轮去重试
饥饿 就是 线程因无法访问所需资源而无法执行下去的情况,饥饿 分为两种情况:一种是其他的线程在临界区做了无限循环或无限制等待资源的操作,让其他的线程一直不能拿到锁进入临界区,对其他线程来说,就进入了饥饿状态;一种是因为线程优先级不合理的分配,导致部分线程始终无法获取到CPU资源而一直无法执行(读写锁会发生饥饿)
解决饥饿的问题有几种方案:(1)保证资源充足,很多场景下,资源的稀缺性无法解决(2)公平分配资源,在并发编程里使用公平锁,例如FIFO策略,线程等待是有顺序的,排在等待队列前面的线程会优先获得资源(3)避免持有锁的线程长时间执行,很多场景下,持有锁的线程的执行时间也很难缩短
死锁 首先需要将死锁发生的是个四个必要条件讲出来:互斥条件 同一时间只能有一个线程获取资源;不可剥夺条件 一个线程已经占有的资源,在释放之前不会被其它线程抢占;请求和保持条件 线程等待过程中不会释放已占有的资源;循环等待条件 多个线程互相等待对方释放资源
预防死锁问题,就是需要破坏这四个必要条件,一般 资源互斥 是无法改变的;
我们可以破坏不可剥夺条件, 一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行
破坏请求与保持条件,有两种方法,第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源;第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源
破坏循环等待条件,一般采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
以上是关于小明谈谈你对多线程的理解建议收藏的主要内容,如果未能解决你的问题,请参考以下文章