声明
多线程对我来说可能是比较薄弱的地方(那会上课在打盹),
然后实际开发中又用的比较少(其实一直都知道多线程的重要性,主要是懒),
这里的话就是自己这些天对多线程问题的一些整理和自我理解。
其实内容也大多都是参考别人的文档,然后加上自己的理解。
问题(1-10)
①并行和并发有什么区别?
解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二: 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三:
并行:指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,
使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。
(其实这个问题的话可以说是明白多线程的基础问题,但其实很多人都不一定清楚真正的解释方法。包括我)
②线程和进程的区别?
进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。线程又叫轻量级进程。
线程的划分小于进程,线程是隶属于某个进程的。进程是程序的一种动态形式,是CPU,内存等资源占用的基本单位,而线程是不能占有这些资源的。
进程之间相互独立,通信比较困难,而线程之间共享一块内存区域,通信比较方便。
进程在执行过程中包含比较固定的入口,执行顺序,出口,而线程的这些过程会被应用程序所控制。
进程
线程
其实简单来说就是,进程就像一个工厂,一个公司(操作系统)会有多个工厂部署在不同地方,所以他们之间不能互相交流,而且他们所生产的产品
也不一样;而线程就像是工厂里的某一条生产线,生产线与生产线之间可以互相交流,而且他们的生产只会受该工厂负责人的管理。
③守护线程是什么?
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。所以java里线程分2种,
- 前提
- 守护线程,比如垃圾回收线程,就是最典型的守护线程。
- 用户线程,就是应用程序里的自定义线程。
- 守护线程的机制
- 守护线程,专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,那么jvm就会退出(即停止运行)——此时,连jvm都停止运行了,守护线程当然也就停止执行了。
- 再换一种说法,如果有用户自定义线程存在的话,jvm就不会退出——此时,守护线程也不能退出,也就是它还要运行,干嘛呢,就是为了执行垃圾回收的任务啊。
- 守护线程又被称为“服务进程”“精灵线程”“后台线程”,是指在程序运行是在后台提供一种通用的线程,这种线程并不属于程序不可或缺的部分。 通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的“保姆”。
④创建线程有哪几种方式?
(其实像这种问题没必要出现在这个文章的,但由于我总是忘记单词英盲,所以还是写在这。忏悔)
- 继承Thread类,重写run方法;
- 实现Runnable接口,重写run方法,但是比继承Thread类好用,实现接口还可以继承类,避免了单继承带来的局限性;
- 使用Executor框架创建线程池。Executor框架是juc里提供的线程池。
- 补充线程(Thread)相关的方法
start():启动线程并执行相应的run()方法
run():子线程要执行的代码放入run()方法中
currentThread():静态的,调取当前的线程
getName():获取此线程的名字
setName():设置此线程的名字
yield():调用此方法的线程释放当前CPU的执行权(很可能自己再次抢到资源)
join():在A线程中调用B线程的join() 方法,表示:当执行到此方法,A线程停止执行,直至B线程执行完毕, A线程再接着join()之后的代码执行
isAlive():判断当前线程是否还存活
sleep(long l):显式的让当前线程睡眠l毫秒 (只能捕获异常,因为父类run方法没有抛异常)
线程通信(方法在Object类中):wait() notify() notifyAll()
getPriority():返回线程优先值 setPriority(int newPriority):改变线程的优先级设置线程的优先级(非绝对,只是相对几率大些)
⑤说一下 runnable 和 callable 有什么区别
- 相同点
- 两者都是接口;(
废话) - 两者都可用来编写多线程程序;
- 两者都需要调用Thread.start()启动线程
- 不同点
- 两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
⑥线程有哪些状态?
线程状态有 5 种:新建,就绪,运行,阻塞,死亡
- 线程 start 方法执行后,并不表示该线程运行了,而是进入就绪状态,意思是随时准备运行,但是真正何时运行,是由操作系统决定的,代码并不能控制(
玄学)。 - 同样的,从运行状态的线程,也可能由于失去了 CPU 资源,回到就绪状态,也是由操作系统决定的。这一步中,也可以由程序主动失去 CPU 资源,只需调用 yield 方法。
- 线程运行完毕,或者运行了一半异常了,或者主动调用线程的 stop 方法,那么就进入死亡。死亡的线程不可逆转。
- 下面几个行为,会引起线程阻塞:
主动调用 sleep 方法。时间到了会进入就绪状态 主动调用 suspend 方法。主动调用 resume 方法,会进入就绪状态。
调用了阻塞式 IO 方法。调用完成后,会进入就绪状态。 试图获取锁。成功的获取锁之后,会进入就绪状态。 线程在等待某个通知。其它线程发出通知后,会进入就绪状态
⑦sleep() 和 wait() 有什么区别?
- 同步锁的对待不同:
**sleep()**后,程序并不会不释放同步锁。
**wait()**后,程序会释放同步锁。
- 用法的不同:
sleep()**可以用时间指定版来使他自动醒过来。如果时间不到你只能调用interreput()来强行打断。wait()可以用notify()**直接唤起。
⑧notify()和 notifyAll()有什么区别?
锁池
假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法 (或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池
假设一个线程A调用了某个对象的 wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。
而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
⑨线程的 run()和 start()有什么区别?
调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run() ;直接调用run()方法,无法达到启动多线程的目的,相当于主线程线性执行Thread对象的run()方法。
一个线程对线的 start()方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常; run() 方法没有限制。
⑩创建线程池有哪几种方式?
newCachedThreadPool() ,它是用来处理大量短时间工作任务的线程池,具有几个鲜明特点:
它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
如果 线程闲置时间超过60秒,则被终止并移除缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用
SynchronousQueue 作为工作队列。newFixedThreadPool(int nThreads) ,重用指定数目( nThreads )的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动线程数目,将在工作队列中等待空闲线程出现;
如果工作线程退出,将会有新的工作线程被创建,以补足指定数目nThreads。
newSingleThreadExecutor() ,它的特点在于工作线程数目限制为1,操作一个无界的工作队列,所以它保证了所有的任务都是被顺序执行,最多会有一个任务处于活动状态,并且不予许使用者改动线程池实例,因此可以避免改变线程数目。
newSingleThreadScheduledExecutor() 和newScheduledThreadPool(int corePoolSize) ,创建的是个 ScheduledExecutorService ,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
newWorkStealingPool(int parallelism) ,这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建 ForkJoinPool ,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
问题(11-20)
⑪线程池都有哪些状态?
RUNNING :线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。
SHUTDOWN :不接收新任务,但能处理已排队的任务。调用线程池的 **shutdown() ** 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。
STOP :不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由( RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。
TIDYING: SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated() 方
法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。
线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就
会由 SHUTDOWN 转变为 TIDYING 状态。线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为
TIDYING 状态。TERMINATED :线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED状态。
⑫线程池中 submit()和 execute()方法有什么区别?
submit(Callable task) 、 submit(Runnable task, T result) 、 submit(Runnable task) 归属于 ExecutorService 接口。
execute(Runnable command) 归属于Executor接口。ExecutorService继承了 Executor 。
⑬在 java 程序中怎么保证多线程的运行安全?
参考文章:http://www.jasongj.com/java/thread_safe/
- 线程的安全性问题主要体现在以下几个特性:
- 原子性 :一个或者多个操作在 CPU 执行的过程中不被中断的特性。
关于原子性,一个非常经典的例子就是银行转账问题 :比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。
- 可见性 :一个线程对共享变量的修改,另外一个线程能够立刻看到。
CPU从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。
有序性 :程序执行的顺序按照代码的先后顺序执行。
boolean started = false; // 语句1
long counter = 0L; // 语句2
counter = 1; // 语句3
started = true; // 语句4
从代码顺序上看,上面四条语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们一定完全按照此顺序执行。
处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。
CPU不按照我的代码顺序执行代码,那怎么保证得到我们想要的效果呢?实际上,完全可以放心,CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。
- 导致的原因如下:
- 缓存导致的可见性问题
- 线程切换带来的原子性问题
编译优化带来的有序性问题解决办法:
JDK Atomic 开头的原子类、 synchronized 、 LOCK ,可以解决原子性问题 synchronized 、 volatile 、 LOCK ,可以解决可见性问题 Happens-Before 规则可以解决有序性问题。
- 本文链接:https://mu-li.cn/2020/12/04/multithreading/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。
您也可以跳转 GitHub Issues 评论,若您无 GitHub 账号,可直接在下方填写信息后进行评论。
注意:邮箱会识别qq邮箱或者注册了gravatar的邮箱账号的头像信息(国内访问不支持gravatar可能要科学上网)~
GitHub Issues