Java重要面试名词整理(三):并发编程(上)

news/2024/12/24 11:07:20/

文章目录

    • 并发编程基础概念
      • 线程&进程
        • 进程
        • 线程
        • 进程&线程区别总结
        • 进程间的通信[面试热点]
        • CPU核心数和线程数的关系
      • 上下文切换(Context switch)
      • **并行和并发**
    • Java里的线程
      • 启动
          • **Thread** **和** **Runnable** **的区别**
          • **Callable** **、Future** **和** **FutureTask**
      • 中止
      • **线程的状态生命周期**
      • **线程的优先级**
      • **线程的调度**
      • **线程和协程**
        • 协程
        • 纤程-Java中的协程
        • JDK19 **的虚拟线程**
        • **守护线程**
      • **线程间的通信和协调、 协作**
        • **管道输入输出流**
        • volatile,最轻量的通信/同步机制
        • **等待/通知机制**
        • lost wake up 问题
      • **CompleteableFuture**
    • ThreadLocal
      • 定义
      • Hash冲突的解决
      • 内存泄漏原因
    • CAS&原子操作
      • CAS实现原子操作的三大问题
        • ABA问题
        • 循环时间长开销大
        • 只能保证一个共享变量的原子操作。
      • AtomicLong对比LongAdder
    • 并发安全
      • 线程安全性
      • 死锁
      • **活锁**
      • **线程饥饿**
      • **线程安全的单例模式**
        • 双重检查锁定
        • 单例模式推荐实现

并发编程基础概念

线程&进程

进程

进程就是用来加载指令、管理 内存、管理 IO 的。

当一个程序被运行, 从磁盘加载这个程序的代码至内存, 这时就开启了一个 进程。

简述:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程

进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进 程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由 你启动的进程。

线程

一个机器中肯定会运行很多的程序, CPU 又是有限的, 怎么让有限的 CPU 运行这么多程序呢?就需要一种机制在程序之间进行协调,也就所谓 CPU 调度。 线程则是 CPU 调度的最小单位。

简述:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据

线程必须依赖于进程而存在,线程是进程中的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不 拥有系统资源,,只拥有在运行中必不可少的资源(如程序计数器,一组寄存器和栈), 但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。 一个进程可以拥有多个线程, 一个线程必须有一个父进程。线程, 有时也被称为轻量级进程 (Lightweight Process ,LWP)

进程&线程区别总结

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

进程间的通信[面试热点]

同一台计算机的进程通信称为 IPC(Inter-process communication),不同计 算机之间的进程通信被称为 R(Remote)PC,需要通过网络,并遵守共同的协议,比 如大家熟悉的 Dubbo 就是一个 RPC 框架, 而 Http 协议也经常用在 RPC 上, 比如 SpringCloud 微服务。

大厂常见的面试题就是,进程间通信有几种方式?

  1. 管道, 分为匿名管道(pipe)及命名管道(named pipe):匿名管道可用 于具有亲缘关系的父子进程间的通信,命名管道除了具有管道所具有的功能外, 它还允许无亲缘关系进程间的通信。

  2. 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较 复杂的通信方式, 用于通知进程有某事件发生, 一个进程收到一个信号与处理器 收到一个中断请求效果上可以说是一致的。

  3. 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点, 具有写权限得进程可以按照一定得规则向消息 队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。

  4. 共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间, 不同进程可以及时看到对方进程中对共 享内存中数据得更新。这种方式需要依靠某种同步操作, 如互斥锁和信号量等。

  5. 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间的同步和互斥手段。

  6. 套接字(socket):这是一种更为一般得进程间通信机制, 它可用于网络 中不同机器之间的进程间通信,应用非常广泛。同一机器中的进程还可以使用 Unix domain socket(比如同一机器中 MySQL 中的控制台 mysql shell 和 MySQL 服 务程序的连接),这种方式不需要经过网络协议栈, 不需要打包拆包、计算校验和、维护序号和应答等,比纯粹基于网络的进程间通信肯定效率更高。

CPU核心数和线程数的关系

CPU 内核和同时运行的线程数 是 1:1 的关系

但 Intel 引入 超线程技术后,产生了逻辑处理器的概念, 使核心数与线程数形成 1:2 的关系。

在 Java 中提供了 Runtime.getRuntime().availableProcessors(),可以让我们获 取当前的 CPU 核心数, 注意这个核心数指的是逻辑处理器数。

上下文切换(Context switch)

上下文切换的概念,它是指 CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。

上下文是CPU寄存器和程序计数器在任何时间点的内容。

寄存器CPU内部的一小部分非常快的内存(相对于CPU内部的缓存和CPU 外部较慢的RAM主内存),它通过提供对常用值的快速访问来加快计算机程序的执行。

程序计数器是一种专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系统。

并行和并发

并发 Concurrent:指应用能够交替执行不同的任务, 比如单 CPU 核心下执行多 线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务, 已达到" 同时执行效果",其实并不是的, 只是计算机的速度太快,我们无法察觉到而已.

并行 Parallel:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边 打电话,这两件事情可以同时执行

两者区别:一个是交替执行,一个是同时执行

Java里的线程

启动

启动线程的方式有:

1 、X extends Thread;,然后 X.start

2 、X implements Runnable;然后交给 Thread 运行

Thread Runnable 的区别

Thread 才是 Java 里对线程的唯一抽象, Runnable 只是对任务(业务逻辑) 的抽象。 Thread 可以接受任意一个 Runnable 的实例并执行。

Callable 、Future FutureTask

Runnable 是一个接口, 在它里面只声明了一个 run()方法,由于 run()方法返 回值为 void 类型,所以在执行完任务之后无法返回任何结果。

Callable 位于 java.util.concurrent 包下, 它也是一个接口, 在它里面也只声明 了一个方法,只不过这个方法叫做 call() ,这是一个泛型接口, call()函数返回的 类型就是传递进来的 V 类型。

Future 就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get 方法获取执行结果, 该方法会阻塞 直到任务返回结果。

因为 Future 只是一个接口,所以是无法直接用来创建对象使用的,因此就有了 FutureTask。

FutureTask 类实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 接口和 Future 接口, 而 FutureTask 实现了 RunnableFuture 接口。所以它既可以 作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

因此我们通过一个线程运行 Callable,但是 Thread 不支持构造方法中传递 Callable 的实例,所以我们需要通过 FutureTask 把一个 Callable 包装成 Runnable, 然后再通过这个 FutureTask 拿到 Callable 运行后的返回值。

中止

线程自然终止

要么是 run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

stop

暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()resume()stop()。但是这些 API 是过期的,也就是不建议使用的。不建议使用的原因主要有:以 suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。

中断

安全的中止则是其他线程通过调用某个线程 A 的 **interrupt()**方法对其进行中断操作,但是线程完全可以不予理会。

线程通过方法 **isInterrupted()**来进行判断是否被中断

线程的状态生命周期

Java 中线程的状态分为 6 种:

  1. 初始(NEW):新创建了一个线程对象,但还没有调用 start()方法。

  2. 运行(RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种 状态笼统的称为“运行”。

线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start()方法。 该状态的线程位于可运行线程池中, 等待被线程调度选中, 获取 CPU 的使用权, 此时处于就绪状态(ready)。就绪状态的线程在获得 CPU 时间片后变为运行中 状态(running)。

  1. 阻塞(BLOCKED):表示线程阻塞于锁。

  2. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作 (通知或中断)。

  3. 超时等待(TIMED_WAITING):该状态不同于 WAITING,它可以在指定的时 间后自行返回。

  4. 终止(TERMINATED):表示该线程已经执行完毕。

线程的优先级

在 Java 线程中, 通过一个整型成员变量 priority 来控制优先级, 优先级的范围从 1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级,默认 优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。

线程的调度

线程调度是指系统为线程分配 CPU 使用权的过程,主要调度方式有两种: 协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)

使用协同式线程调度的多线程系统, 线程执行的时间由线程本身来控制, 线程把自己的工作执行完之后, 要主动通知系统切换到另外一个线程上。

使用抢占式线程调度的多线程系统, 每个线程执行的时间以及是否切换都由 系统决定。在这种情况下, 线程的执行时间不可控。

Java 线程调度就是抢占式调度,为什么?

因为Java虚拟机(JVM)采用了基于优先级的抢占式调度算法。

线程和协程

任何语言实现线程主要有三种方式:使用内核线程实现(1:1 实现),使用用 户线程实现(1:N 实现) ,使用用户线程加轻量级进程混合实现(N:M 实现)。

内核线程(Kernel-Level Thread , KLT) 就是直接由操作系统内核(Kernel , 下称内核) 支持的线程,这种线程由内核来完成线程切换, 内核通过操纵调度器(Scheduler) 对线程进 行调度, 并负责将线程的任务映射到各个处理器上。

严格意义上的用户线程指的是完全建立在用户空间的线程库上, 系统内核不 能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完 全在用户态中完成, 不需要内核的帮助。用户线程的优势在于不需要系统内核支援,非常快, 劣势也在于没有系统内核的支援, 所有的线程操作都需要由用户程序自己去处理。

协程

为什么用户线程又被称为协程呢?我们知道, 内核线程的切换开销是来自于 保护和恢复现场的成本, 那如果改为采用用户线程, 这部分开销就能够省略掉 吗? 答案还是“不能”。 但是, 一旦把保护、恢复现场及调度的工作从操作系统交到程序员手上, 则可以通过很多手段来缩减这些开销。

由于最初多数的用户线程是被设计成协同式调度(Cooperative Scheduling) 的,所以它有了一个别名—— “协程”(Coroutine) 完整地做调用栈的保护、 恢复工作,所以今天也被称为“有栈协程”(Stackfull Coroutine)。

协程提供规模(更高的吞吐量),而不是速度(更低的 延迟)。

纤程-Java中的协程

Fibers是Java Project Loom的核心特性。它们是一种轻量级的、用户态的线程实现,可以通过Fiber API进行创建、挂起、恢复和取消。与传统线程相比,Fibers的创建和销毁成本较低,并且可以高效地复用线程资源,使得应用程序可以拥有数千甚至数百万个并发执行的Fibers,而不会产生显著的内存开销。

目前 Java 中比较出名的协程库是 Quasar[ˈkweɪzɑː®](Loom 项目的 Leader 就 是 Quasar 的作者 Ron Pressler), Quasar 的实现原理是字节码注入,在字节码 层面对当前被调用函数中的所有局部变量进行保存和恢复。这种不依赖 Java 虚 拟机的现场保护虽然能够工作,但影响性能。

JDK19 的虚拟线程

在具体实现上, 虚拟线程当然是基于用户线程模式实现的,JDK 的调度程序 不直接将虚拟线程分配给处理器, 而是将虚拟线程分配给实际线程,是一个 M: N 调度,具体的调度程序由已有的 ForkJoinPool 提供支持。

守护线程

Daemon(守护) 线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。当一个 Java 虚拟机中不存在非 Daemon 线程的时候, Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置 为 Daemon 线程。我们一般用不上,比如垃圾回收线程就是 Daemon 线程。

Daemon 线程被用作完成支持性工作, 但是在 Java 虚拟机退出时 Daemon 线 程中的 finally 块并不一定会执行。在构建 Daemon 线程时, 不能依靠 finally 块中 的内容来确保执行关闭或清理资源的逻辑。

线程间的通信和协调、 协作

管道输入输出流

join()

把指定的线程加入到当前线程, 可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的 Join()方法, 直到线程 A 执行完毕后, 才会继续 执行线程 B 剩下的代码。

synchronized 内置锁

对象锁是用于对象实例方法, 或者一个对象实例上的, 类锁是用于类的静态 方法或者一个类的 class 对象上的。

volatile,最轻量的通信/同步机制

volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某 个变量的值,这新值对其他线程来说是立即可见的。

volatile 最适用的 场景**:一个线程写,多个线程读**。

等待/通知机制

指一个线程 A 调用了对象 O 的 wait()方法进入等待状态,而另一个线程 B 调用了对象 O 的 notify()或者notifyAll()方法, 线程 A 收到通知后从对象 O 的 wait()方法返回, 进而执行后续操 作。上述两个线程通过对象 O 来完成交互, 而对象上的 wait()和 notify/notifyAll() 的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

尽可能用 notifyall(),谨慎使用 notify() ,因为 notify()只会唤醒一个线程, 我 们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

lost wake up 问题

线程 A 调用 wait() 方法进入阻塞状态,接下来没有其他线程去唤醒线程 A,或者其他线程唤醒时机不对(早于线程 A 的 wait() ),导致线程 A 永远阻塞下去。

一般是由于 CPU 的调度,发生上下文切换导致指令执行顺序发生交错导致的。

CompleteableFuture

CompletableFuture 实现了 Future , CompletionStage两个接口。 实现了Future 接口,意味着可以像以前一样通过阻塞或者轮询的方式获得结果。

CompletableFuture 方法归类:

变换类 thenApply:关键入参是函数式接口 Function。它的入参是上一个阶段计算后的结果, 返 回值是经过转化后结果。

消费类 thenAccept:关键入参是函数式接口 Consumer。它的入参是上一个阶段计算后的结果, 没有返回值。

执行操作类 thenRun:对上一步的计算结果不关心,执行下一个操作,入参是一个 Runnable 的实 例,表示上一步完成后执行的操作。

ThreadLocal

定义

此类提供线程局部变量。这些变量与普通对应变量的不同之处在于, 访问一 个变量的每个线程(通过其 get 或 set 方法)都有自己独立初始化的变量副本。 ThreadLocal 实例通常是希望将状态与线程(例如, 用户 ID 或事务 ID)相关联 的类中的私有静态字段。

Hash冲突的解决

由于每个线程中可能会存在多个副本,因此在这个ThreadLocal类内部,又有一个 ThreadLocalMap 静态内部类,主要用于存储这些ThreadLocal副本,由于map结构查询数据的时间复杂度为O(1),因此优先考虑使用map这种数据结构存储数据

既然是用到了map这种数据结构,就要考虑hash冲突的问题,hashMap解决hash碰撞是通过数组加链表再加红黑树实现的(jdk1.8),而这个 ThreadLocalMap里面解决这个hash碰撞是引入了 Entry 数组实例

Java的Entry是一个静态内部类内部类,实现Map.Entry< K ,V> 这个接口,通过entry类可以构成一个单向链表。

开放定址法:

基本思想是,出现冲突后按照一定算法查找一个空位置存放,根据算法的不 同又可以分为线性探测再散列、二次探测再散列、伪随机探测再散列。ThreadLocal 里用的则是线性探测再散列

链地址法:

这种方法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的单链表, 并将单链表的头指针存在哈希表的第 i 个单元中, 因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。 Java里的 HashMap 用的就是链地址法,为了避免 hash 洪水攻击,1.8 版本开始还引 入了红黑树。

再哈希法:

这种方法是同时构造多个不同的哈希函数: Hi=RH1(key) i=1 ,2 ,… ,k 当哈希地址 Hi=RH1(key)发生冲突时,再计算 Hi=RH2(key)……,直到冲突 不再产生。这种方法不易产生聚集,但增加了计算时间。

建立公共溢出区

这种方法的基本思想是: 将哈希表分为基本表和溢出表两部分, 凡是和基本 表发生冲突的元素, 一律填入溢出表。

内存泄漏原因

ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长, 如果没有手动删除对应 key 就会导致内存泄漏, 而不是因为弱引用, 使用弱引用可以多一层保障。

总结

JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。 JVM 利用调用 remove 、get 、set 方法的时候,回收弱引用。

当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get 、set 方法,那么将导致内存泄漏。

使用线程池**+** ThreadLocal 时要小心, 因为这种情况下, 线程是一直在不断的 重复运行的,从而也就造成了 value 可能造成累积的情况。

CAS&原子操作

CAS 的基本思路就是, 如果这个地址上的值和期望的值相等, 则给其赋予新 值, 否则不做任何事儿,但是要返回原值是多少。 自然 CAS 操作执行完成时, 在 业务上不一定完成了, 这个时候我们就会对 CAS 操作进行反复重试, 于是就有了 循环 CAS。

Java 中的 Atomic 系列的原子操作类的实现则是利用了循环 CAS 来实现。

CAS实现原子操作的三大问题

ABA问题

因为 CAS 需要在操作值的时候, 检查值有没有发生变化, 如果没有发生变化 则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行 检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA 问题的解决思路就是使用版本号。

循环时间长开销大

自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。

只能保证一个共享变量的原子操作。

当对一个共享变量执行操作时, 我们可以使用循环 CAS 的方式来保证原子操作, 但是对多个共享变量操作时, 循环 CAS 就无法保证操作的原子性, 这个时候 就可以用锁。

AtomicLong对比LongAdder

AtomicLong 是利用了底层的 CAS 操作来提供并发性的, 调用了 Unsafe 类的 getAndAddLong 方法, 该方法是个 native 方法, 它的逻辑是采用自旋的方式不断 更新目标值,直到更新成功。

LongAdder 的基本思路就是分散热点,将 value 值分散到一个数组中,不同 线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行 CAS 操作, 这样热点就被分散了,冲突的概率就小很多。如果要获取真正的 long 值,只要 将各个槽中的变量值累加返回。

LongAdder 其实是一种“空间换时间”的思想

并发安全

线程安全性

定义:当多个线程访问某个类时, 不管运行时环境采用何种调度方式或者这些线程 将如何交替执行, 并且在调用代码中不需要任何额外的同步或者协同, 这个类都 能表现出正确的行为,那么就称这个类是线程安全的。

如何实现线程安全?

  • 线程封闭:把对象封装到一个线程里, 只有这一个线程能看到此对象。举例:栈封闭、ThreadLocal

  • 无状态的类:没有任何成员变量的类,就叫无状态的类,这种类一定是线程安全的。

  • 让类不可变:让状态不可变,加 final 关键字,对于一个类,所有的成员变量应该是私有的,同样只要有可能,所有的成员变量应该加上 final 关键字,但是加上 final要注意如果成员变量又是一个对象时,这个对象对应的类也要是不可变,才能保证整个类不可变。

  • 加锁和 CAS:我们最常使用的保证线程安全的手段, 使用 synchronized 关键字,使用显式 锁,使用各种原子变量,修改数据时使用 CAS 机制等等。

死锁

概念:指两个或两个以上的进程在执行过程中, 由于竞争资源或者由于彼此通信 而造成的一种阻塞的现象, 若无外力作用, 它们都将无法推进下去。此时称系统 处于死锁状态或系统产生了死锁。

死锁的发生必须具备以下四个必要条件。

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内 某资源只由一个进程占用。如果此时还有其它进程请求资源, 则请求者只能等待, 直至占有资源的进程用毕释放。

2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源 请求, 而该资源已被其它进程占有, 此时请求进程阻塞, 但又对自己已获得的其 它资源保持不放。

3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

4)环路等待条件: 指在发生死锁时, 必然存在一个进程——资源的环形链, 即进程集合{P0,P1,P2, ···,Pn}中的 P0 正在等待一个 P1 占用的资源; P1 正在等待 P2 占用的资源, ……,Pn 正在等待已被 P0 占用的资源。

避免死锁常见的算法有有序资源分配法、银行家算法。

活锁

两个线程在尝试拿锁的机制中, 发生多个线程之间互相谦让, 不断发生同一 个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到, 而将本来已经持有 的锁释放的过程。

解决办法:每个线程休眠随机数,错开拿锁的时间。

线程饥饿

低优先级的线程,总是拿不到执行时间

线程安全的单例模式

在设计模式中, 单例模式是比较常见的一种设计模式, 如何实现单例呢? 一 种比较常见的是双重检查锁定。

双重检查锁定

为了提高性能,会延迟初始化某些类,在第一次使用的时候做类的初始化。为了保证多线程下的线程安全,一般会做安全同步。主要用于懒初始化(lazy initialization)场景。 它确保了在多线程情况下,某个资源(如单例实例)只被初始化一次,并且在初始化后访问时无需加锁,从而减少不必要的锁开销。

双重检查锁定的核心思想是,在对某个共享资源进行访问时,首先在锁外进行一次检查,如果不满足条件(例如资源尚未初始化),才在锁内进行第二次检查,并执行 初始化操作。 这种方式可以避免每次访问资源时都进行加锁操作,降低锁带来的性能开销。

单例模式推荐实现

懒汉式

类初始化模式,也叫延迟占位模式。在单例类的内部由一个私有静态内部类来持有这个单例类的实例。 因为在 JVM 中, 对类的加载和类初始化,由虚拟机保证线程安全。

延迟占位模式还可以用在多线程下实例域的延迟赋值。

饿汉式

在声明的时候就 new 这个类的实例,或者使用枚举也可以。


http://www.ppmy.cn/news/1557721.html

相关文章

Android Bootable Recovery 中的 `bspatch.cpp` 文件详解

Android Bootable Recovery 中的 bspatch.cpp 文件详解 引言 在 Android 系统中,Recovery 模式是一个非常重要的组件,它允许用户在设备启动时执行一系列的维护操作,例如系统更新、数据擦除、备份和恢复等。Android Bootable Recovery 的核心功能之一是处理增量更新(Delta…

Java实现贪吃蛇游戏

目录 一、项目结构 二、实现步骤 1. 创建 Snake 类 2. 创建 Food 类 3. 创建 GameBoard 类 4. 创建 SnakeGame 类 三、总结 贪吃蛇是一个经典的电子游戏&#xff0c;它的玩法非常简单&#xff0c;但又充满了挑战。玩家通过控制蛇的移动&#xff0c;吃到食物并不断成长&a…

云技术基础知识(二):虚拟化与容器技术

内容预览 ≧∀≦ゞ 虚拟化与容器技术虚拟化技术一、虚拟化的核心概念二、虚拟化的主要类型1. 服务器虚拟化2. 操作系统虚拟化&#xff08;容器化&#xff09;3. 网络虚拟化4. 存储虚拟化 三、虚拟化的实现方法和工具1. 服务器虚拟化实现2. 操作系统虚拟化&#xff08;容器化&am…

最新雷蛇鼠标键盘驱动Razer Synapse 4(雷云) 下载与安装

雷蛇最近更新了驱动程序&#xff0c;Razer Synapse 4&#xff08;雷云&#xff09; 拥有全新的多线程架构&#xff0c;速度提高了 30%*。通过简化的界面体验无与伦比的速度、流畅性和稳定性&#xff0c;使用户能够快速导航&#xff0c;实现独立安装和精确设置配置。 更新一&am…

gitlab克隆仓库报错fatal: unable to access ‘仓库地址xxxxxxxx‘

首次克隆仓库&#xff0c;失效了&#xff0c;上网查方法&#xff0c;都说是网络代理的问题&#xff0c;各种清理网络代理后都无效&#xff0c;去问同事&#xff1a; 先前都是直接复制的网页url当做远端url&#xff0c;或者点击按钮‘使用http克隆’ 这次对于我来说有效的远端u…

使用FreeNAS软件部署ISCSI的SAN架构存储(IP-SAN)练习题

一&#xff0c;实验用到工具分别为&#xff1a; VMware虚拟机&#xff0c;安装教程&#xff1a;VMware Workstation Pro 17 安装图文教程 FreeNAS系统&#xff0c;安装教程&#xff1a;FreeNAS-11.2-U4.1安装教程2024&#xff08;图文教程&#xff09; 二&#xff0c;新建虚…

Android使用PorterDuffXfermode模式PorterDuff.Mode.SRC_OUT橡皮擦实现“刮刮乐”效果,Kotlin(2)

Android使用PorterDuffXfermode模式PorterDuff.Mode.SRC_OUT橡皮擦实现“刮刮乐”效果&#xff0c;Kotlin&#xff08;2&#xff09; 在 Android使用PorterDuffXfermode的模式PorterDuff.Mode.SRC_OUT实现橡皮擦&#xff0c;Kotlin&#xff08;1&#xff09;-CSDN博客文章浏览阅…

实战设计模式之抽象工厂模式

概述 前一篇文章中提到的工厂方法模式允许子类决定具体要创建的对象类型&#xff0c;但它一次只创建一个对象。抽象工厂模式则更加复杂&#xff0c;它关注的是创建一系列相关的对象。这些对象通常构成了一个完整的“家族”&#xff0c;并且在不同的实现中保持一致性和兼容性。 …