5. Java并发编程-Java线程状态、生命周期

首先Java中的线程对应到OS中的线程,深刻理解了OS中的线程学习Java中线程是非常简单的。

通用OS中线程状态

线程生命周期中涉及到五个状态,分别是:new(初始), ready(就绪), running(运行), block(阻塞)和terminated(终止)

new, ready, runnning, terminated比较好理解,重点分析一下block。

当运行态的线程调用了一个阻塞API(如读写文件),在条件变量上等待(wait),休眠(sleep), 等待其他线程结束(join)等,此时进入block状态。

Java中线程状态

Java中线程状态相比通用线程状态做了一些细分,包括:
1)NEW
2) RUNNABLE (就绪或运行中)
3)BlOCKED
4)WAITING
5) TIMED_WAITING
6) TERMINATED

Java中线程状态对阻塞状态做了细分, 用Runnable包括了就绪和运行中。

BLOCKED

只有当线程等待synchronized隐式锁时,线程会从RUNNABLE转换到BLOCKED状态。

WAITING

从RUNNABLE转换到WAITING有三种场景会触发这种转换。

1)获得synchronized隐式锁的线程,调用了无参数的wait()方法
2)调用了目标线程的join()方法,此时本线程会阻塞等待目标线程完成
3)调用了LockSupport.park()方法(jdk并发包中的锁是基于它实现)则线程重RUNNABLE变成WAITING, 当调用LockSupport.unpark(Thread t)又可唤醒目标线程,状态又从WAITING变成RUNNABLE。

TIMED_WAITING

有五种场景会触发这种状态转换:
1)调用Thread.sleep(long millis); 本线程会阻塞若干毫秒,期间不释放锁,时间到自行恢复
2)获得synchronized隐式锁的线程,调用了带参数的wait(long timeout)方法
3) 调用了目标线程带参数的join(long millis)方法,此时本线程会阻塞等待目标线程完成
4) 调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
5) 调用带超时参数的 LockSupport.parkUntil(long deadline) 方法。

TERMINATED

线程执行完run方法自动转换为TERMINATED状态,如果在run方法中抛出异常,线程也会终止。 另外如果需要强制终止线程执行,调用interrupt()方法。

interrupt vs stop
早期终止线程可以用stop方法,如今以及标记为@Deprecated, 原因是调用stop()方法后线程立即被kill,都没有释放锁的机会,导致其他线程获取不到锁。

interrupt如何使用
interrupt方法仅仅是通知线程,被interrupt的线程有机会执行一些后续操作,也可以无视这个通知。 可以通过捕获异常和主动检测的方式得到interrupt通知:
1)当线程处于WAITING,TIMED_WAITING时,线程会返回到RUNNABLE状态,且触发InterruptedException
2)当线程处于RUNNABLE时,并且阻塞在 java.nio.channels.InterruptibleChannel上时,会触发java.nio.channels.ClosedByInterruptException; 阻塞在java.nio.channels.Selector 上时,如果其他线程调用线程 A 的 interrupt() 方法,线程 A的 java.nio.channels.Selector 会立即返回
3)当线程处于RUNNABLE, 且并未阻塞在IO上,此时就得依赖线程主动检测了,通过 isInterrupted()方法判断。

jstack

jstack是一个jvm命令,当系统疑似出现活跃性问题(死锁,活锁,饥饿),我们需要使用该命令dump当前jvm线程栈,然后分析各线程的状态,排查问题。

jstack -l PID > dump.txt
如何设置正确的线程数

工作中常常会遇到诸如配置线程连接池,数据库连接池,tomcat连接数等,到底配置多少合适, 本节进行展开。

为什么需要使用多线程

目的当然是提升性能,充分利用多核CPU,使用吞吐量和延迟两个指标来度量,期望是低延时和高吞吐量。

多线程的应用场景

如何实现低延时和高吞吐量,基本有两个方向:优化算法和充分利用硬件性能。前者是算法方向,不是我们我们本文研究的重点,而第二硬件方面主要是两类:CPU和IO。并发编程中提升性能的本质就是提升CPU和IO的利用率。

创建多少线程合适

一般程序都是CPU计算和IO操作混合,两者交叉执行。如果IO操作执行的时间相比CPU执行时间要长很多,就可以称为IO密集型计算,反之称为CPU密集型应用。两个场景下,计算最佳线程数的方法是不同的

对于CPU密集型应用,多线程本质上是提升多核利用率,所以理论上运行线程数等于核心数就可以了,再多创建线程只会增加线程切换的成本,性能反而降低。在工程上一般设置为核心数+1

对于IO密集型应用,最佳线程数 =核心数 * (1 +(I/O 耗时 / CPU 耗时)), 如IO和CPU对半时,单个U最佳线程只需要2个即可。 在工程实践中可以设置为初始值:2*核心数 + 1, 通过压力测试的方式观察吞吐量和延迟,一般来讲IO/CPU会大于1,从压力测试的结果来看随着线程数的增加,吞吐量增加,延迟小幅增加,至吞吐量增长缓慢甚至开始下降,而延迟快速增加时即为最佳线程数。

用面向对象思想写好并发程序

可以从封装共享变量、识别共享变量间的约束条件和制定并发访问策略这三个方面下手。

封装共享变量

将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略。对于不会发生变化的共享变量,使用final来修饰。

识别共享变量间的约束条件

识别这些约束条件很重要,因为这些约束条件,决定了并发访问策略。

制定并发访问策略

通常对应三种方案:
1)避免共享:如ThreadLocal
2) 不可变模式,即Immutable
3) 互斥锁:synchronized, 或并发包中的锁

总结

本节先学习了线程状态,再讨论了设置线程数的话题,最后总结了如何用面向对象思想写好并发程序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值