JUC-并发

本文详细介绍了Java并发编程的基础知识,包括并发编程的原因、存在的问题、线程安全、并发与并行的区别,以及synchronized关键字的原理和优化。讨论了线程的创建、生命周期、状态以及线程间通信,解释了wait()和sleep()的区别。深入讲解了锁的机制,如死锁的条件、Lock接口的实现类、读写锁、乐观锁和悲观锁。还涵盖了线程池的使用、线程池的配置和工作原理。最后探讨了并发工具类如CountDownLatch、CyclicBarrier、Semaphore和Exchanger的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、多线程/并发基础

1、为什么要并发编程/使用多线程

  • 充分利用多核CPU(充分利用计算资源)
  • 先从总体上来说:从计算机底层来说: 线程是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
  • 再深入到计算机底层来探讨:单核时代:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。

2、单核CPU支持Java多线程

  • 操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。
  • 操作系统主要通过两种线程调度方式来管理多线程的执行:抢占式调度(Preemptive Scheduling):操作系统决定何时暂停当前正在运行的线程,并切换到另一个线程执行。这种切换通常是由系统时钟中断(时间片轮转)或其他高优先级事件(如 I/O 操作完成)触发的。这种方式存在上下文切换开销,但公平性和 CPU 资源利用率较好,不易阻塞。协同式调度(Cooperative Scheduling):线程执行完毕后,主动通知系统切换到另一个线程。这种方式可以减少上下文切换带来的性能开销,但公平性较差,容易阻塞。
  • Java 使用的线程调度是抢占式的。也就是说,JVM 本身不负责线程的调度,而是将线程的调度委托给操作系统。操作系统通常会基于线程优先级和时间片来调度线程的执行,高优先级的线程通常获得 CPU 时间片的机会更多

3、并发编程存在的问题

  • 上下文切换:CPU通过时间片分配来执行任务,当前任务执行一个时间片后会切换到下一个任务。在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
    • 线程创建和上下文切换会带来额外的开销
  • 内存泄露
  • 线程安全问题
    • 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
    • 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。
  • 死锁:线程A持有资源,线程B持有资源;都想申请对方的资源,就会相互等待而进入死锁。(互相等待对方释放锁)
  • 资源限制

4、线程安全/并发编程三要素/如何保证多线程的运行安全

  • 原子性:一个或多个操作要么同时成功要么同时失败(原子类、synchronized、lock)
  • 可见性:一个线程对共享变量修改,另一个线程能够立即看到(volatile、synchronized、Lock)
  • 有序性:程序执行顺序按照代码的先后顺序执行(volatile、Happens-before)(处理器可能指令重排)

5、并发和并行的区别

  • 并发:多个线程操作同一资源,CPU快速切换(多个作业在同一 时间段 内执行)
  • 并行:多个处理器同时处理多个任务(多个作业在同一 时刻 内执行)

6、同步和异步的区别

  • 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
  • 异步:调用在发出之后,不用等待返回结果,该调用直接返回。

7、伪共享

  • 背景:
    • 为了提高CPU的利用率,平衡CPU和内存之间的速度差异,在CPU里面设计了三级缓存。CPU在向内存发起IO操作的时候,一次性会读取64个字节的数据作为一个缓存行,缓存到CPU的高速缓存里面。在Java 中一个long类型是8个字节,意味着一个缓存行可以存储8个long 类型的变量。缓存行的设计对于CPU 来说,可以有效的减少和内存的交互次数,从而避免了CPU的IO等待,以提升CPU的利用率。
    • 是什么:多个线程修改同一个缓存行里面的多个独立变量的时候,基于缓存一致性协议,就会无意中影响了彼此的性能
    • 解决方案:对齐填充

二、线程

1、线程和进程

  • 进程:操作系统分配资源的基本单位。进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在 Java 中,启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
  • 线程:执行具体的任务和功能,是CPU调度和分配的最小单位。一个进程在执行的过程中可以产生多个线程。同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
  • 内存分配:同一进程中线程共享本进程的地址空间和资源,进程之间地址空间和资源相互独立
  • 二者的关系:一个进程中可以有多个线程,多个线程共享进程的方法区的资源,但是每个线程有自己的程序计数器虚拟机栈本地方法栈线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

2、java线程与操作系统的线程比较

  •  Java 线程的本质其实就是操作系统的线程(一对一)
  • Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。

3、java中创建线程的方式(单继承、多实现)

  • 继承Thread类,重写run()
  • 实现Runnable接口,重写run(),无返回值
  • 实现Callable接口,重写call(),有返回值
  • 线程池,execute();submit(),返回Future
  • 使用CompletableFuture
  • 使用ForkJoin线程池或Stream并行流
  • 总:Java创建线程有上述等方式。严格来说,Java只有通过new Thread().start()创建线程这一种方式。其他方式仅仅只是创建线程体-多线程任务,并不属于真正的Java线程,它们的执行,最终还是需要依赖于使用new Thread().start()。

4、线程的生命周期

  • 新建、就绪、运行、阻塞、死亡

5、线程的状态

  • NEW、Runnable、Blocked、WAITING、TIME_WAITING、TERMINATED
  • BLOCKED 是锁竞争失败后被被动触发的状态,WAITING 是人为的主动触发的状态;
  • BLCKED 的唤醒时自动触发的,而WAITING 状态是必须要通过特定的方法来主动唤醒;

6、线程上下文切换

  • 线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。
  • 上下切换场景:
    • 主动让出 CPU,比如调用了 sleep(), wait() 等。
    • 时间片用完,操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
    • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。

7、Object.wait()和Thread.sleep()的区别

  • 共同点:暂停线程的执行
  • 来自不同类:wait()来自Object,sleep()来自Thread
  • 锁的范围不同:wait()必须在同步代码块中,sleep()哪里都可以睡
  • 锁的释放不同:wait()会释放锁,sleep()会持有锁
  • 是否捕获异常:wait()不用捕获异常,sleep()必须捕获异常
  • 场景:wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • CPU:sleep()只让出CPU而不会释放同步资源,wait()会暂时退出同步资源锁
  • 为什么wait不在Thread中:wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。为什么 sleep() 方法定义在 Thread 中:因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

8、run()和start()的区别

  • 调用start()启动线程,轮到该线程会自动调用run(),一个线程只能调用一次start()方法,第二次就会抛出异常。
  • 不能调用两次start():有线程状态检查
  • 为什么start()调用run方法
    • new 一个Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行run() 方法的内容,这是真正的多线程工作。
    • 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
    • 总结:调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,在主线程里执行。

9、守护线程和用户线程的区别

  • 用户 (User) 线程:运行在前台,执行具体的任务
  • 守护 (Daemon) 线程:运行在后台,为用户线程服务。当一个Java虚拟机中所有用户线程结束时,Java虚拟机将会退出,所有Daemon线程都需要立即终止。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。Daemon 属性需要在启动线程之前设 置。在Java虚拟机退出时Daemon线程中的finally块并不一定会执行。

10、线程间通信

  • 共享变量:线程可以访问和修改共享变量的值(volatile、synchronized)
  • 等待/通知机制
    • Object的wait()、notify()/notifyAll()—需先对对象加synchronized
      • 为什么在Object类中
        • Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程
      • 为什么必须在同步方法或者同步块中被调用
        • 当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。
    • Condition接口:await()、signal()—需加Lock锁—精确通知和唤醒
    • LockSupport工具类的静态方法park()、unpark()

11、wait()如何使用

  • wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。
    	synchronized (monitor) {
    		// 判断条件谓词是否得到满足
    		while(!locked) {
    			// 等待唤醒
    			monitor.wait();
    		}
    		// 处理其他的业务逻辑
    	}
    

12、如何停止一个正在运行的线程

  1. run()执行完
  2. stop():可能不释放资源
  3. 使用interrupt方法中断线程

13、进程切换比线程切换慢

  • 每个进程拥有独立的虚拟地址空间,进程切换涉及虚拟地址空间的切换,导致内存中的页表也需要进行切换。

14、Thread 和Runnable 的区别

  • Thread 是一个类,Runnable 是接口,接口可以支持多继承,而类只能单继承。如果继承了其他类,又要实现线程,只能实现Runnable接口
  • Runnable 表示一个线程的顶级接口,Thread 类也是实现了Runnable 接口。
  • 以面向对象的思想来说,Runnable 相当于一个任务,而Thread 才是真正处理的线程,所以我们只需要用Runnable 去定义一个具体的任务,然后交给Thread去处理就可以了,这样达到了松耦合的设计目的。
  • Runnable 接口定义了线程执行任务的标准方法run
  • 总结:实际应用中,建议实现Runnable 接口实现线程的任务定义,然后使用Thread 的start 方法去启动线程并执行Runnable 这个任务。

15、fail-safe 机制与fail-fast 机制分别有什么作用

  • 是什么:是多线程并发操作集合时的一种失败处理机制。
  • fail-fast:表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException 异常,从而导致遍历失败
    • java.util 包下的集合类都是快速失败机制的, 常见使用fail-fast 方式遍历的容器有HashMap 和ArrayList 等
  • fail-safe:失败安全,若出现集合元素的修改,不会抛出ConcurrentModificationException。原因是采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到。比如CopyOnWriteArrayList,在对这个集合遍历过程中,对集合元素做修改后,不会抛出异常,但同时也不会打印出增加的元素。
    • java.util.concurrent 包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。

16、虚拟线程

  • 虚拟线程在 Java 21 正式发布,这是一项重量级的更新
  • 什么是虚拟线程:是 JDK实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量
  • 虚拟线程和平台线程:在引入虚拟线程之前,java.lang.Thread 包已经支持所谓的平台线程(Platform Thread),也就是没有虚拟线程之前,我们一直使用的线程。JVM 调度程序通过平台线程(载体线程)来管理虚拟线程,一个平台线程可以在不同的时间执行不同的虚拟线程(多个虚拟线程挂载在一个平台线程上),当虚拟线程被阻塞或等待时,平台线程可以切换到执行另一个虚拟线程。在密集IO常见,虚拟现场效率更高

  • 优缺点:非常轻量级:可以在单个线程中创建成百上千个虚拟线程而不会导致过多的线程创建和上下文切换。简化异步编程: 虚拟线程可以简化异步编程,使代码更易于理解和维护。它可以将异步代码编写得更像同步代码,避免了回调地狱(Callback Hell)。减少资源开销: 由于虚拟线程是由 JVM 实现的,它能够更高效地利用底层资源,例如 CPU 和内存。虚拟线程的上下文切换比平台线程更轻量,因此能够更好地支持高并发场景。缺点:不适用于计算密集型任务: 虚拟线程适用于 I/O 密集型任务,但不适用于计算密集型任务,因为密集型计算始终需要 CPU 资源作为支持。与某些第三方库不兼容: 虽然虚拟线程设计时考虑了与现有代码的兼容性,但某些依赖平台线程特性的第三方库可能不完全兼容虚拟线程。

三、JMM

1、CPU缓存模型

  •  CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题;CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。

  •  现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache。CPU Cache 的工作方式: 先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在内存缓存不一致性的问题 !CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议(比如MESI协议)或者其他手段来解决。 这个缓存一致性协议指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范。不同的 CPU 中,使用的缓存一致性协议通常也会有所不同。操作系统通过内存模型(Memory Model)定义一系列规范来解决内存缓存不一致性问题

2、指令重排

  • 为了提升执行速度/性能,计算机在执行时,会对指令进行重排序(即与代码顺序不同)
  • 常见的指令重排序有下面 2 种情况
    • 编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序
    • 指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
  • Java 源代码会经历 编译器优化重排 —> 指令并行重排 —> 内存系统重排(主存和本地内存的内容可能不一致的过程,最终才变成操作系统可执行的指令序列。
    •  对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。
    • 对于处理器,通过插入内存屏障(Memory Barrier)(CPU指令)的方式来禁止特定类型的处理器重排序。

3、JMM

  • JMM(Java 内存模型)主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。
  • 可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。

4、JMM如何抽象线程和主存之间的关系

  • 主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中。为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值