Java多线程理论知识(持续更新)

1. 什么是线程

相信这个问题已经有很官方的回答,线程是轻量级的进程。

那么进程有什么?一般意义上可以理解为一个应用对应一个进程。比如你打开浏览器,打开游戏, JVM等等这些都在操作系统层面展现为一个进程。进程是操作系统给出的定义, 操作系统以进程为单位分配系统资源这就是 JVM 可以设置最大最小memory的由来了, 其实最终呢还是把这些参数提交给操作系统,做资源分配。

进程是解释清楚了,再来说线程。当你打开一个应用的时候,不可能同时只干一件事吧,为了更好的使用系统资源,我们会在进程内部向操作系统提交一些task,而这些task执行单位就是线程,所以线程是一个逻辑概念。

2. 为什么需要多线程

在IO密集型服务器,如web应用等大部分时间可能耗费在数据查询,调用下游服务上,这样如果系统内部只有一个线程的话,就不能很好的利用系统资源了。所以多线程产生的背景也是软件向硬件的一次压榨了,压榨内存,压榨CPU。

3. Java 的线程和操作系统的线程有什么关系

没有关系,只是名字相同而已。

操作系统线程,是操作系统执行任务的最小单位

Java的线程是JVM层面的逻辑概念,对外体现为一个进程。

操作系统线程会在不同进程之间切换,执行不同的任务。

类比一下, 操作系统线程就好比拉货的大卡车,而Java线程就好比货物,大卡车有多辆,货物也有多个,大卡车会在这些货物之前选择运送, 直到送完为止。卡车送货物的同时,会记录当次的运送状态(线程上下文, 需要耗费时间),以便下一次接着送。

4. 多线程一定就快?

不一定, 对于IO密集型应用确实可以提高效率,但对于CPU密集型却不一定有时还会慢。

我们需要多线程初衷就是要利用被IO展区的那段时间了的CPU资源,但是如果我们IO速度非常快呢?那就得另说了,像Redis这种内存型数据库,由于IO速度非常的快,再要用多线程就会适得其反。因为CPU在做上下文切换的时间已经超过IO的时间了。

所以当我们单线程,已经能有一个很高的CPU利用率,那么就没必要使用多线程。

5. 多线程时刻要考虑同步?

要解释这个问题得先搞清楚什么是多线程竞争,在多线程环境下,当多个线程同时对一个共享变量

进行写操作是就可能会发生线程竞争问题。 比如一线程把共享变量的值改成了10, 另一个要把共享变量的值改成20,那最终的结果就可能是10或者20,这是因为线程执行的不确定性(执行期间可能会发生cpu时间片切换)。

但是换句话说,如果程序执行期间不会对共享变量执行写操作,或者压根不涉及共享变量也竞争也就无从谈起,更遑论线程同步。

6. ThreadLocal能解决线程竞争问题么?

能或者不能。

ThreadLocal解决线程竞争问题选择了线程间不共享变量的实现(线程本地变量),所以ThreadLocal能不能避免线程竞争,在于你传给ThreadLocal是不是共享变量而不在于ThreadLocal本身。

如果你在ThreadLocal初始化时传入了共享变量像下面这样, 那么你就要自己处理线程同步问题了。

int val = 1;
ThreadLocal<Integer> tl = ThreadLocal.withInitial(() -> val);

而正确的ThreaLocal用法如下。

ThreadLocal<Integer> tl = ThreadLocal.withInitial(() -> 1);

7. 单例对象传入ThreadLocal,线程安全吗?

参考上面的ThreadLocal用法不难给出答案, 取决于你传入的单例对象本身是否线程安全。

因为在这种情况下ThreadLocal将不能提供线程级隔离了。

非要给答案,就是能(单例安全),或者不能(单例不安全)分情况讨论。

8. 什么是线程池

类比String常量池、DB连接池,其实就是一组线程而已,从这个概念上理解叫它线程组(Thread Group)也是可以的,相信已经有人听过这个叫法了,都是一回事的。

9.为什么需要需要线程池

主要的切入点我觉得是可以在系统中维护一组可以复用的线程,当然JVM创建和销毁线程是需要时间的,但是相对于耗时的业务逻辑我想这个时间是可以忽略的。

那么线程池能带来什么便利呢?

> 提供统一的线程创建和使用接口,简单易用

> 控制线程的数量,避免在极端情况下JVM需要创建大量的线程而导致内存耗尽,或应用卡死的情况

>在系统因为大量请求而即将崩溃时,线程池可以通过一些策略(拒绝策略, 后面会有专门的内容来说这块知识),来维持系统的基本可用,为什么时基本可用呢?因为当线程池资源耗尽时,新来的请求就会执行拒绝策略,这个时候拒绝策略就很重要了,一般默认是抛出异常哈,这样就会有一部分用户体验不好了哦。

10. 线程池一定就好么?

这个世界没有没有完美的人,也没有完美的系统,好不好主要在于你的应用场景。

在使用线程池的时候,线程池里的线程会跟随应用的启动创建,应用结束时销毁,这样就会有个问题,对于那些线程级别的共享变量就不得注意下了。

举个例子,如ThreadLocal, 在极端情况下(你很偏爱线程本地变量,通过程序生成了大量这样的变量),就会导致因为ThreadLocal而引发内存泄漏,进而导致App崩溃。所以在使用ThreadLocal时,需要额外注意这种情况。

11. Java线程的生命周期

> new -- 线程刚刚创建完成,初生状态

> runnable -- 可执行/就绪状态,初生状态的线程调用start方法、线程在阻塞时成功抢到锁、wait方法被唤起、wait方法超时后都会进入这个状态

> running -- 执行状态,当前线程获取到cpu资源正在执行逻辑,cpu会在多个running的线程之前切换执行,这点很重要

> blocked -- 线程抢占synchronized锁资源失败后,进入该状态,从blocked到runnable状态转换会产生内核态到用户态的切换

> waiting/timed_wating -- 线程调用wait, wait(long) 后进入该状态,wait状态属于用户态

> terminate  -- 未被线程池管理的线程,当线程内的任务执行完成后释放资源,销毁线程

> interrupted -- 很多资料都会没有这个状态, 我认为这个也应该时线程生命周期的一部分,当线程调用interrupt后进入该状态,中断状态是一个mark状态,要怎么处理中断要看业务逻辑。比如业务中需要发起batch Job,当你点了启动后在中途想要退出怎么办?其实就可以借助这个中断状态来实现,主程序设置业务线程状态为中断状态,业务线程不断监控中断,一旦发现当前线程被中断就抛出中断异常或退出Job。

12. 关于Java中的锁

我第一次听说这个叫法之后,还跟其他人理论一阵子,明明是sychronized关键字嘛,咋就成锁了呢?这个得从sychronized关键字底层实现说起,加上这个关键字底层会锁住总线(暂且就最差情况来说),而且我们也可以从编译后的字节码文件看出一些端倪,会有一条lock指令。这其实是把关键字的作用用到了关键字的表面含义了,要不然直译sychronized关键字,会让很多人搞不懂它在做什么了。

JDK1.5以后,Java针对这个关键字,做了一些优化,具体操作过程如下:

> 偏向锁: 最接近无锁的状态,当只有一个或少量线程反复执行加锁的代码时,JVM总是喜欢将锁分配给已经抢占过锁的线程,这个分配过程就是偏心的过程,偏心于那些已经混了脸熟的线程,偏向锁的名字也是由其操作过程中翻译而来的;

> 轻量级锁: 当有数个线程同时竞争锁资源时,偏向锁已经不能完全handle这种情况,JVM会启动轻量级锁代替偏向锁,获取成功的线程执行业务逻辑,失败的线程则进入自旋状态,至于自旋多久也是有默认值的,超过后退出等待;

> 重量级锁: JVM保证线程同步的兜底操作,在轻量级锁状态下任不能很好的处理线程同步,有很多线程因为等待超时或抢锁行为过于密集,JVM会切换至重量级锁,之所以称为重量级锁,是因为这个过程是借助于底层操作系统完成的,这样cpu就会从应用态切换至内核态,锁释放后再回到应用态这个切换过程很耗时。举个形象的例子,比如你驾驶汽车进入路口时遇到了红灯,轻量级锁的做法时刹车等待(车子还是启动状态),而重量级锁的做法是直接将车子熄火。那种做法比较好,一眼就能看出来。

> 轻量级锁与重量级锁的对比:

1)轻量级锁不会导致系统在应用态与内核态切换,线程执行更高效省时,重量级锁则相反;

2) 当有多个线程在轻量级锁自旋时,由于线程不会释放cpu资源,这样就会造成cpu资源浪费(占着茅坑不拉屎),而重量级锁则不会造成cpu压力。

偏向锁升级为轻量级锁,再升级为重量级锁的过程,由JVM自己控制,用户不可控。

13. 有没有一种加锁方式可以让Java自己控制锁的加锁释放过程呢?

有,它就是JUC工具包提供的锁,有很多的比较常用的是ReentrantLock。当然JUC这些锁都是轻量级的锁,优点和缺点在前一个知识点里已经分享了哈,可以看回知识点12。

14. 多线程执行任务时,CPU会在各个线程间切换,如何保证程序完整执行呢?

要解释这个问题得分两个层面

一个是硬件层面,我们都知道CPU内部有个寄存器,专门用来存储CPU下一次要执行的代码位置。这里为大家科普下,CPU其实分为两个部分一个控制部分,一个是运算部分,而程序计数器就是在控制器部分,帮忙控制CPU的工作过程。

另一个层面是JVM,一个CPU内的程序计数器只有一个如何支持多线程切换,这就得JVM解决了。在JVM内部也有个程序计数器,不过区别于硬件中的程序计数器,JVM中程序计数器是线程私有的,功能于硬件的一致,保存的是当前线程需要执行的下一个命令的位置。这样在多线程情况下JVM就会协助CPU完成线程的调度,JVM中的程序计数器跟随线程的创建而创建,线程完成任务回收后,程序基数器也就跟随其一起回收了。

15. 未完待续。。。

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值