Java并发第一篇(从零开始:一文读懂Java并发编程核心基础)

从零开始:一文读懂Java并发编程核心基础

    • 一. 为什么需要并发编程?
    • 二. 并发编程的“另一面”:挑战与代价
      • 2.1 频繁的上下文切换
      • 2.2 线程安全问题(如:死锁)
    • 三. 夯实基础:必须掌握的核心概念与操作
      • 3.1 厘清基本概念
      • 3.2 创建你的第一个线程
        • 方式一:继承 `Thread` 类
        • 方式二:实现 `Runnable` 接口 (推荐)
        • 方式三:实现 `Callable` 接口 (可获取返回值)
      • 3.3 线程的生命周期:状态转换
      • 3.4 线程间的“对话”:基本操作
        • `sleep()` 与 `wait()` 的经典对比
        • `join()` - 线程的协作
        • `interrupt()` - 优雅的通知机制
        • `yield()` - 主动的让步
      • 3.5 默默的守护者:Daemon线程

为什么需要用到并发?凡事总有好坏两面,这其中的权衡(trade-off)是什么,也就是说并发编程具有哪些缺点?以及在进行并发编程时,我们应该了解和掌握的核心概念又是什么?这篇文章将主要围绕这三个问题,为你揭开Java并发编程的神秘面纱。

一. 为什么需要并发编程?

你可能会奇怪,我们讨论的是软件编程,为什么会扯到硬件的发展?这要从著名的“摩尔定律”说起。

在很长一段时间里,摩尔定律预示着单核处理器的计算能力会呈指数级增长。然而,大约在2004年,物理极限的瓶颈开始显现,单纯提升单核频率变得异常困难。聪明的硬件工程师们转变了思路:不再追求单个计算单元的极致速度,而是将多个计算单元整合到一颗CPU中。这就是“多核CPU”时代的到来。

如今,家用的i7处理器拥有8个甚至更多的核心已是常态,服务器级别的CPU核心数则更为庞大。硬件已经铺好了路,但如何才能榨干这些核心的性能呢?

答案就是并发编程。

顶级计算机科学家Donald Ervin Knuth曾半开玩笑地评价:“在我看来,并发这种现象或多或少是由于硬件设计者无计可施了,他们将摩尔定律的责任推给了软件开发者。”

这句评价一语中的。正是多核CPU的普及,催生了并发编程的浪潮。通过并发编程,我们可以:

  • 充分利用多核CPU的计算能力:将复杂的计算任务分解,让多个核心同时工作,从而大幅提升程序性能。想象一下处理一张高清图片,如果串行处理数百万个像素点会非常耗时,但如果将图片分成几块,交由不同的核心并行处理,速度就会成倍提升。
  • 方便进行业务拆分,提升应用响应速度:在很多业务场景中,并发是天生的需求。例如,在网上购物下单时,系统需要同时完成检查库存、生成订单、扣减优惠券、通知物流等多个操作。如果这些操作串行执行,用户需要等待很长时间。而通过并发技术,这些操作可以被拆分到不同的线程中“同时”进行,极大地缩短了用户的等待时间,提升了体验。

正是这些显著的优点,使得并发编程成为现代软件开发者必须掌握的关键技能。

二. 并发编程的“另一面”:挑战与代价

既然并发编程如此强大,我们是否应该在所有场景下都使用它呢?答案显然是否定的。它是一把双刃剑,在带来性能提升的同时,也引入了新的复杂性和挑战。

2.1 频繁的上下文切换

在我们看来,多个线程似乎是“同时”执行的,但这在单核CPU上只是一种宏观上的错觉。CPU会为每个线程分配一个极短的时间片(通常是几十毫秒),然后快速地在不同线程间轮换。这个切换过程,被称为上下文切换

切换时,系统需要保存当前线程的运行状态(如程序计数器、寄存器值等),以便下次轮到它时能恢复现场。这个保存和恢复的过程本身是有性能开销的。如果线程数量过多,或者切换过于频繁,上下文切换消耗的时间甚至可能超过线程真正执行任务的时间,导致程序性能不升反降。

如何减少上下文切换?

  • 无锁并发编程:例如ConcurrentHashMap的分段锁思想,让不同线程处理不同数据段,减少锁竞争。
  • CAS算法:Java的Atomic包使用了CAS(比较并交换)这种乐观锁机制,它在很多场景下能避免加锁带来的阻塞和上下文切换。
  • 使用最少线程:创建适量的线程,避免大量线程处于空闲等待状态。
  • 使用协程:在单线程内实现多任务调度,这是更轻量级的“线程”。

2.2 线程安全问题(如:死锁)

这是并发编程中最棘手、也最容易出错的地方。当多个线程访问共享资源(也称为“临界区”)时,如果没有恰当的同步机制,就可能导致数据错乱、状态不一致,甚至出现死锁

死锁是指两个或多个线程无限期地互相等待对方释放资源,导致所有相关的线程都无法继续执行。

来看一个经典的死锁示例:

public class DeadLockDemo {private static final String resource_a = "资源A";private static final String resource_b = "资源B";public static void main(String[] args) {Thread threadA = new Thread(() -> {synchronized (resource_a) {System.out.println(Thread.currentThread().getName() + " 获得了 " + resource_a);try {// 等待一会儿,确保threadB能获得resource_bThread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 尝试获得 " + resource_b + "...");synchronized (resource_b) {System.out.println(Thread.currentThread().getName() + " 获得了 " + resource_b);}}}, "线程A");Thread threadB = new Thread(() -> {synchronized (resource_b) {System.out.println(Thread.currentThread().getName() + " 获得了 " + resource_b);System.out.println(Thread.currentThread().getName() + " 尝试获得 " + resource_a + "...");synchronized (resource_a) {System.out.println(Thread.currentThread().getName() + " 获得了 " + resource_a);}}}, "线程B");threadA.start();threadB.start();}
}

在这个例子中,线程A获得了resource_a的锁,然后尝试去获得resource_b的锁;而线程B同时获得了resource_b的锁,并尝试获得resource_a的锁。两者互相持有对方需要的锁,并等待对方释放,从而陷入了永久的等待,形成了死锁。

我们可以使用JDK自带的jpsjstack命令来诊断这种情况,jstack会明确地告诉你“Found 1 deadlock”。

如何避免死锁?

  1. 避免一个线程同时获取多个锁:尽量减少锁的持有范围和时间。
  2. 保证加锁顺序:确保所有线程都按照相同的顺序来获取锁。
  3. 使用定时锁:使用lock.tryLock(timeout),当等待超时后线程可以主动放弃,而不是无限期阻塞。
  4. 将锁和资源隔离:对于数据库锁,确保加锁和解锁在同一个数据库连接中完成。

三. 夯实基础:必须掌握的核心概念与操作

了解了并发的优缺点后,让我们深入到实践层面,看看在Java中到底该如何使用和操作线程。

3.1 厘清基本概念

  • 同步 vs 异步 (Synchronous vs Asynchronous):这通常用来描述一次方法调用。

    • 同步:调用方发起调用后,必须原地等待被调用方法执行完毕并返回结果,才能继续执行后续代码。就像你去实体店买东西,必须排队、付款、拿到商品后才能离开。
    • 异步:调用方发起调用后,不等待结果,立即返回并继续执行后续代码。被调用的方法在后台执行,完成后通过回调、通知等方式告诉调用方。就像网购,你下单后就可以去做别的事了,快递到了会通知你去取。
  • 并发 vs 并行 (Concurrency vs Parallelism)

    • 并发:指多个任务在一段时间内都得到了执行,它们在宏观上是“同时”发生的,但在微观上可能是通过时间片快速交替执行的。好比一个人在同时处理做饭、接电话、看孩子三件事,他需要不停地在任务间切换。
    • 并行:指多个任务在同一时刻真正地同时执行。这必须在多核CPU上才能实现。好比三个人,一人做饭,一人接电话,一人看孩子,他们是真正在同一时间做着不同的事。
  • 阻塞 vs 非阻塞 (Blocking vs Non-blocking):这通常用来形容线程间的相互影响。

    • 阻塞:一个线程的操作导致它自身被挂起,等待某个条件满足(如等待I/O完成、等待获取锁)。在此期间,它不会占用CPU。
    • 非阻塞:一个线程的操作不会导致自身被挂起,无论操作是否成功都会立即返回。

3.2 创建你的第一个线程

一个Java程序从main()方法启动时,JVM就已经创建了多个线程(如主线程、垃圾回收线程等)。要在我们自己的程序中创建线程,主要有以下三种方式:

方式一:继承 Thread

这是最直接的方式,通过继承Thread并重写run()方法来定义任务。

class MyThread extends Thread {@Overridepublic void run() {System.out.println("通过继承Thread类创建线程");}
}// 使用
MyThread thread = new MyThread();
thread.start(); // 必须调用start()来启动新线程
  • 优点:实现简单,易于理解。
  • 缺点:Java是单继承的,如果你的类已经继承了其他类,就无法再继承Thread,这极大地限制了其灵活性。
方式二:实现 Runnable 接口 (推荐)

这是更常用、也更受推荐的方式。它将“任务”(Runnable)和“执行任务的载体”(Thread)解耦开来。

class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("通过实现Runnable接口创建线程");}
}// 使用
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
  • 优点
    • 任务与线程解耦,结构更清晰。
    • 避免了单继承的限制,你的任务类还可以继承其他类。
    • 多个线程可以共享同一个Runnable实例,方便实现资源共享。
  • 缺点:代码比方式一稍微多一点。
方式三:实现 Callable 接口 (可获取返回值)

Runnablerun()方法没有返回值,也不能抛出受检异常。如果你的任务需要一个执行结果或可能抛出异常,Callable是更好的选择。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {Thread.sleep(2000); // 模拟耗时任务return "通过实现Callable接口返回的结果";}
}// 使用
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());System.out.println("主线程继续做其他事情...");// 在需要结果时,调用get()方法阻塞等待
String result = future.get(); 
System.out.println(result);
executor.shutdown();
  • 优点
    • 可以获得任务的返回值。
    • 可以向外抛出异常。
  • 说明Callable通常与线程池(ExecutorService)配合使用,submit()方法返回一个Future对象,你可以用它来跟踪任务状态并获取结果。

3.3 线程的生命周期:状态转换

一个Java线程在其生命周期中,会经历多种状态的变迁。这些状态定义在java.lang.Thread.State枚举中。

在这里插入图片描述

  • NEW (新建): new Thread()之后,但还未调用start()
  • RUNNABLE (可运行): 调用start()后,线程进入就绪队列,等待CPU调度。它可能正在运行,也可能在等待运行。
  • BLOCKED (阻塞): 线程等待获取一个synchronized监视器锁。
  • WAITING (无限等待): 线程调用Object.wait()Thread.join()等方法后进入此状态,需要被其他线程显式唤醒。
  • TIMED_WAITING (计时等待): 与WAITING类似,但有超时限制,时间到了会自动返回RUNNABLE状态。
  • TERMINATED (终止): run()方法执行完毕或因异常退出。

3.4 线程间的“对话”:基本操作

sleep()wait() 的经典对比

sleep()是让线程“睡一会”,而wait()是让线程“等通知”。这是面试高频题,也是理解线程协作的关键。

特性Thread.sleep(long millis)Object.wait()
所属类Thread (静态方法)Object (实例方法)
锁的释放不释放对象锁释放对象锁
使用前提任何地方都可以调用必须在synchronized代码块或方法中
唤醒方式时间到期后自动唤醒需要其他线程调用notify()notifyAll()
join() - 线程的协作

join()方法允许一个线程等待另一个线程执行完成。就像接力赛跑,你必须等前一个队友跑完把接力棒交给你,你才能开始跑。

// 在main线程中
Thread worker = new Thread(() -> {System.out.println("工作线程正在处理任务...");try { Thread.sleep(3000); } catch (InterruptedException e) {}
});
worker.start();
worker.join(); // main线程会在这里暂停,直到worker线程执行完毕
System.out.println("工作线程已结束,主线程继续执行。");
interrupt() - 优雅的通知机制

interrupt()并非强制中断线程,而是一种协作式的“打招呼”机制。它会设置目标线程的中断标志位。

  • 如果线程正在sleepwaitjoin,它会立即被唤醒并抛出InterruptedException,同时清除中断标志位
  • 如果线程正在正常运行,它需要自己通过Thread.currentThread().isInterrupted()来检查这个标志,并决定如何响应。
final Thread busyThread = new Thread(() -> {while (true) {} // 死循环,消耗CPU
}, "busyThread");busyThread.start();
busyThread.interrupt(); // 设置中断标志// 等待片刻后检查
System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted()); // 输出: true
yield() - 主动的让步

yield()是一个静态方法,它会向线程调度器暗示:当前线程愿意让出CPU,给其他同等优先级的线程一个执行机会。但这仅仅是一个建议,调度器可能会忽略它,所以它不保证当前线程一定会暂停。

3.5 默默的守护者:Daemon线程

Java中的线程分为两类:用户线程(User Thread)和守护线程(Daemon Thread)。

  • 用户线程:是我们平时创建的普通线程,执行系统的业务逻辑。
  • 守护线程:在后台运行,为其他线程(主要是用户线程)提供服务。最典型的例子就是垃圾回收(GC)线程。

当JVM中所有的用户线程都执行完毕后,无论是否还有守护线程在运行,JVM都会退出。

Thread daemonThread = new Thread(() -> {while (true) {System.out.println("我是守护线程,正在后台守护...");try {Thread.sleep(1000);} catch (InterruptedException e) {}}
});daemonThread.setDaemon(true); // 必须在start()之前设置
daemonThread.start();System.out.println("Main线程即将结束...");
// Main线程(用户线程)结束后,JVM会退出,daemonThread也会随之终止

一个重要的注意事项:守护线程在JVM退出时会被强制终止,其finally代码块不保证一定会被执行。因此,不要在守护线程的finally中执行关键的资源释放操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/89307.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/89307.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【删库跑路】一次删除pip的所有第三方库

进入命令行&#xff0c;先list看下库存pip list导出所有的第三方库至一文件列表pip freeze >requirements.txt按照列表卸载所有库pip uninstall -r requirements.txt -y再list看下&#xff0c;可见库存已清空

python 【技术面试题和HR面试题】➕列表操作、条件判断、循环、函数定义编程题

1.技术面试题 &#xff08;1&#xff09;解释Linux中的进程、线程和守护进程的概念&#xff0c;以及如何管理它们&#xff1f; 答&#xff1a; 进程 概念&#xff1a;程序运行的实例&#xff0c;有独立资源&#xff08;如内存&#xff09;&#xff0c;是系统调度的基本单位。 管…

Debian 12中利用dpkg命令安装MariaDB 11.8.2

MariaDB 11.8解决了2038问题&#xff0c;即在32位系统中将timestamp从2038-01-19 03:14:07 UTC扩展到2106-02-07 06:28:15 UTC&#xff0c;向后延长了68年。由于写此文时Debian 12的源中还没有MariaDB 11.8,采用源码编译又太费时&#xff0c;可用二进制码或dpkg安装 .下面简要记…

Go语言高并发聊天室(三):性能优化与压力测试

Go语言高并发聊天室&#xff08;三&#xff09;&#xff1a;性能优化与压力测试 &#x1f3af; 本篇目标 在前两篇文章中&#xff0c;我们完成了聊天室的基础功能。本篇将深入性能优化&#xff0c;实现真正的高并发&#xff1a; &#x1f50d; 性能瓶颈分析⚡ 关键优化技术&…

【leetcode】852. 山脉数组的封顶索引

文章目录题目题解1. 遍历2. 二分查找题目 852. 山脉数组的封顶索引 给定一个长度为 n 的整数 山脉 数组 arr &#xff0c;其中的值递增到一个 峰值元素 然后递减。 返回峰值元素的下标。 你必须设计并实现时间复杂度为 O(log(n)) 的解决方案。 示例 1&#xff1a; 输入&a…

Java期末考试准备

文章目录Java期末考试准备一、Java的输入.next()输入.nextLine()输入区别补充二、Java的输出三、类中常写方法toString()equals()其他四、容器/数组五、继承六、静态属性、方法.七、抽象类八、接口九、初始化模块十、泛型考完结束语Java学习历程注:这篇文章本来是写给同学的&am…

飞算JavaAI进阶:重塑Java开发范式的AI革命

引言&#xff1a;当代码生成进入"自动驾驶"时代 在2025年的Java开发领域&#xff0c;一场由AI驱动的革命正在重塑传统开发范式。当GitHub Copilot还在通过代码补全提升效率时&#xff0c;飞算JavaAI已实现从需求分析到完整工程代码生成的"端到端"闭环。这款…

如何在银河麒麟桌面系统中启用 sudo 密码的星号反馈

引文 我们在银河麒麟桌面操作系统上使用 sudo 命令时&#xff0c;都遇到过这样的困扰&#xff1a;输入密码时光标一动不动&#xff0c;屏幕上没有任何提示&#xff08;没有星号 *&#xff0c;也没有任何字符&#xff09;&#xff1f;就像在黑暗中摸索钥匙孔一样&#xff0c;心里…

二刷 黑马点评 秒杀优化

优化逻辑 把耗时较短的逻辑判断放入redsi中&#xff0c;比如库存是否足够以及是否一人一单&#xff0c;只要这样的逻辑完成&#xff0c;就代表一定能下单成功&#xff0c;我们就将结果返回给用户&#xff0c;然后我们再开一个线程慢慢执行队列中的信息 问题&#xff1a; 如何快…

HANA SQLScript中的变量类型汇总

在 SAP HANA SQLScript 中&#xff0c;可以使用多种变量类型&#xff0c;包括标量&#xff08;Scalar&#xff09;类型、表类型和结构化类型。以下是各种变量类型的详细说明和示例。1. 标量变量&#xff08;Scalar Variables&#xff09; 标量变量是用于存储单个值&#xff08;…

基于 Amazon Nova Sonic 和 MCP 构建语音交互 Agent

1、引言 随着人工智能技术的飞速发展&#xff0c;自然语言处理和语音交互技术正在深刻改变人机交互的方式。语音交互正从简单的“机械应答”向更自然的“类人对话”演进 。传统的语音系统通常采用模块化架构&#xff0c;将语音处理流程割裂为 ASR&#xff08;自动语音识别&…

项目的存量接口怎么低成本接入MCP?

项目的存量接口怎么低成本接入MCP&#xff1f; 老项目里的一些接口&#xff0c;如何低成本的接入MCP&#xff08;0成本不可能&#xff09;&#xff0c;变成MCP server 的tools&#xff1f; 先抛出这个问题&#xff1f;评论区的xdm如果有懂的&#xff0c;可以打在评论区&#xf…

用图片生成高保真3D模型!Hi3DGen以法线为桥,为高清三维几何生成另辟蹊径

主页&#xff1a;http://qingkeai.online/ 原文&#xff1a;用图片生成高保真3D模型&#xff01;Hi3DGen以法线为桥&#xff0c;为高清三维几何生成另辟蹊径 随着从二维图像构建高保真三维模型的需求日益增长&#xff0c;现有方法由于域间隙的限制以及 RGB 图像固有的模糊性&a…

Charles抓包工具中文安装和使用详解,快速掌握API调试与网络优化

Charles抓包工具中文安装和使用详解 在软件开发中&#xff0c;调试API请求、捕获网络流量以及优化应用性能是开发者日常工作中不可或缺的环节。Charles抓包工具作为业内领先的网络调试工具&#xff0c;以其功能强大、易用性高、支持HTTPS流量解密等特点&#xff0c;广泛应用于A…

Java :List,LinkedList,ArrayList

文章目录List常用方法List集合的遍历方式ArrayList底层的原理LinkedList底层原理常用方法List常用方法 //1.创建一个ArrayList集合对象&#xff08;有序、有索引、可以重复&#xff09; List<String> list new ArrayList<>(); list.add("蜘蛛精"); list…

LLM面试题及讲解 4

LLM面试题及讲解 4 目录 LLM面试题及讲解 4 题目讲解 一、基础概念与理论 二、模型训练与优化 三、应用与实践 四、前沿研究与趋势 大型语言模型(LLM)的核心特征是什么? LLM与传统NLP技术的本质区别是什么? Transformer架构的基本组成部分有哪些?其在LLM中为何重要? BERT…

Harmony-Next鸿蒙实战开发项目-仿小米商城App----V2

1.、简介 本项目是Harmony-Next原生开发&#xff0c;真实网络请求。采用V2等状态管理装饰器。包含&#xff08;首页、分类、发现、购物车、我的、登录、搜索&#xff0c;搜索结果&#xff0c;商品详情等&#xff09;.包含V2对接口返回数据的深度监听。 2、页面展示&#xff1…

python闭包和装饰器(超详解)

目录 一、闭包的概念 1.概念 2.闭包的特征 3.闭包的作用 二、装饰器 1.什么是装饰器 2.装饰器的作用 1.统计代码耗时 2.对代码进行权限检查 3.记录日志 3.闭包和装饰器的关系 4.注意事项&#xff1a; 一、闭包的概念 1.概念 闭包&#xff08;Closure&#xff09;指…

解决hadoop常用到的问题

1.namenode无法启动问题 报错1. ERROR: Attempting to operate on hdfs namenode as root ERROR: but there is no HDFS_NAMENODE_USER defined. 原因&#xff1a;不能用 root 用户直接启动 Hadoop 的 HDFS 组件&#xff08;NameNode / DataNode / SecondaryNameNode&#xff0…

深度学习G3周:CGAN入门(生成手势图像)

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 基础任务&#xff1a; 1.条件生成对抗网络&#xff08;CGAN&#xff09;的基本原理 2.CGAN是如何实现条件控制的 3.学习本文CGAN代码&#xff0c;并跑通代码…