Java线程的实践及原理揭秘
- 并发是什么?
- 系统支持高并发的因素是哪些?
1.如何理解系统的并发
一般来说,系统在单位时间内能够承载的并发数就是整个系统同事能够处理的请求数量。对于并发的指标通常通过TPS/QPS来表示
- QPS:每秒处理的查询数(Queries-Per-Second)
- TPS:每秒处理的事务数(Transactions-Per-Second)
2.系统如何支撑高并发
从一个完整架构来说,无非就是通过软件和硬件层面
硬件的组成CPU,网卡,带宽,磁盘,内存
对I/O性能要求比较高尽可能多的使用内存来存储,
对于运算性能比较高的,使用对线程处理
如果单个计算机硬件资源达到瓶颈,可以采用水平扩展的方式来提升性能。即横向增加服务器,利用多个计算机组成分布式计算机,当然,整个系统的复杂性增加,比如涉及服务治理,服务监控,服务的高可用。
对于一个高可用的应用来说,最大的问题就是如何让更多用户在最短时间内获取想要的信息及完成想要的操作,一般用RT(Reaction-Time)作为衡量指标
软件层面上的一些其他优化方案
- 集群化部署
- 多线程异步化
- 缓存机制
- CDN(内容分发网络)
3.线程的前世今生
线程是操作系统能够运算和调度的最小单元,一个进程可以创建多个线程,每个线程可以并行执行多个任务,并行执行的线程的数量是由CPU的核心数量来决定的。
利用多道程序和CPU时间片调度的方式让多个用户可以同时使用一台计算机的方式是分时系统即计算机对资源的一种共享方式
什么是多道程序?由于单个程序无法让CPU和I/O设备始终处于忙碌状态,所以操作系统允许同时加载多个程序到内存,也就是说可以同时启动多个进程,系统给这些进程分配独立的地址空间,以保证每个进程的地址不会相互干扰。
ulimit -n:查看进程能够并行处理的连接数
4.Java中如何使用多线程
在Java中实现多线程的方式有:
- 继承Thread类
- 实现Runnable接口
- 使用ExecutorService线程池
- 使用Callable/Future实现带返回值的多线程
5.多线程如何应用到实际场景
- 网络请求分发场景
- 文件导入处理的场景
- 异步业务场景
Tomcat 7之后采用线程池改造的BIO,在Zookeeper,Nacos,Dubbo等中间件
6.多线程的基本原理
public class ThreadExample implements Runnable{
@Override
public void run() {
System.out.println("Running");
}
public static void main(String[] args) {
new Thread(new ThreadExample(),"THREAD-1").start();
}
}
void Thread::start(Thread* thread) {
trace("start", thread);
if (!DisableStartThread) {
if (thread->is_Java_thread()) {
java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
java_lang_Thread::RUNNABLE);
}
os::start_thread(thread);
}
}
7.线程的运行状态
public enum State {
// 新建状态,也就是说调用new Thread()时的状态
NEW,
// 运行状态,通过start()方法启动后的状态
RUNNABLE,
// 阻塞状态,执行Synchronized代码,并且未抢占到同步锁时,会变成该状态
BLOCKED,
// 调用Object.wait()等方法,会让线程变成该状态
WAITING,
// 超时等待状态,sleep(timeout),超时会自动唤醒
TIMED_WAITING,
// 终止状态,线程的run()方法中的指令执行完成后的状态
TERMINATED;
}
TIME_WAITING状态演示
public class TimeWaitingStatusExample {
public static void main(String[] args) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
,"TIME_WAITING").start();
}
}
E:\IDEA Space\school\thread-01\target\classes\cn\zhima>jps
7520 TimeWaitingStatusExample
17412 Launcher
19748 Jps
8932
8940 RemoteMavenServer
E:\IDEA Space\school\thread-01\target\classes\cn\zhima>jstack 7520
2023-08-05 10:21:12
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.121-b13 mixed mode):
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x000000000134e800 nid=0x29ec waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"TIME_WAITING" #12 prio=5 os_prio=0 tid=0x000000001b816000 nid=0x46ec waiting on condition [0x000000001c08e000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at cn.zhima.TimeWaitingStatusExample.lambda$main$0(TimeWaitingStatusExample.java:16)
at cn.zhima.TimeWaitingStatusExample$$Lambda$1/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
8.如何正确的终止线程
- stop()
- kill - 9
- interrupt()
- 设置中断标识,把run()中的指令执行完成,再完成线程中断的功能
- 如果想把被阻塞的线程中断,则此线程需要先被唤醒,然后才能中断
void Thread::interrupt(Thread* thread) {
trace("interrupt", thread);
debug_only(check_for_dangling_thread_pointer(thread);)
os::interrupt(thread);
}
void os::interrupt(Thread* thread) {
assert(Thread::current() == thread || Threads_lock->owned_by_self(),
"possibility of dangling Thread pointer");
// 获取本地线程
OSThread* osthread = thread->osthread();
// 判断本地线程对象是否为中断状态
if (!osthread->interrupted()) {
// 设置中断标识为true
osthread->set_interrupted(true);
OrderAccess::fence();// 内存屏障,用来解决可见性问题
// slp -> sleep 判断当前线程是否处于sleep状态,唤醒该线程
ParkEvent * const slp = thread->_SleepEvent ;
if (slp != NULL) slp->unpark() ;
}
//及时已经处于挂起状态的线程,也要进行唤醒
if (thread->is_Java_thread())
((JavaThread*)thread)->parker()->unpark();
ParkEvent * ev = thread->_ParkEvent ;
if (ev != NULL) ev->unpark() ;
}
volatile bool interrupted() const { return _interrupted != 0; }
void set_interrupted(bool z) { _interrupted = z ? 1 : 0; }
9.理解上下文切换带来的性能问题
为了支持更多的线程运行,CUP会把自己的时间片分配给其他线程,这个过程就是上下文切换
- 进程上下文切换(进程的上下文切换,是指当前进程的CPU时间片分配给其他进程执行,进程切换分三种情况)
- CUP时间片分配
- 当进程系统资源不足,进程会被挂起
- 当存在优先级更高的进程运行时,当前进程可能会被挂起,CPU时间片分配给优先级更高的进程运行
- 线程上下文切换
- 当两个线程切换属于不同的进程时,由于进程资源不共享,所以线程切换其实就是进程的切换
- 当两个线程属于同一进程时,需要保存线程的上下文
- 中断上下文切换
- CPU本身故障,程序故障
- I/O中断
如何减少上下文切换?
- 减少进程数
- 采用无锁的设计(CAS)
10.守护线程
thread.setDaemon(true);
应用场景:GC,事件监听,心跳检测
注意:线程状态的继承及thread.setDaemon(true);要写在start之前
11.快速定位并解决线程导致的生产问题
- 通过jps命令查看java进程的pid
- 通过jstack 命令查看线程的dump日志
- 如果是死锁—>Found one Java-level deadlock