tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础与面向对象

  • 高级进阶

  • 并发合集

    • Java中创建线程的几种方式
    • 并发相关概念与体系图
      • 什么情况下存在线程安全问题
        • Java中的共享变量是什么呢?
      • 同步和异步
      • 并发和并行
        • 扩展
        • 多核cpu执行并发任务
        • 上下文切换的过程是怎样的?
        • 上下文切换 跟 并发的关系:
      • 临界区
      • 阻塞和非阻塞
      • 并发的情况
        • 1. 并发请求处理
        • 2. 数据库连接池
        • 3. 缓存管理
        • 4. 文件读写
        • 5. 任务调度
        • 6. 共享队列
        • 7. 并发计算
        • 8. 会话管理
        • 9. 日志记录
        • 10. 并发计数器
        • 扩展
      • 死锁、饥饿、活锁
      • 并发级别
        • 阻塞
        • 无饥饿
        • 无障碍
        • 无锁
        • 无等待
      • 并发体系图
    • 线程状态与操作系统的用户态、内核态
    • 线程中的声明与守护线程_基础
    • 程序中的幽灵错误_基础
    • JDK并发包
    • 线程池相关
    • 并发中的安全集合
    • 生产者和消费者
    • 玩转单例模式
    • 一些工具类的原理
    • 并发包中的AQS
    • ThreadLocal与JMM
    • 锁的探究
  • JVM合集

  • 实战与细节

  • 代码之丑与提升

  • 《Java》学习笔记
  • 并发合集
EffectTang
2024-04-22
目录

并发相关概念与体系图

# 并发相关概念与体系图

# 什么情况下存在线程安全问题

多个线程同时争抢一个共享变量,并对其进行写(或删除)操作时,就会存在线程安全问题。

或者更准确一点是:线程安全问题发生需要同时满足以下 3 个条件:

  1. 多线程环境:有多个线程同时运行
  2. 共享变量存在:多个线程操作同一个共享变量
  3. 非原子性操作:至少有一个线程在修改(写)共享变量,且操作不具备原子性

# Java中的共享变量是什么呢?

共享变量是指可以被多个线程同时访问的变量,其作用域跨越了多个线程。具体包括:

  1. 实例变量(对象的成员变量,存储在堆内存)
  2. 静态变量(类变量,存储在方法区)
  3. 作为参数传递的对象引用(当对象引用作为参数传递给方法时,如果该对象在方法内外都被使用,则其内部的状态也是共享的。)

下面是第三个的代码例子:

public void updateValue(SharedData data) {
    data.value++; // 如果多个线程调用此方法并传入相同的SharedData对象,则data.value是共享变量
}
1
2
3

# 同步和异步

同步(Synchronous)和异步(Asynchronous)通常用来形容一次方法调用。

同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。

异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中“真实”地执行。整个过程,不会阻碍调用者的工作。对于调用者来说,异步调用似乎是一瞬间就完成的。如果异步调用需要返回结果,那么当这个异步调用真实完成时,则会通知调用者。

简单总结下就是,同步是所有事情在一条线上完成,而异步则是在多条线上去完成。

# 并发和并行

并发(Concurrency)和并行(Parallelism)是两个非常容易被混淆的概念。

它们都可以表示两个或者多个任务一起执行,但是偏重点有些不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。而并行是真正意义上的“同时执行”。

严格意义上来说,并行的多个任务是真实的同时执行,而对于并发来说,这个过程只是交替的,一会儿运行任务A一会儿执行任务B,系统会不停地在两者间切换。但对于外部观察者来说,即使多个任务之间是串行并发的,也会造成多任务间是并行执行的错觉。

实际上,如果系统内只有一个CPU,而使用多进程或者多线程任务,那么真实环境中这些任务不可能是真实并行的,毕竟一个CPU一次只能执行一条指令,这种情况下多进程或者多线程就是并发的,而不是并行的(操作系统会不停切换多个任务)。真实的并行也只可能出现在拥有多个CPU的系统中(比如多核CPU)。

# 扩展

在一个多核CPU系统中,一个任务通常会被分配给一个特定的CPU核心来执行。在大多数情况下,操作系统会尽量保持任务在同一个核心上执行,以减少上下文切换的开销并提高缓存命中率。然而,某些情况下,操作系统可能会将任务从一个核心迁移到另一个核心。

# 多核cpu执行并发任务

多核cpu执行并发任务(如果任务数小于cpu内核数)会发生上下文切换吗?

答案是:仍会发生上下文切换

如果出现以下这些原因,仍可能会出现上下文切换

  • 优先级变化
  • 中断操作
  • 负载均衡
  • 时间片轮转
  • 任务阻塞

# 上下文切换的过程是怎样的?

当操作系统需要在多个任务(如进程或线程)之间切换时,它会保存当前正在执行的任务的状态信息,并恢复下一个任务的状态信息。这个过程就是上下文切换。

上下文切换指的是操作系统在多任务环境中将CPU的执行权从一个任务转移到另一个任务的过程。

# 上下文切换 跟 并发的关系:

  • 上下文切换是操作系统在不同任务之间切换控制权的过程,涉及保存和恢复任务的状态。
  • 并发是指多个任务在同一时间段内交错执行的能力,通过上下文切换实现。
  • 上下文切换是实现并发的一个重要机制,但它本身也有一定的开销,需要合理管理和优化。

# 临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。

# 阻塞和非阻塞

阻塞(Blocking)和非阻塞(Non-Blocking)通常用来形容多线程间的相互影响。

比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。

非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。所有的线程都会尝试不断前向执行。

# 并发的情况

概念说的够多了,先暂停下,思考一个问题,什么样的情况下,会发生并发呢?或者说需要写多线程,需要加锁去处理呢?你要知道一些新的概念出现都是在处理一些问题时创造的,它们的诞生都是为了后来者可以更好的理解这类型的问题,并学习如何去解决他们。

在 Java 中,多线程和加锁通常用于处理需要并发执行的任务,特别是当多个线程需要访问和修改共享资源时。以下是一些常见的业务场景,这些场景下可能需要使用多线程和加锁:

# 1. 并发请求处理

场景描述:Web 应用或服务需要同时处理多个客户端请求。 解决方案:使用多线程来处理每个请求,确保服务器能够高效地响应多个客户端。 加锁需求:如果多个请求需要访问和修改共享资源(如数据库连接池、缓存等),需要使用锁来确保线程安全。

# 2. 数据库连接池

场景描述:应用程序需要频繁地从数据库读取和写入数据。 解决方案:使用数据库连接池来管理数据库连接,提高连接的复用率。 加锁需求:连接池的管理和分配需要加锁,确保同一时间只有一个线程可以获取或释放连接。

# 3. 缓存管理

场景描述:应用程序使用缓存来提高数据访问速度。 解决方案:使用多线程来处理缓存的读写操作。 加锁需求:缓存的读写操作需要加锁,确保数据的一致性和完整性。

# 4. 文件读写

场景描述:多个线程需要读写同一个文件。 解决方案:使用多线程来并行处理文件读写操作。 加锁需求:文件的读写操作需要加锁,确保数据的一致性和完整性。

# 5. 任务调度

场景描述:需要定期执行某些任务,如定时备份、数据同步等。 解决方案:使用多线程来并行执行多个任务。 加锁需求:任务的调度和状态管理需要加锁,确保任务的正确性和一致性。

# 6. 共享队列

场景描述:多个生产者线程向队列中添加数据,多个消费者线程从队列中取出数据。 解决方案:使用多线程来处理生产者和消费者的任务。 加锁需求:队列的插入和删除操作需要加锁,确保线程安全。

# 7. 并发计算

场景描述:需要进行大规模的计算任务,如科学计算、图像处理等。 解决方案:使用多线程来并行处理计算任务,提高计算效率。 加锁需求:如果计算过程中需要共享某些资源,需要加锁确保数据的一致性。

# 8. 会话管理

场景描述:Web 应用需要管理用户的会话信息。 解决方案:使用多线程来处理用户的请求和会话管理。 加锁需求:会话信息的读写操作需要加锁,确保数据的一致性和安全性。

# 9. 日志记录

场景描述:应用程序需要记录日志,多个线程可能同时写入日志文件。 解决方案:使用多线程来处理日志记录。 加锁需求:日志文件的写入操作需要加锁,确保日志的一致性和完整性。

# 10. 并发计数器

场景描述:需要统计某个事件的发生次数,如网站的访问次数。 解决方案:使用多线程来处理事件的统计。 加锁需求:计数器的增减操作需要加锁,确保计数的准确性。

# 扩展

单例 Bean (singleton)在 Spring Boot 中默认是单实例的,但这并不意味着它只能处理一个请求。实际上,单例 Bean 可以同时处理多个请求,这是因为在多线程环境中,多个线程可以同时访问同一个 Bean 实例。这就引出了线程安全的问题。

在Springboot框架中,它的bean默认是单实例,假如将其改为多实例,这样后即使在代码中加锁,比如加了重入锁,也可能会有线程安全的问题。

以下是简要解析:

  1. 多实例 Bean (prototype):
    • 每次请求都会创建一个新的 Bean 实例。
    • 每个实例都有自己的状态,因此在实例内部的状态通常是线程安全的。
    • 但是,如果多个实例访问和修改同一个共享资源(如静态变量、数据库连接池、缓存等),仍然需要考虑线程安全问题。
  2. 共享资源:
    • 如果多个实例访问和修改同一个共享资源,即使在每个实例内部加锁,也不能保证线程安全。
    • 例如,如果有两个 prototype 实例同时访问和修改同一个静态变量,仍然会有竞态条件。

# 死锁、饥饿、活锁

死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)都属于多线程的活跃性问题。如果发现上述几种情况,那么相关线程可能就不再活跃,也就说它可能很难再继续往下执行了。

死锁就是线程彼此都需要对方掌握的部分资源才能继续往下执行,但互相又都不想让,于是所有线程都无法执行。

饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。比如它的线程优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。在自然界中,母鸟喂食雏鸟时,很容易出现这种情况。由于雏鸟很多,食物可能有限,雏鸟之间的食物竞争可能非常厉害,小雏鸟因为经常抢不到食物,有可能会被饿死。线程的饥饿也非常类似这种情况。另外一种可能是,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。与死锁相比,饥饿还是有可能在未来一段时间内解决的(比如高优先级的线程已经完成任务,不再疯狂的执行)。

活锁就是在多线程的执行过程中都秉承着“谦让”的原则,主动将资源释放给他人使用,那么就会出现资源不断在两个线程中跳动,而没有一个线程可以同时拿到所有资源而正常执行。

# 并发级别

由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,我们可以把并发的级别进行分类,大致上可以分为阻塞、无饥饿、无障碍、无锁、无等待几种。

# 阻塞

一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用synchronized关键字,或者重入锁时,我们得到的就是阻塞的线程。

在这种级别中,线程在试图获取某个被其他线程持有的锁时会被阻塞,即挂起等待,直到锁被释放。Java中的synchronized关键字、ReentrantLock等都是实现阻塞同步的经典工具。阻塞是传统并发控制中最常见的方式,它采用悲观策略,假设资源竞争激烈,通过锁定机制防止数据冲突。阻塞可能导致线程上下文切换频繁,影响性能,但在许多场景下,其简单直观的同步逻辑易于理解和实现。

# 无饥饿

如果线程之间是有优先级的,那么线程调度的时候总是会倾向于满足高优先级的线程。也就说是,对于同一个资源的分配,是不公平的。对于非公平的锁来说,系统允许高优先级的线程插队。这样有可能导致低优先级线程产生饥饿。但如果锁是公平的,满足先来后到,那么饥饿就不会产生,不管新来的线程优先级多高,要想获得资源,就必须乖乖排队。那么所有的线程都有机会执行。

无饥饿保证即使在高并发环境下,每个线程只要等待足够长的时间,最终都能够获得所需的资源并继续执行,避免了因优先级反转或其他原因导致的永久等待(饥饿)现象。无饥饿通常需要同步机制提供某种形式的公平性保证,如ReentrantLock可以通过构造函数指定为公平锁,确保线程按照申请锁的顺序获得锁。

# 无障碍

无障碍级别的并发控制允许线程在没有其他线程干扰的情况下(即在某个瞬间没有其他活跃线程对共享数据进行修改)执行操作并成功完成。这意味着一个线程的操作不会因为其他线程的存在而永远无法完成,但可能存在暂时的延迟。无障碍并发并不保证线程在任何时刻都能立即取得进展,只是保证在没有竞争时能够成功执行。

# 无锁

无锁并发是指在设计数据结构或算法时,避免使用传统的互斥锁来同步线程,而是通过原子操作(如CAS,Compare-and-Swap)来保证数据的一致性。在这种级别下,线程在执行操作时不会被阻塞,而是通过循环重试等方式来应对竞争,一旦没有其他线程干扰,它们就可以立即完成操作。无锁编程可以减少上下文切换,提高并发性能,但编写正确的无锁代码通常较为复杂,需要对硬件指令和并发编程有深入理解。

# 无等待

无等待是最高等级的并发,它要求无论系统中有多少线程以及它们如何交错执行,每个线程都能够保证在有限步数内完成其操作。这意味着所有线程都能持续向前推进,不会因竞争而无限期停滞。无等待并发提供了最强的并发保证,但实现难度大,对算法设计和数据结构的要求极高,且可能牺牲一定的空间效率和局部性。

总结来说,Java编程中的并发级别从低到高依次为:阻塞、无饥饿、无障碍、无锁、无等待。

# 并发体系图

随着接触和负责的系统越来越复杂,我逐渐发现,无论是对于优秀的系统设计,还是对于程序员的成长提高、职业发展,并发编程都是必须要跨过去的“坎”,而一旦你跨过了这道“坎”,便会豁然开朗,原来一切都如此简单,职业发展也会更上一层楼。

  • 并发知识体系
    • 线程基础
    • 为了线程安全
    • 管理线程、提高效率
    • 线程配合
      • CountDownLatch
    • 底层原理
      • Java内存模型
      • CAS原理
      • AQS框架
#并发
上次更新: 2025/04/23, 16:23:16
Java中创建线程的几种方式
线程状态与操作系统的用户态、内核态

← Java中创建线程的几种方式 线程状态与操作系统的用户态、内核态→

最近更新
01
面向切面跟自定义注解的结合
05-22
02
时间跟其他数据的序列化
05-19
03
数据加密与安全
05-17
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式