张三并发编程实践:掌握多线程技巧,打造高性能应用!

embedded/2024/10/18 13:32:37/

请在此添加图片描述

_线程(Thread)_是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以有多个线程,它们共享进程的资源,如内存空间、文件句柄等。线程相较于进程,具有更小的资源开销,创建和切换线程的速度也更快。

线程的故事

有一天,一个程序员在开发一个应用程序,这个应用程序需要处理大量的任务。程序员决定使用多线程来提高程序的执行效率。

于是,程序员创建了一个线程池,线程池中有很多线程。当有新任务到来时,线程池中的一个空闲线程会被分配任务去执行。在执行过程中,线程可能会遇到一些阻塞操作,如等待文件读写、等待网络请求等。此时,线程会进入阻塞状态,线程池会将这个线程置换出去,让其他线程继续执行任务。当阻塞操作完成后,线程会重新回到线程池中,等待分配新任务。

有一次,线程池中的线程都在忙碌,但应用程序仍然有大量任务需要处理。这时,线程池决定扩容,增加更多的线程。新来的线程迅速地接管了任务,提高了程序的处理能力。

另一次,线程池中的线程数量过多,导致系统资源紧张。这时,线程池决定缩容,减少一些线程。被减少的线程会完成当前任务后,自动退出。

通过这个故事,我们可以了解到线程的基本概念和作用。在实际开发中,我们需要根据应用程序的需求和系统资源情况,合理地使用多线程来提高程序的执行效率。

请在此添加图片描述

线程的状态

线程在执行过程中会经历不同的状态:

新建(New):

当用new关键字创建一个线程对象时,线程处于新建状态。此时,线程对象已经创建,但线程还没有开始执行。

Thread thread = new Thread();

就绪(Runnable):

当线程对象调用了start()方法后,线程进入可运行状态。此时,线程已经准备好执行,等待系统分配资源。

thread.start();

运行(Running):

当线程获得系统资源后,线程开始执行run()方法中的代码,此时线程处于运行状态。

public void run() {// 线程执行的代码
}

阻塞(Blocked):

线程在运行过程中,可能会因为某些原因暂时无法继续执行,如等待 I/O 操作完成、等待获取锁等。此时,线程进入阻塞状态。

synchronized (lock) {// 等待获取锁
}

阻塞的三种分类:

等待阻塞(Waiting for I/O):

线程在等待 I/O 操作完成,如等待文件读写、网络请求等。此时,线程会被挂起,不占用 CPU 资源。当 I/O 操作完成后,线程会重新进入可运行状态。

FileInputStream fis = new FileInputStream("file.txt");
int data = fis.read(); // 等待文件读取完成

同步阻塞(Synchronization Blocked):

线程在等待获取锁。当一个线程试图访问被synchronized关键字修饰的同步代码块时,它需要获取锁。如果锁已经被其他线程持有,当前线程会被阻塞,等待锁被释放。

synchronized (lock) {// 等待获取锁
}

其他阻塞(Other Blocked):

线程在等待某些系统资源,如等待操作系统分配内存、等待线程调度等。此时,线程会被挂起,不占用 CPU 资源。当系统资源可用时,线程会重新进入可运行状态。

Thread.sleep(1000); // 等待1秒

了解这三种阻塞情况有助于我们更好地理解多线程编程中的问题和解决方案。在实际开发中,我们需要根据具体需求合理地控制线程状态,以实现高效的并发编程。同时,要注意避免死锁、资源竞争等问题。

等待(Waiting):

线程在运行过程中,主动调用了wait()join()park()方法,暂时放弃 CPU 资源,进入等待状态。

lock.wait();

超时等待(Timed Waiting):

线程在等待状态的基础上,设置了等待超时时间。

lock.wait(timeout);

终止(Terminated):

线程执行完run()方法中的代码,或者因为异常而终止,线程进入终止状态。

线程状态之间的转换关系如下:

  • 新建 -> 可运行:调用start()方法
  • 可运行 -> 运行:获得系统资源
  • 运行 -> 阻塞:等待 I/O 操作完成、等待获取锁等
  • 运行 -> 等待:调用wait()join()park()方法
  • 运行 -> 超时等待:调用带有超时参数的wait()方法
  • 阻塞、等待、超时等待 -> 运行:获得锁、I/O 操作完成、超时等待结束等
  • 运行 -> 终止:执行完run()方法或发生异常

了解线程的状态有助于我们更好地理解多线程编程中的问题和解决方案。在实际开发中,我们需要根据具体需求合理地控制线程状态,以实现高效的并发编程。

线程调整优先级

在 Java 中,我们可以通过调整线程的优先级来影响线程调度。线程优先级是一个整数值,范围在 1(最低优先级)到 10(最高优先级)之间。默认情况下,新创建的线程优先级与其父线程相同。这些优先级常量分别由 Thread 类中的 MAX_PRIORITYNORM_PRIORITYMIN_PRIORITY 定义。

_Thread.MAX_PRIORITY__(10):_表示线程的最高优先级。当一个线程的优先级设置为最高优先级时,它具有更高的概率被调度执行。然而,这并不意味着最高优先级的线程总是优先执行。线程调度仍然取决于操作系统和 JVM 的实现。

_Thread.NORM_PRIORITY__(5):_表示线程的默认优先级。当创建一个新线程时,如果没有显式设置优先级,那么它将继承父线程的优先级。默认优先级适用于大多数线程,它不会导致线程饥饿,也不会导致过分的线程切换开销。

_Thread.MIN_PRIORITY__(1):_表示线程的最低优先级。当一个线程的优先级设置为最低优先级时,它具有较低的概率被调度执行。这可以用于确保低优先级线程不会影响到高优先级线程的执行。

Thread thread = new Thread(() -> {// 线程执行的代码
});// 设置线程优先级
thread.setPriority(Thread.MAX_PRIORITY); // 设置为最高优先级
thread.setPriority(Thread.NORM_PRIORITY); // 设置为默认优先级
thread.setPriority(Thread.MIN_PRIORITY); // 设置为最低优先级

需要注意的是,线程优先级并不能保证线程按照预期的顺序执行。线程调度仍然取决于操作系统和 JVM 的实现。此外,过分依赖线程优先级可能导致程序难以维护和理解。在实际开发中,我们应该根据应用程序的需求和系统资源情况,合理地设置线程优先级,以实现高效的并发编程。同时,要注意避免死锁、资源竞争等问题。

线程调度策略

线程调度是操作系统用来决定哪个线程应该获得处理器资源的过程。线程调度策略会影响程序的执行效率和响应时间。

常见的线程调度策略:

  1. **协同式多线程(Cooperative Multithreading):**协同式多线程是一种非抢占式的线程调度策略。在这种策略中,线程需要主动地让出处理器资源,以便其他线程可以执行。这种调度策略的优点是实现简单,但缺点是可能导致线程饥饿(一个线程长时间得不到执行)。
  2. **抢占式多线程(Preemptive Multithreading):**抢占式多线程是一种抢占式的线程调度策略。在这种策略中,操作系统可以在任何时候暂停一个正在执行的线程,将处理器资源分配给其他线程。这种调度策略可以避免线程饥饿,但实现相对复杂。
  3. **优先级调度(Priority Scheduling):**优先级调度是一种基于线程优先级的调度策略。线程可以被分配一个优先级,优先级较高的线程更有可能获得处理器资源。优先级调度可以确保重要的线程优先执行,但可能导致低优先级线程饥饿。
  4. **时间片轮转调度(Round-Robin Scheduling):**时间片轮转调度是一种将处理器资源分配给线程的公平策略。每个线程都有一个时间片,当时间片用完时,线程会被挂起,让其他线程执行。这种调度策略可以确保每个线程都有机会执行,但可能导致线程切换频繁,增加上下文切换开销。
  5. **最高响应比优先调度(Highest Response Ratio Next, HRRN):**最高响应比优先调度是一种既考虑线程等待时间又考虑线程优先级的调度策略。线程的响应比定义为(等待时间 + 服务时间)/ 服务时间,响应比较高的线程更有可能获得处理器资源。这种调度策略可以在保证公平性的同时,尽量减少线程的等待时间。

线程的基本方法

🐢start()

启动线程。这个方法会调用线程的 run() 方法,使线程开始执行。

Thread thread = new Thread(() -> {// 线程执行的代码
});
thread.start(); // 启动线程

🐢run()

线程执行的代码。这个方法需要在实现 Runnable 接口的类中重写,或者在继承 Thread 类的子类中重写。

class MyRunnable implements Runnable {@Overridepublic void run() {// 线程执行的代码}
}Thread thread = new Thread(new MyRunnable());
thread.start();

或者:

class MyThread extends Thread {@Overridepublic void run() {// 线程执行的代码}
}MyThread thread = new MyThread();
thread.start();

🐢join()

等待线程执行完成。这个方法会阻塞当前线程,直到被调用的线程执行完成。

Thread thread = new Thread(() -> {// 线程执行的代码
});
thread.start();
thread.join(); // 等待线程执行完成

🐢sleep(long millis)

使当前线程暂停执行指定的时间(以毫秒为单位)。这个方法需要在 try 块中调用,因为它可能抛出 InterruptedException 异常。

try {Thread.sleep(1000); // 使当前线程暂停1秒
} catch (InterruptedException e) {e.printStackTrace();
}

🐢interrupt()

中断线程。这个方法会设置线程的中断标志,线程可以通过检查 isInterrupted() 方法来响应中断。

Thread thread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {// 线程执行的代码}
});
thread.start();
thread.interrupt(); // 中断线程

🐢isAlive()

检查线程是否仍在运行。如果线程已经启动并且尚未终止,则返回 true

Thread thread = new Thread(() -> {// 线程执行的代码
});
thread.start();
boolean isAlive = thread.isAlive(); // 检查线程是否仍在运行

🐢getId()

获取线程的唯一标识符。

Thread thread = new Thread(() -> {// 线程执行的代码
});
thread.start();
long threadId = thread.getId(); // 获取线程的唯一标识符

🐢getName()

setName(String name)获取和设置线程的名称。

Thread thread = new Thread(() -> {// 线程执行的代码
});
thread.setName("MyThread"); // 设置线程名称
String threadName = thread.getName(); // 获取线程名称

🐢getPriority()

setPriority(int priority):获取和设置线程的优先级。

Thread thread = new Thread(() -> {// 线程执行的代码
});
thread.setPriority(Thread.MAX_PRIORITY); // 设置线程优先级
int threadPriority = thread.getPriority(); // 获取线程优先级

🐢currentThread()

获取当前正在执行的线程。

Thread currentThread = Thread.currentThread(); // 获取当前正在执行的线程

🐢yield()

当线程调用 yield() 方法时,它会主动放弃当前的 CPU 时间片,让自己从运行状态(RUNNING)变为就绪状态(RUNNABLE)。这样,其他具有相同优先级的线程就有机会获得 CPU 时间片并执行。

需要注意的是,__yield() 方法并不保证一定会使当前线程立即停止执行。线程调度器可以自由决定是否立即调度其他线程。实际上,__yield() 方法的效果往往取决于具体的操作系统和 JVM 实现。在某些情况下,调用 yield() 可能只是让当前线程稍作休息,然后很快再次获得 CPU 时间片。

yield() 方法的使用场景相对较少,通常不推荐过度依赖它来控制线程的执行顺序。更好的做法是使用其他同步工具,如 wait()notify()SemaphoreLock 等,来实现线程间的协调和控制。

t1 线程在循环到 2 时调用 yield(),可能会让出 CPU 给 t2 线程执行。但是,这并不是绝对的,取决于线程调度器的决策。

public class YieldExample {public static void main(String[] args) {Thread t1 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("Thread t1: " + i);if (i == 2) {Thread.yield(); // 放弃CPU时间片}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("Thread t2: " + i);}});t1.start();t2.start();}
}

线程类型

用户线程(User Thread)

用户线程是由程序员创建和管理的线程。它们通常用于执行特定任务,如处理用户输入、执行计算任务等。用户线程的创建和管理完全由程序员控制。

Thread userThread = new Thread(() -> {// 线程执行的代码
});
userThread.start();

守护线程(Daemon Thread)

守护线程是一种特殊类型的线程,其主要作用是为其他线程提供服务。守护线程在后台运行,不会阻止 JVM 的正常终止。当所有非守护线程(即用户线程)都结束时,守护线程会自动终止。守护线程通常用于执行后台任务,如垃圾回收、内存管理等。

Thread daemonThread = new Thread(() -> {// 线程执行的代码
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();

主线程(Main Thread)

主线程是 Java 程序的入口点。当 Java 程序启动时,JVM 会创建一个主线程来执行 main 方法。主线程通常负责启动其他线程、初始化程序等任务。当主线程结束时,JVM 会等待所有非守护线程都结束后才会终止。

public class MainThreadExample {public static void main(String[] args) {// 主线程执行的代码}
}

线程池线程(Thread Pool Thread)

线程池线程是由线程池管理的线程。线程池是一种用于管理和复用线程的机制,它可以提高系统性能和资源利用率。线程池线程在线程池中被创建和管理,当有任务需要执行时,线程池会分配一个空闲的线程池线程来执行任务。任务完成后,线程池线程会返回线程池,等待下一个任务。

ExecutorService threadPool = Executors.newFixedThreadPool(5);
threadPool.execute(() -> {// 线程执行的代码
});
threadPool.shutdown();

定时器线程(Timer Thread)

定时器线程是用于执行定时任务的线程。Java 提供了 java.util.Timer 类来实现定时任务。定时器线程会在指定的时间间隔内执行任务,或者在指定的时间点执行任务。

Timer timer = new Timer();
timer.schedule(new TimerTask() {@Overridepublic void run() {// 线程执行的代码}
}, 1000); // 延迟1秒后执行任务

写在最后

并发编程是一种编程范式,它允许多个任务在同一时间段内独立运行。在Java中,并发编程主要关注如何在多个线程之间有效地共享资源和协调操作,以实现高性能和响应能力。

在现代软件开发中,随着硬件技术的发展,多核处理器已经成为主流。为了充分利用多核处理器的性能,我们需要编写并发程序。并发编程不仅可以提高程序的性能,还可以提高程序的响应能力和资源利用率。这对于提高用户体验和系统吞吐量具有重要意义。

我们需要根据具体需求和场景选择合适的并发编程技巧和工具。通过深入学习和实践并发编程,我们可以更好地理解Java并发编程的原理和应用,从而在实际项目中实现高性能、可扩展的应用程序。在未来的工作中,我将继续关注并发编程的最新动态和技术,以便为项目和团队提供更好的支持和指导。


http://www.ppmy.cn/embedded/128461.html

相关文章

PhpStudy的安装及使用教程----适合入门小白

一&#xff1a;简介 phpStudy是一个PHP调试环境的程序集成包。 该程序包集成最新的ApachePHPMySQLphpMyAdminZendOptimizer&#xff0c;一次性安装&#xff0c;无须配置即可使用&#xff0c;是非常方便、好用的PHP调试环境。 该程序不仅包括PHP调试环境&#xff0c;还包括了…

LLM实践--支线:拯救Continue Pretrain的数据

背景 首先介绍下什么是Continue Pretrain&#xff08;CP&#xff09;。CP 和 Pretrain、SFT一样指的是 LLM 训练的一个阶段&#xff0c;在前大模型时代还被称作Post Pretrain。CP 是在Pretrain和SFT之间的训练阶段&#xff0c;目的是为模型注入领域知识&#xff0c;这个领域是…

2024年全球增强现实(AR)市场分析报告

一、增强现实统计数据(2024) 市场价值:2024年,全球AR市场价值超过320亿美元,并预计到2027年将突破500亿美元。用户基础:目前约有14亿活跃的AR用户设备,这一数字预计将在2024年增长至17.3亿。消费者认知:大约四分之三的44岁以下成年人对AR有所了解。购物体验:基于AR的购物…

色选机用电磁阀分类

一、概述 目前合肥地区已经形成了色选机产业集群&#xff0c;从整机制造到各种色选机用零部件产业都已经十分完善&#xff0c;作为色选机的核心部件&#xff0c;高速电磁阀目前没有一个很官方的行业标准&#xff0c;目前种类多&#xff0c;外形各异。下面结合自己的实际使用经验…

[NewStar 2024] week2

Crypto 第2周的密码依然都是签到题 这是几次方&#xff1f; 疑惑&#xff01; 给了个提示&#xff1a;hint p^e 10086 这里边不要当成乘幂&#xff0c;而且加法的优先级高于异或&#xff0c;所以phint ^ (e10086)得到p就能正常解RSA了 Since you konw something 给的c是…

C++——stack和queue

目录 前言 一、接口 二、 模拟实现 三、deque双端队列&#xff08;了解&#xff09; 1.大致思路 2.迭代器 3.缺陷 4.为什么选择deque作为queue和stack的默认容器 前言 不管是stack还是queue&#xff0c;我们都可以通过调用vector来对其进行模拟实现&#xff0c;实际上库…

获取非加密邮件协议中的用户名和密码——安全风险演示

获取非加密邮件协议中的用户名和密码——安全风险演示 引言 在当今的数字时代,网络安全变得越来越重要。本文将演示如何通过抓包工具获取非加密邮件协议中的用户名和密码,以此说明使用非加密协议的潜在安全风险。 注意: 本文仅用于教育目的,旨在提高安全意识。未经授权访问他…

5种边界填充

目录 边界填充需要知道的两个东西什么算边界边界的范围是多少举例 复制填充反射法反射101法外包装法数值填充法原图代码最终效果 边界填充需要知道的两个东西 什么算边界 顾名思义&#xff1a;就是图片的最外边 边界的范围是多少 根据你自己的需要而设置 举例 这里我选择…