八股学习-JUC java并发编程

本文仅供个人学习使用,参考资料:JMM(Java 内存模型)详解 | JavaGuide 

线程基础概念

用户线程:由用户空间程序管理和调度的线程,运行在用户空间。

内核线程:由操作系统内核管理和调度的线程,运行在内核空间。

二者的区别和特点:用户献线程创建和切换成本低,但不可以利用多核,内核态线程,创建和切换成本高,可以利用多核。

jdk1.2之后的线程都是操作系统的线程,即基于原生线程实现(Native Threads)。

线程模型:一对一,多对一,多对多,这里就不说了。

在Windows和Linux中,java线程采用的都是一对一的模型。

线程和进程的区别:

线程是进程划分为更小的运行单位。线程和进程的最大的不同之处在于基本上各进程是独立的,而各线程不一定,同一进程中的线程极有可能会相互影响。线程执行开销小,但是不利于资源的管理和保护;而进程正相反。

创建线程的方式:严格来说,java只用一种方式可以创建线程:new Thread().start()

什么时候会发生线程的上下文切换?

  • 主动让出 CPU,比如调用了 sleep(), wait() 等。
  • 时间片用完。
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
  • 被终止或结束运行

Thread.sleep()和Object.wait()方法对比

这两个方法都能暂停线程的执行,

  • Thread.sleep():定义在 Thread 类中,是一个静态方法。

  • Object.wait():定义在 Object 类中,是一个实例方法。

Thread.sleep()方法能让当前线程暂停执行指定的时间,进入(Timed_watiting状态)不释放任何锁资源,主要用于延迟执行或定时任务。

Object.wait()让当前线程进入等待状态,进入waiting或者Timed_waiting状态,必须持有对象的监视器锁*(即在synchronized块中调用),调用后会释放锁。主要用于线程间通信,等待其他线程通过notify()或者notifyAll()唤醒。

对于object.wait()的理解:

想象你和朋友合租,共用卫生间(共享资源):

  1. synchronized(lock):卫生间的门锁,一次只能一个人用。

  2. lock.wait():你进去后发现没纸了,于是出来并把钥匙挂回门口(释放锁),坐在沙发上等(等待)。

  3. lock.notify():室友买了纸后喊一声“有纸了!”,你听到后可以去抢钥匙。

这样设计保证了安全和效率!

可以直接调用Thread类中的run方法吗?

当直接执行run()方法中的内容时,会把run()方法当成一个main线程下普通方法,这并不是真正的多线程工作,只有new一个Thread,然后线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

单核cpu支持java多线程吗?怎么实现的?

单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。

os主要通过两种线程调度方式来管理多线程的执行:

  • 抢占式调度(Preemptive Scheduling):操作系统决定何时暂停当前正在运行的线程,并切换到另一个线程执行。(时间片轮转,公平性较好,cpu利用率高)
  • 协同式调度(Cooperative Scheduling):线程执行完毕后,主动通知系统切换到另一个线程。这种方式可以减少上下文切换带来的性能开销,但公平性较差,容易阻塞。

使用多线程可能带来什么问题?

内存泄露,死锁,线程不安全等等。

死锁的四个条件:

互斥条件,请求与保持条件,不剥夺条件,循环等待条件。

如何预防死锁?

破坏请求与保持:一次性申请所有资源

破坏不剥夺:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放

破坏循环等待:通过按序申请资源,释放资源则反序释放。

如何避免死锁?

银行家算法评估。

JAVA内存模型(JMM)

JMM主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。

问题引出:

现代编译器在单线程下会对指令进行重排序来优化性能,但是没有义务保证多线程间的语义也一致。

常见的指令重排序有两种:

  • 编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
  • 指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

对着两种重排序的处理方式也不一样:

  • 对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。

  • 对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。

内存屏障是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它会在处理器写入值时,强制将写缓冲区中的数据刷新到主内存;在读取值之前,使处理器本地缓存中的相关数据失效,强制从主内存中加载最新值,从而保障变量的可见性。

什么是JMM?

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

说白了就是定义了一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序。对于 Java 开发者说,你不需要了解底层原理,直接使用并发相关的一些关键字和类(比如 volatilesynchronized、各种 Lock)即可开发出并发安全的程序。

java如何抽象线程和内存之间的关系

在现有的java内存模型下,线程可以把变量保存到本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写,这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

给出JMM的抽象示意图:

主内存vs本地内存

主内存(Main Memory)
  • 是什么:所有线程共享的内存区域,存储全局变量(如堆中的对象、静态变量等)。

  • 特点

    • 线程间共享,所有线程都能“看到”主内存中的数据。

    • 速度慢:访问主内存需要经过总线、缓存等,效率较低。

本地内存(Local Memory)
  • 是什么:每个线程独有的内存区域(实际是 JMM 的抽象概念,可能对应 CPU 缓存、寄存器等)。

  • 特点

    • 线程私有,其他线程无法直接访问。

    • 速度快:本地内存是线程的“工作副本”,用于缓存主内存中的数据。

    • 线程对变量的操作(读/写)优先在本地内存中进行,之后才会同步到主内存。

为什么会出现本地内存?

  • 性能优化:直接操作主内存太慢,本地内存(如 CPU 缓存)能大幅提升线程运行速度。

  • 副作用:本地内存的缓存机制会导致线程间数据不一致(需要开发者处理)。

然而由于二者的存在可能导致可见性和原子性的问题

可见性问题:

public class VisibilityProblem {private static boolean flag = true; // 主内存中的变量public static void main(String[] args) {// 线程 A:1秒后修改 flagnew Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}flag = false; // 修改后可能只更新了本地内存,未同步到主内存}).start();// 线程 B:循环检测 flagnew Thread(() -> {while (flag) { // 可能一直读取本地内存中的旧值// 空循环}System.out.println("线程 B 检测到 flag 已修改");}).start();}
}

结果可能:线程 B 永远无法退出循环,因为它读取的一直是自己本地内存中的旧值 flag = true

如何解决可见性问题:

方法 1:使用 volatile 关键字
  • 强制变量的读写直接操作主内存,跳过本地内存。

  • 修改示例代码:

    private static volatile boolean flag = true; // 添加 volatile
方法 2:使用 synchronized 同步块
  • 进入同步块时,会清空本地内存,从主内存重新加载变量。

  • 退出同步块时,会将本地内存的修改强制写回主内存。

并发编程的三个特性:

原子性:一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。(synchronized锁和各种lock)

可见性:

当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。

在 Java 中,可以借助synchronizedvolatile 以及各种 Lock 实现可见性。

如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

有序性:

由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。

我们上面讲重排序的时候也提到过:

指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。

在 Java 中,volatile 关键字可以禁止指令进行重排序优化。

Volatile关键字

如果一个变量被volatile关键字声明了,那么在java内存模型中读取它时就变成了这样的方式:

一个变量如果用volatile关键字修饰,就能保证数据的可见性,但是不能保证数据的原子性,synchronized关键字二者都能保证。

如何禁止指令重排序

在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:

public native void loadFence();
public native void storeFence();
public native void fullFence();

通过这三个指令也能实现和volatile禁止重排序一样的效果,只是很麻烦。

这里给出一个例题,来自javaguide,同时也是我快手日常实习一面挂掉的一道题

解释并手写一下双重检验锁方式实现单例模式

代码:

public class Singleton{private volatile static Singleton uniqueInstance;private Singleton(){   }public static Singleton getUniqueInstance(){if(uniqueInstance==null){synchronized(Singleton.class){if(uniqueInstance == null){uniqueInstance =new Sngleton();}}}return uniqueInstance;}

uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

虽然这个volatile能保证多线程环境下变量的可见性,但是保证不了变量的原子性,只有synchronized锁和ReentranLock锁才能保证原子性和可见性。

悲观锁和乐观锁

悲观锁:每次获取资源都要上锁,其他线程想要拿到资源就要阻塞,直至锁被释放。共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程。

典型代表:synchronized和ReentranLock锁。

问题:高并发的场景下激烈的锁竞争会造成线程阻塞,导致频繁的上下文切换,增加系统的性能开销。还存在死锁问题。

乐观锁:乐观锁不加锁,不停的执行,只是在提交修改的时候去验证对应的资源是否被其他线程修改了

典型代表:版本号机制或者CAS算法

在 Java 中java.util.concurrent.atomic包下面的原子变量类(比如AtomicIntegerLongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。

问题:乐观锁相对悲观锁不存在锁竞争造成阻塞的问题,但是在写占比非常多的时候,会频繁的失败和重试,这样也会非常影响性能。

综上,悲观锁适用于写比较多的情况,乐观锁适用于读比较多的情况。

乐观锁的实现方式:

版本号机制和CAS算法(java并没有直接实现CAS,CAS相关的实现是通过C++内联汇编的形式实现的,JNI调用)

sun.misc包下的Unsafe类提供了compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong方法来实现的对Objectintlong类型的 CAS 操作

Java中如何实现CAS?

使用Unsafe类,AtomicInteger的底层实现就是利用了Unsafe类提供的方法。

CAS操作可能会因为并发冲突而失败,因此通常会与while循环搭配使用,在失败后不断重试,直到操作成功。这就是 自旋锁机制 。

CAS的ABA问题怎么解决?

ABA问题的解决思路是在变量前面追加上版本号或者时间戳。

如果版本号和预期值都相等,那么就可以更新。

CAS的缺点:

CAS会采用自旋操作来进行重试,也就是不成功就一直循环直到执行成功,会带来很大的CPU开销

CAS操作只对单个共享变量有效,当需要操作多个共享变量时,CAS就无能为力,jdk1.5开始提供了AtomicReference类,通过将多个变量封装在一个对象中,我们可以使用AtomicReference来执行 CAS 操作。

除了 AtomicReference 这种方式之外,还可以利用加锁来保证。

synchronized关键字


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

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

相关文章

C++基础 [八] - list的使用与模拟实现

目录 list的介绍 List的迭代器失效问题 List中sort的效率测试 list 容器的模拟实现思想 模块分析 作用分析 list_node类设计 list 的迭代器类设计 迭代器类--存在的意义 迭代器类--模拟实现 模板参数 和 成员变量 构造函数 * 运算符的重载 运算符的重载 -- 运…

VScode的debug

如果有命令行参数的话: 打开调试配置: 在 VS Code 中,按下Ctrl Shift D打开调试面板。点击面板顶部的齿轮图标,选择“添加配置…” (Add Configuration...)。 创建新的调试配置: 选择Python,然后选择…

工作记录 2017-02-08

工作记录 2017-02-08 序号 工作 相关人员 1 修改邮件上的问题。 更新RD服务器。 郝 更新的问题 1、CPT的录入页面做修改 1.1、Total 改为 Price 1.2、当删除行时,下面的行自动上移。 2、Pending Payments、Payment Posted、All A/R Accounts页面加了CoIns…

Java SE 面经

1、Java 语言有哪些特点 Java 语言的特点有: ①、面向对象。主要是:封装,继承,多态。 ②、平台无关性。一次编写,到处运行,因此采用 Java 语言编写的程序具有很好的可移植性。 ③、支持多线程。C 语言没…

springboot基于session实现登录

文章目录 1.理解session2.理解ThreadLocal2.1 理解多线程2.2 理解lambda表达式2.3 ThreadLocal 3.基于session登录流程图4.具体登录的代码实现4.1短信发送功能4.2 短信验证码登录注册功能4.登录校验功能4.1 配置登录拦截器LoginInterceptor4.1.1 ThrealLocal类实现 4.2登录拦截…

【ArduPilot】Windows下使用Optitrack通过MAVProxy连接无人机实现定位与导航

Windows下使用Optitrack通过MAVProxy连接无人机实现定位与导航 配置动捕系统无人机贴动捕球配置无人机参数使用MAVProxy连接Optitrack1、连接无人机3、设置跟踪刚体ID4、校正坐标系5、配置IP地址(非Loopback模式)6、启动动捕数据推流 结语 在GPS信号弱或…

MSys2统一开发环境,快速搭建windows opencv环境

文章目录 摘要下载msys2安装Mingw64安装Cmake安装opencv报错一报错二问题一 摘要 本篇基于之前发布的opencv两篇文章,进行的流程简化,旨在优化windows opencv环境和实例运行,Msys2统一开发环境,有利于长远的开发环境,也简化了后续集成的难度…

基于单片机的多功能热水器设计(论文+源码)

1系统方案设计 基于单片机的多功能热水器系统,其系统框图如图2.1所示。主要采用了DS18B20温度传感器,HC-SR04超声波模块,STC89C52单片机,液晶,继电器等来构成整个系统。硬件上主要通过温度传感器进行水温的检测&am…

详解Sympy:符号计算利器

Sympy是一个专注于符号数学计算的数学工具,使得用户可以轻松地进行复杂的符号运算,如求解方程、求导数、积分、级数展开、矩阵运算等。其中比较流行的深度学习框架pytorch的用到了Sympy,主要用于将模型的计算图转换为符号化表达式,以便进行分…

《Python实战进阶》No27: 日志管理:Logging 模块的最佳实践(上)

No27: 日志管理:Logging 模块的最佳实践(上) 摘要 日志记录是软件开发中不可或缺的一部分,尤其是在复杂的生产环境中。Python 的内置 logging 模块提供了强大的工具来管理和记录程序运行中的各种信息。本集将深入探讨 logging 模块的核心概念&#xff0…

每日Attention学习27——Patch-based Graph Reasoning

模块出处 [NC 25] [link] Graph-based context learning network for infrared small target detection 模块名称 Patch-based Graph Reasoning (PGR) 模块结构 模块特点 使用图结构更好的捕捉特征的全局上下文将图结构与特征切片(Patching)相结合,从而促进全局/…

ospf动态路由

一、为什么使用动态路由 OSPF(open shortest path first开放最短路径优先)是内部网关协议(IGP)的一种,基于链路状态算法(LS)。 OSPF企业级路由协议(RFC2328 OSPFv2),核心重点协议 OSPF共三个版本,OSPFV1主要是实验室…

记一次服务器中木马导致cpu占用高的问题

最近准备搭建一个个人博客,发现才放了一个nginx和一个很简单的java后台cpu占用率就居高不下,然后用top命令查看果然有问题 其中这个networkservice 和sysupdate占用很高,原本还以为是系统相关的进程,但是想想如果是系统相关的进程…

基于LabVIEW的Windows平台高速闭环控制

在Windows系统下,通过LabVIEW实现高速闭环控制面临两大核心挑战:非实时操作系统的调度延迟与硬件接口的传输速度限制。以USB-6351(NI USB-6351 DAQ卡)为例,其理论采样率可达1.25 MS/s(单通道)&a…

深入理解 Linux ALSA 音频架构:从入门到驱动开发

文章目录 一、什么是 ALSA?二、ALSA 系统架构全景图核心组件详解:三、用户空间开发实战1. PCM 音频流操作流程2. 高级配置(asound.conf)四、内核驱动开发指南1. 驱动初始化模板2. DMA 缓冲区管理五、高级主题1. 插件系统原理2. 调试技巧3. 实时音频优化六、现代 ALSA 发展七…

【C语言】自定义类型:结构体

一、结构体类型的声明 我们前面学习操作符的时候已经接触过结构体了,下面我们回顾一下结构体的基本内容。 创建结构体的语法如上所示: struct是创建结构体的关键字,然后tag就是我们结构体的名称,member-list是结构体的成员列表&…

python基本运用:类的介绍和使用

一、介绍类 类(class): 用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例 实例化:创建一个类的实例,类的具体对象。 对象:通过类定义的数据结构实例。对象包括两个数据成员&#x…

Elasticsearch:使用 ColPali 进行复杂文档搜索 - 第 1 部分 - 8.18

作者:来自 Elastic Peter Straer 及 Benjamin Trent 这篇文章介绍了 ColPali 模型,这是一种 late-interaction 模型,可简化包含图片和表格的复杂文档搜索过程,并讨论了其在 Elasticsearch 中的实现。 在构建搜索应用时&#xff0c…

2025-03-19 学习记录--C/C++-C 库函数 - qsort() 实现快速排序

C 库函数 - qsort() 实现快速排序 ⭐️ C 标准库 - <stdlib.h> &#xff08;一&#xff09;、命名介绍 &#x1f36d; qsort 是 C 标准库&#xff08;stdlib.h&#xff09;中提供的一个快速排序函数&#xff0c;用于对数组进行排序。❀它的名字来源于 “Quick Sort”&…

04 泛型编程

1、概论 编程范式&#xff1a;面向过程编程、面向对象编程、泛型编程。 泛型编程&#xff1a;目的是编写能够适合多种数据类型的代码&#xff0c;而不是为每种特定的数据类型编写重复的代码。 模板是实现泛型的主要工具&#xff0c;主要分为函数模板和类模板。 函数模板&am…