八股学习-JUC java并发编程

news2025/3/22 14:23:19

本文仅供个人学习使用,参考资料: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秒后修改 flag
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = false; // 修改后可能只更新了本地内存,未同步到主内存
        }).start();

        // 线程 B:循环检测 flag
        new 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关键字

synchronized锁的应用

synchronized 是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

synchronized锁的主要使用方式有以下三种:

1.修饰实例方法(锁当前的对象实例)

synchronized void method() {
    //业务代码
}

进入同步代码前,要获得当前对象实例的锁

2.修饰静态方法(锁当前类)

synchronized static void method() {
    //业务代码
}

给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁

3.修饰代码块

synchronized(this) {
    //业务代码
}
  • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
  • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁

注意构造方法不能使用synchronized关键字修饰,不过可以在构造方法内部使用synchronized。


                

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

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

相关文章

PostgreSQL_数据下载并保存(psycopg2)

目录 前置: 1 数据下载 1.1 多个股票多个交易日 1.2 一个交易日所有股票 2 数据保存,使用python中的psycopg2包 2.1 在PyCharm中创建新项目,并安装包 2.2 代码-多个股票多个交易日 2.3 代码-一个交易日所有股票 2.4 在 pgAdmin4 中…

启明星辰春招面试题

《网安面试指南》https://mp.weixin.qq.com/s/RIVYDmxI9g_TgGrpbdDKtA?token1860256701&langzh_CN 5000篇网安资料库https://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247486065&idx2&snb30ade8200e842743339d428f414475e&chksmc0e4732df793fa3bf39…

边缘计算革命:重构软件架构的范式与未来

摘要 边缘计算通过将算力下沉至网络边缘,正在颠覆传统中心化软件架构的设计逻辑。本文系统分析了边缘计算对软件架构的范式革新,包括分布式分层架构、实时资源调度、安全防护体系等技术变革,并结合工业物联网、智慧医疗等场景案例&#xff0c…

【读点论文】Chain Replication for Supporting High Throughput and Availability

在分布式系统中,强一致性往往和高可用、高吞吐是矛盾的。比如传统的关系型数据库,其保证了强一致性,但往往牺牲了可用性和吞吐量。而像 NoSQL 数据库,虽然其吞吐量、和扩展性很高,但往往只支持最终一致性,无…

Servlet、Servlet的5个接口方法、生命周期、以及模拟实现 HttpServlet 来写接口的基本原理

DAY15.1 Java核心基础 Servlet Servlet是一个接口,是java的基础,java之所以编写web的程序,接收请求并响应,就是因为Sevlet接口 Java 类实现了Servlet接口的时候就可以接收并响应请求,成为web服务器 Web服务器就是接…

贝叶斯公式的一个直观解释

E E E:抓到娃娃 H H H:坐地铁 H ˉ \bar H Hˉ:坐公交 P ( E ) P ( H ) P ( E ∣ H ) P ( H ‾ ) P ( E ∣ H ‾ ) P({E}) P({H}) P({E} \mid {H}) {P}(\overline{{H}}) {P}({E} \mid \overline{{H}}) P(E)P(H)P(E∣H)P(H)P(E∣H) P (…

Java 大视界 -- Java 大数据分布式计算中的通信优化与网络拓扑设计(145)

💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也…

reconstruct_3d_object_model_for_matching例子

文章目录 1.获取om3文件2.准备可视化3.准备3D可视化4.读取3D模型5.显示成对注册结果16.显示成对注册结果27.联合注册模型8.处理图像8.1子采样8.2 图像计算与平滑8.3 三角测量 9.基于表面做3D匹配10.评估模型准确度10.1 在场景中找到模型10.2 计算模型和场景之间的距离 11.立体系…

【JavaWeb学习Day27】

Tlias前端 员工管理 条件分页查询&#xff1a; 页面布局 搜索栏&#xff1a; <!-- 搜索栏 --><div class"container"><el-form :inline"true" :model"searchEmp" class"demo-form-inline"><el-form-item label…

Webrtc编译官方示例实现视频通话

Webrtc编译官方示例实现视频通话 前言 webrtc官网demo中给了一个供我们学习和应用webrtc的一个很好的例子&#xff1a;peerconnection&#xff0c;这期我们就来编译和运行下这个程序看看视频通话的效果以。 1、打开源码工程 继上期源码编译完成后&#xff0c;我们使用vs打开…

大数据学习(80)-数仓分层

&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一…

刘强东突然发声:不该用算法压榨最底层兄弟!东哥,真正的人民企业家

今天忙了一天&#xff0c;很累&#xff0c;准备睡觉的时候&#xff0c;看到网上盛传的刘强东的朋友圈&#xff0c;东哥又在朋友圈发文了。 说实话&#xff0c;看完之后&#xff0c;感动&#xff0c;真的感动。 尤其是当我看到这两句话的时候。 1、我们所学的知识、商业模式、技…

Java 记忆链表,LinkedList 的升级版

文章目录 记忆链表 MemoryLinkedList实战源代码 众所周知&#xff0c;ArrayList 和 LinkedList 是 Java 集合中两个基本的数据结构&#xff0c;对应数据结构理论中的数组和链表。但在这两个数据结构&#xff0c;开发者们通常使用 ArrayList&#xff0c;而不使用 LinkedList。JD…

poetry安装与使用

文章目录 安装方法创建虚拟环境其他常用命令从 poetry.lock 中安装第三方依赖包 安装方法 安装命令&#xff08;全局安装&#xff0c;不要在虚拟环境中安装&#xff0c;方便后面创建环境使用&#xff09; pip install poetry修改虚拟环境路径&#xff08;首次使用poetry时执行&…

UVM config机制及uvm_resource_pool

目录 1. uvm_config_db 类源码 1.1 set 1.2 get 2. uvm_resource_pool 2.1 uvm_resource_pool::set 2.2 uvm_resource 3. usage 4. 小结 uvm提供一种uvm_config_db机制使得在仿真中通过变量设置来修改环境,使环境更加灵活。本文主要介绍uvm_config_db#(type)::get/set…

JAVA学习*接口

接口 在生活中我们常听说USB接口&#xff0c;那接口是什么呢&#xff1f; 在Java中&#xff0c;接口相当于多个类的一种公共规范&#xff0c;是一种引用数据类型。 定义接口 public interface IUSB {public static final String SIZE "small";public abstract vo…

Python实验:读写文本文件并添加行号

[实验目的] 熟练掌握内置函数open()的用法&#xff1b;熟练运用内置函数len()、max()、和enumerate()&#xff1b;熟练运用字符串的strip()、ljust()和其它方法&#xff1b;熟练运用列表推导式。 [实验和内容] 1.编写一个程序demo.py&#xff0c;要求运行该程序后&#xff0…

IDEA导入jar包后提示无法解析jar包中的类,比如无法解析符号 ‘log4j‘

IDEA导入jar包后提示无法解析jar包中的类 问题描述解决方法 问题描述 IDEA导入jar包的Maven坐标后&#xff0c;使用jar中的类比如log4j&#xff0c;仍然提示比如无法解析符号 log4j。 解决方法 在添加了依赖和配置文件后&#xff0c;确保刷新你的IDE项目和任何缓存&#xff…

数据结构——顺序栈seq_stack

前言&#xff1a;大家好&#x1f60d;&#xff0c;本文主要介绍了数据结构——顺序栈 目录 一、概念 1.1 顺序栈的基本概念 1.2 顺序栈的存储结构 二、基本操作 2.1 结构体定义 2.2 初始化 2.3 判空 2.4 判满 2.5 扩容 2.6 插入 入栈 2.7 删除 出栈 2.8 获取栈顶元…

python3.13.2安装详细步骤(附安装包)

文章目录 前言一、python3.13.2下载二、python3.13.2安装详细步骤1.查看安装文件2.启动安装程序3.安装模式选择4.自定义安装配置5.高级选项设置6.执行安装7.开始安装8.安装完成8.打开软件9.安装验证 前言 在数字化时代&#xff0c;Python 已成为不可或缺的编程语言。无论是开发…