# 线程与多线程

# 了解高并发

高并发指的是:同一时间系统可以承受的并发数。

TPS 和 QPS 是衡量系统并发性能的两个常见指标,尤其在讨论数据库、Web 服务器或其他类型的服务时经常被提及。它们各自代表不同的含义:

  • TPS(Transactions Per Second):每秒事务数。这是一个度量单位,用于表示一个系统或服务每秒钟能够处理的事务数量。这里的“事务”可以指代任何预定义的操作集合,比如在数据库中完成的一次完整查询和更新操作。
  • QPS(Queries Per Second):每秒查询率。这是另一个度量单位,用来衡量系统或服务每秒能够处理的查询请求数量。通常用于描述Web服务器或数据库管理系统等能够处理的查询请求的数量。

简单来说,TPS 更侧重于整个事务的处理能力,而 QPS 则专注于查询请求的处理速度。两者都是评估系统性能的重要指标,特别是在高并发环境下,这些指标可以帮助了解系统的最大承载能力和响应效率。

# 如何支撑高并发

硬件资源:决定了应用系统性能的上限,主要包括以下硬件:

1.CPU的核心数决定着同一时间可以处理的并发任务数量上限

2.内存的大小,因为内存打了就可以存储更多的热点数据,又因为内存的IO速度很快,所以加快了数据处理的速度。

3.网卡 决定了网络数据的传输效率和响应时间。例如网卡的带宽是指在单位时间内可以传输的数据量。我们生活中100M,50M就是指定带宽。

软件层面主要解决一下三个问题:

1.充分的利用CPU的多核的能力

2.解决IO速度的影响。(IO -> 数据库交互 ->刷到磁盘 (IO速度影响着响应速度,那么怎么去提升IO速度呢) 可以使用内存/缓存/异步刷盘..;数据库做分库分表,分布式缓存,分布式消息中间件)

3.单节点系统瓶颈,可以多个计算机组成一个分布式系统。

# 一、线程与多线程

进程和线程是操作系统中的两个核心概念,它们都涉及到程序的执行,但有着不同的特点和用途。下面分别介绍进程和线程,并比较它们之间的区别。

# 进程(Process)

进程是指操作系统进行资源分配和调度的基本单位。它是程序执行过程中的一个实例,包含了正在运行的程序的代码、数据以及执行状态。每个进程都有独立的内存空间,包括代码区、数据区、堆栈区等,这使得进程间的数据通常是相互隔离的。进程的状态可以包括新建、就绪、运行、阻塞、终止等。

  • 资源拥有:进程是系统资源分配的基本单位,它拥有独立的地址空间、文件描述符等资源。
  • 隔离性:不同进程之间默认是相互隔离的,一个进程崩溃通常不会直接影响其他进程。
  • 开销:创建和销毁进程需要较大的系统开销,因为需要分配和回收资源。

# 线程(Thread)

线程是进程内的一个执行单元,是操作系统能够进行运算调度的最小单位。一个进程中可以包含多个线程,这些线程共享进程的资源,如内存地址空间、文件描述符等。线程之间的切换开销比进程小得多,因此多线程编程在处理并发任务时非常有效。

  • 资源共享:同一进程下的所有线程共享该进程的资源,包括内存地址空间和文件资源。
  • 轻量级:与进程相比,线程是一种更为轻量级的执行单位,创建、撤销和切换所需的开销更小。
  • 通信便捷:由于线程共享相同的地址空间,线程间的通信更加方便高效。

# 进程与线程的区别

  1. 资源占用:进程是资源分配的基本单位,拥有独立的资源;而线程是处理器任务调度和执行的基本单位,线程之间共享所属进程的资源。
  2. 系统开销:进程的创建、销毁和切换开销大于线程,因为进程间的数据和状态完全独立,而线程之间共享了大部分资源。
  3. 通信方式:进程间通信(IPC)较为复杂,通常需要使用专门的机制,如管道、消息队列等;而线程间可以直接访问相同内存区域,通信更为直接。
  4. 稳定性:由于进程间相互独立,一个进程的崩溃不会直接影响其他进程;而一个进程中某个线程的问题可能会影响整个进程的所有线程。

理解进程和线程的区别对于开发高效的并发程序至关重要。选择合适的设计取决于具体的应用场景和需求。

# 多线程

多线程是指在一个程序中同时运行多个线程,每个线程执行不同的任务。它是实现并发处理的一种方式,通过允许多个操作并行执行来提高程序的效率和响应速度。以下是关于多线程的一些重要概念、优势以及挑战。

# 多线程的优势

  1. 提高效率:在多核处理器上,多个线程可以同时执行,从而更高效地利用CPU资源。
  2. 增强响应性:对于GUI应用程序而言,使用多线程可以让耗时的操作在后台线程中进行,保持用户界面的流畅性和响应性。
  3. 资源共享:同一进程下的所有线程共享该进程的资源(如内存地址空间、文件描述符等),这使得线程间的通信和数据共享更加方便。
  4. 简化编程模型:在某些情况下,将复杂的应用程序分解为多个相对独立的任务(线程)可以使程序结构更清晰,便于管理和维护。

# 二、实现线程的方式

在Java中,实现线程的方式大体上分为三种,通过继承Thread类、实现Runnable接口,实现Callable接口。简单的示例代码分别如下所示。

  • 继承Thread类代码
package io.binghe.concurrent.executor.test;

/**
 * @author binghe
 * @version 1.0.0
 * @description 继承Thread实现线程
 */
public class ThreadTest extends Thread {
    @Override
    public void run() {
        //TODO 在此写在线程中执行的业务逻辑
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 实现Runnable接口代码
package io.binghe.concurrent.executor.test;

/**
 * @author binghe
 * @version 1.0.0
 * @description 实现Runnable实现线程
 */
public class RunnableTest implements Runnable {
    @Override
    public void run() {
        //TODO 在此写在线程中执行的业务逻辑
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 实现Callable接口代码
package io.binghe.concurrent.executor.test;

import java.util.concurrent.Callable;

/**
 * @author binghe
 * @version 1.0.0
 * @description 实现Callable实现线程
 */
public class CallableTest implements Callable<String> {
    @Override
    public String call() throws Exception {
        //TODO 在此写在线程中执行的业务逻辑
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 注意事项

直接调用run方法和通过Thread.start()方法的区别

# 调用 run() 方法

当你直接调用 run() 方法时,它就像普通方法调用一样执行,并不会启动一个新的线程。代码仍然运行在当前线程中。

特点:

  • 不会创建新线程:直接调用 run() 方法并不会启动新的线程,而是以普通方法的方式执行。
  • 顺序执行:代码会按照调用的顺序依次执行,没有并发行为。
  • 简单方法调用run() 方法只是一个普通的 Java 方法,调用它不会涉及线程的调度。

# Thread.start()方法

当通过 Thread.start() 方法启动线程时,JVM 会为该线程分配资源并调度其运行,最终会调用线程的 run() 方法。此时,代码会在新线程中执行。

特点:

  • 启动新线程start() 方法会通知 JVM 创建一个新线程,并让新线程执行 run() 方法。
  • 并发执行:新线程与主线程(或其他线程)可以并发运行。
  • 线程调度:由 JVM 的线程调度器决定线程的执行顺序。
  • 只能调用一次:每个线程对象的 start() 方法只能被调用一次,多次调用会抛出 IllegalThreadStateException

# 开启一个线程的发生了什么

首先我们通过执行一个main方法,开启虚拟机加载类执行到Thread.start()方法的时候就会通过JVM来调用操作系统的底层方法告诉CPU我要创建一个新的线程,创建成功之后我们的线程等待CPU的调度进行执行,当CPU把创建的线程交给CPU核心区执行

image-20250416050436991

# 1. Java 层面:主线程执行 Thread.start() 方法

  • 当程序运行时,JVM 启动并加载 main 方法所在的类。
  • 在main方法中,当我们调用new Thread(...).start()时:
    • JVM 会检查当前线程对象是否已经启动过(即 start() 方法是否被调用过)。如果已启动,再次调用会抛出 IllegalThreadStateException
    • 如果是首次调用 start(),JVM 会为该线程分配必要的资源(如栈空间等),并将线程注册到线程调度器中。
    • JVM 调用底层的本地方法(Native Method),将线程创建请求传递给操作系统的线程管理模块。

# 2. 操作系统层面:创建线程

  • 操作系统接收到 JVM 的线程创建请求后,会执行以下步骤:
    1. 线程结构初始化:
      • 操作系统会为新线程分配一个线程控制块(TCB, Thread Control Block),用于存储线程的状态信息(如寄存器值、栈指针、优先级等)。
      • 为新线程分配独立的栈空间(用户态栈或内核态栈,取决于线程模型)。
    2. 线程状态设置:
      • 新线程被标记为“就绪”状态,进入操作系统的线程调度队列。
    3. 线程注册到调度器:
      • 操作系统将新线程注册到线程调度器中,等待 CPU 时间片分配。

# 3. CPU 层面:线程调度与执行

  • 线程调度:

    • CPU 的调度器根据一定的调度算法(如时间片轮转、优先级调度等)选择合适的线程来执行。
    • 当新线程被调度器选中时,它会从“就绪”状态变为“运行”状态。
  • 线程执行:

    • CPU 核心加载线程的上下文信息(从 TCB 中恢复寄存器、栈指针等)。
    • CPU 开始执行线程的入口函数(对于 Java 线程来说,就是 run() 方法中的代码逻辑)。
    • 执行过程中,线程可能会因 I/O 操作、锁竞争等原因进入“阻塞”状态,或者因时间片耗尽回到“就绪”状态。
    Java 层面
       |
       v
    main() -> new Thread(...) -> start()
       |
       v
    JVM 层面
       |
       v
    调用 Native 方法 -> 操作系统线程管理
       |
       v
    操作系统层面
       |
       v
    线程初始化 -> 注册到调度器 -> 进入就绪队列
       |
       v
    CPU 层面
       |
       v
    调度器分配时间片 -> CPU 执行 run() 方法
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

# 三、线程的生命周期

# 1.生命周期

一个线程从创建,到最终的消亡,需要经历多种不同的状态,而这些不同的线程状态,由始至终也构成了线程生命周期的不同阶段。线程的生命周期可以总结为下图。

其中,几个重要的状态如下所示。

  • NEW:初始状态,线程被构建,但是还没有调用start()方法。
  • RUNNABLE:可运行状态,可运行状态可以包括:运行中状态和就绪状态。
  • BLOCKED:阻塞状态,处于这个状态的线程需要等待其他线程释放锁或者等待进入synchronized。
  • WAITING:表示等待状态,处于该状态的线程需要等待其他线程对其进行通知或中断等操作,进而进入下一个状态。
  • TIME_WAITING:超时等待状态。可以在一定的时间自行返回。
  • TERMINATED:终止状态,当前线程执行完毕。

# 2.代码示例

为了更好的理解线程的生命周期,以及生命周期中的各个状态,接下来使用代码示例来输出线程的每个状态信息。

  • WaitingTime

创建WaitingTime类,在while(true)循环中调用TimeUnit.SECONDS.sleep(long)方法来验证线程的TIMED_WARTING状态,代码如下所示。

package io.binghe.concurrent.executor.state;

import java.util.concurrent.TimeUnit;

/**
 * @author binghe
 * @version 1.0.0
 * @description 线程不断休眠
 */
public class WaitingTime implements Runnable{
    @Override
    public void run() {
        while (true){
            waitSecond(200);
        }
    }

    //线程等待多少秒
    public static final void waitSecond(long seconds){
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  • WaitingState

创建WaitingState类,此线程会在一个while(true)循环中,获取当前类Class对象的synchronized锁,也就是说,这个类无论创建多少个实例,synchronized锁都是同一个,并且线程会处于等待状态。接下来,在synchronized中使用当前类的Class对象的wait()方法,来验证线程的WAITING状态,代码如下所示。

package io.binghe.concurrent.executor.state;

/**
 * @author binghe
 * @version 1.0.0
 * @description 线程在Warting上等待
 */
public class WaitingState implements Runnable {
    @Override
    public void run() {
        while (true){
            synchronized (WaitingState.class){
                try {
                    WaitingState.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • BlockedThread

BlockedThread主要是在synchronized代码块中的while(true)循环中调用TimeUnit.SECONDS.sleep(long)方法来验证线程的BLOCKED状态。当启动两个BlockedThread线程时,首先启动的线程会处于TIMED_WAITING状态,后启动的线程会处于BLOCKED状态。代码如下所示。

package io.binghe.concurrent.executor.state;

/**
 * @author binghe
 * @version 1.0.0
 * @description 加锁后不再释放锁
 */
public class BlockedThread implements Runnable {
    @Override
    public void run() {
        synchronized (BlockedThread.class){
            while (true){
                WaitingTime.waitSecond(100);
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • ThreadState

启动各个线程,验证各个线程输出的状态,代码如下所示。

package io.binghe.concurrent.executor.state;

/**
 * @author binghe
 * @version 1.0.0
 * @description 线程的各种状态,测试线程的生命周期
 */
public class ThreadState {

    public static void main(String[] args){
        new Thread(new WaitingTime(), "WaitingTimeThread").start();
        new Thread(new WaitingState(), "WaitingStateThread").start();

        //BlockedThread-01线程会抢到锁,BlockedThread-02线程会阻塞
        new Thread(new BlockedThread(), "BlockedThread-01").start();
        new Thread(new BlockedThread(), "BlockedThread-02").start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行ThreadState类,如下所示。

可以看到,未输出任何结果信息。可以在命令行输入“jps”命令来查看运行的Java进程。

c:\>jps
21584 Jps
17828 KotlinCompileDaemon
12284 Launcher
24572
28492 ThreadState
1
2
3
4
5
6

可以看到ThreadSate进程的进程号为28492,接下来,输入“jstack 28492”来查看ThreadSate进程栈的信息,如下所示。

c:\>jstack 28492
2020-02-15 00:27:08
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.202-b08 mixed mode):

"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x000000001ca05000 nid=0x1a4 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"BlockedThread-02" #15 prio=5 os_prio=0 tid=0x000000001ca04800 nid=0x6eb0 waiting for monitor entry [0x000000001da4f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at io.binghe.concurrent.executor.state.BlockedThread.run(BlockedThread.java:28)
        - waiting to lock <0x0000000780a7e4e8> (a java.lang.Class for io.binghe.concurrent.executor.state.BlockedThread)
        at java.lang.Thread.run(Thread.java:748)

"BlockedThread-01" #14 prio=5 os_prio=0 tid=0x000000001ca01800 nid=0x6e28 waiting on condition [0x000000001d94f000]
   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 io.binghe.concurrent.executor.state.WaitingTime.waitSecond(WaitingTime.java:36)
        at io.binghe.concurrent.executor.state.BlockedThread.run(BlockedThread.java:28)
        - locked <0x0000000780a7e4e8> (a java.lang.Class for io.binghe.concurrent.executor.state.BlockedThread)
        at java.lang.Thread.run(Thread.java:748)

"WaitingStateThread" #13 prio=5 os_prio=0 tid=0x000000001ca06000 nid=0x6fe4 in Object.wait() [0x000000001d84f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000780a7b488> (a java.lang.Class for io.binghe.concurrent.executor.state.WaitingState)
        at java.lang.Object.wait(Object.java:502)
        at io.binghe.concurrent.executor.state.WaitingState.run(WaitingState.java:29)
        - locked <0x0000000780a7b488> (a java.lang.Class for io.binghe.concurrent.executor.state.WaitingState)
        at java.lang.Thread.run(Thread.java:748)

"WaitingTimeThread" #12 prio=5 os_prio=0 tid=0x000000001c9f8800 nid=0x3858 waiting on condition [0x000000001d74f000]
   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 io.binghe.concurrent.executor.state.WaitingTime.waitSecond(WaitingTime.java:36)
        at io.binghe.concurrent.executor.state.WaitingTime.run(WaitingTime.java:29)
        at java.lang.Thread.run(Thread.java:748)

"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x000000001c935000 nid=0x6864 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000000001c88c800 nid=0x6a28 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001c880000 nid=0x6498 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001c87c000 nid=0x693c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001c87b800 nid=0x5d00 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001c862000 nid=0x6034 runnable [0x000000001d04e000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        - locked <0x0000000780b2fd88> (a java.io.InputStreamReader)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        - locked <0x0000000780b2fd88> (a java.io.InputStreamReader)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001c788800 nid=0x6794 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001c7e3800 nid=0x3354 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001c771000 nid=0x6968 in Object.wait() [0x000000001cd4f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000780908ed0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
        - locked <0x0000000780908ed0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000001c770800 nid=0x6590 in Object.wait() [0x000000001cc4f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000780906bf8> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x0000000780906bf8> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=2 tid=0x000000001a979800 nid=0x5c2c runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000033b9000 nid=0x4dc0 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000033ba800 nid=0x6690 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000033bc000 nid=0x30b0 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000033be800 nid=0x6f68 runnable

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x00000000033c1000 nid=0x6478 runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x00000000033c2000 nid=0x4fe4 runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x00000000033c5000 nid=0x584 runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x00000000033c6800 nid=0x6988 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x000000001c959800 nid=0x645c waiting on condition

JNI global references: 12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

由以上输出的信息可以看出:名称为WaitingTimeThread的线程处于TIMED_WAITING状态;名称为WaitingStateThread的线程处于WAITING状态;名称为BlockedThread-01的线程处于TIMED_WAITING状态;名称为BlockedThread-02的线程处于BLOCKED状态。

注意:使用jps结合jstack命令可以分析线上生产环境的Java进程的异常信息。

也可以直接点击IDEA下图所示的图表直接打印出线程的堆栈信息。

输出的结果信息与使用“jstack 进程号”命令输出的信息基本一致。