java并发编程面试题精讲——day02

目录

[synchronized 支持重入吗?如何实现的?](#synchronized 支持重入吗?如何实现的?)

syncronized锁升级的过程讲一下

JVM对Synchornized的优化?

介绍一下AQS

[CAS 和 AQS 有什么关系?](#CAS 和 AQS 有什么关系?)

[如何用 AQS 实现一个可重入的公平锁?](#如何用 AQS 实现一个可重入的公平锁?)

[Threadlocal作用,原理,具体里面存的key value是啥,会有什么问题,如何解决?](#Threadlocal作用,原理,具体里面存的key value是啥,会有什么问题,如何解决?)

悲观锁和乐观锁的区别?

Java中想实现一个乐观锁,都有哪些方式?

[CAS 有什么缺点?](#CAS 有什么缺点?)

为什么不能所有的锁都用CAS?

[CAS 有什么问题,Java是怎么解决的?](#CAS 有什么问题,Java是怎么解决的?)

voliatle关键字有什么作用?

指令重排序的原理是什么?

volatile可以保证线程安全吗?

volatile和sychronized比较?

什么是公平锁和非公平锁?

非公平锁吞吐量为什么比公平锁大?

Synchronized是公平锁吗?

ReentrantLock是怎么实现公平锁的?

什么情况会产生死锁问题?如何解决?

线程池

介绍一下线程池的工作原理

线程池的参数有哪些?

线程池工作队列满了有哪些拒接策略?

有线程池参数设置的经验吗?

核心线程数设置为0可不可以?

线程池种类有哪些?

线程池一般是怎么用的?

[线程池中shutdown (),shutdownNow()这两个方法有什么作用?](#线程池中shutdown (),shutdownNow()这两个方法有什么作用?)

提交给线程池中的任务可以被撤回吗?

场景

多线程打印奇偶数,怎么控制打印的顺序

单例模型既然已经用了synchronized,为什么还要在加volatile?

3个线程并发执行,1个线程等待这三个线程全部执行完在执行,怎么实现?

[假设两个线程并发读写同一个整型变量,初始值为零,每个线程加 50 次,结果可能是什么?](#假设两个线程并发读写同一个整型变量,初始值为零,每个线程加 50 次,结果可能是什么?)


synchronized 支持重入吗?如何实现的?

synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

synchronized底层是利用计算机系统mutex Lock实现的。每一个可重入锁都会关联一个线程ID和一个锁状态status。

当一个线程请求方法时,会去检查锁状态。

  1. 如果锁状态是0,代表该锁没有被占用,使用CAS操作获取锁,将线程ID替换成自己的线程ID。
  2. 如果锁状态不是0,代表有线程在访问该方法。此时,如果线程ID是自己的线程ID,如果是可重入锁,会将status自增1,然后获取到该锁,进而执行相应的方法;如果是非重入锁,就会进入阻塞队列等待。

在释放锁时,

  1. 如果是可重入锁的,每一次退出方法,就会将status减1,直至status的值为0,最后释放该锁。
  2. 如果非可重入锁的,线程退出方法,直接就会释放该锁

syncronized锁升级的过程讲一下

无锁->偏向锁->轻量级锁->重量级锁。

对象刚创建时,对象头的Mark Word为无锁状态,无线程关联。

当我们第一个线程获取该锁的时候,就会升级成偏向锁,第一次通过cas方式将线程id放到对象头mark word中,

修改成功就获取该锁,后续该线程再次获取锁时不用cas方式,只需检查Mark Word的线程ID是否相等------是则直接获取锁,第二个线程尝试获取该锁,Mark Word的线程ID不是当前线程id,偏向锁失效,升级为轻量级锁,线程通过CAS操作将Mark Word替换为自己的锁记录指针,成功就获取轻量级锁 ,失败了自旋,若自旋达到上限仍未获取锁,轻量级锁升级为重量级锁,未获取锁的线程进入操作系统阻塞队列,后续通过操作系统互斥量竞争锁。

JVM对Synchornized的优化?

  • 锁膨胀(锁升级):synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级。JDK 1.6 之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁时都会从用户态转换成内核态,而转换的效率是比较低的。但有了锁膨胀机制之后,synchronized 的状态就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核态的转换了,这样就大幅的提升了 synchronized 的性能。
  • 锁消除:JVM 虚拟机如果检测到这段代码的锁只有当前线程能用,没有任何其他线程会碰,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。
  • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁(就是多次小锁"合并成"一次大锁",减少"锁操作的次数")
  • •自适应自旋锁:指通过自身循环,尝试获取锁的一种方式,优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。

介绍一下AQS

AQS的话,其实就一个jdk提供的类AbstractQueuedSynchronizer(抽象队列同步器),是阻塞式锁和相关的同步器工具的框架。

内部有一个属性 state 属性来表示资源的状态,默认state等于0,表示没有获取锁,state等于1的时候才标明获取到了锁。通过cas 机制设置 state 状态

在它的内部还提供了基于 FIFO 的等待队列,是一个双向列表,其中

  • tail 指向队列最后一个元素
  • head 指向队列中最久的一个元素

其中我们刚刚聊的ReentrantLock底层的实现就是一个AQS。

CAS 和 AQS 有什么关系?

CAS 和 AQS 两者的区别

• CAS 是一种乐观锁机制,它包含三个操作数:内存位置(V)、预期值(A)和新值(B)。CAS 操作的逻辑是,如果内存位置 V 的值等于预期值 A,则将其更新为新值 B,否则不做任何操作。整个过程是原子性的。

• AQS 是一个用于构建锁和同步器的框架,许多同步器如 ReentrantLock、Semaphore、CountDownLatch 等都是基于 AQS 构建的。AQS 使用一个 volatile 的整数变量 state 来表示同步状态,通过内置的 FIFO 队列来管理等待线程。

它提供了一些基本的操作,如 acquire(获取资源)和 release(释放资源),这些操作会修改 state 的值,并根据 state 的值来判断线程是否可以获取或释放资源。AQS 的 acquire 操作通常会先尝试获取资源,如果失败,线程将被添加到等待队列中,并阻塞等待。release 操作会释放资源,并唤醒等待队列中的线程。

CAS 和 AQS 两者的联系

• CAS 为 AQS 提供原子操作支持:AQS 内部使用 CAS 操作来更新 state 变量,以实现线程安全的状态修改。

在 acquire 操作中,当线程尝试获取资源时,会使用 CAS 操作尝试将 state 从一个值更新为另一个值,如果更新失败,说明资源已被占用,线程会进入等待队列。在 release 操作中,当线程释放资源时,也会使用 CAS 操作将 state 恢复到相应的值,以保证状态更新的原子性。

如何用 AQS 实现一个可重入的公平锁?

继承 AbstractQueuedSynchronizer,

实现可重入逻辑:检查当前线程是否已经持有锁,如果是,则增加锁的持有次数(通过 state 变量);如果不是,尝试使用 CAS操作来获取锁。

实现公平性,按照队列顺序来获取锁

创建锁的外部类,创建一个外部类,内部持有 AbstractQueuedSynchronizer 的子类对象,并提供 lock 和 unlock 方法,进行获取锁和释放锁。

Threadlocal作用,原理,具体里面存的key value是啥,会有什么问题,如何解决?

面试官:谈谈你对ThreadLocal的理解

候选人

嗯,是这样的~~

ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享

面试官:好的,那你知道ThreadLocal的底层原理实现吗?

候选人

嗯,知道一些~

在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来存储资源对象

当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中

当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值

当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

面试官:好的,那关于ThreadLocal会导致内存溢出这个事情,了解吗?

候选人

嗯,我之前看过源码,我想一下~~

是因为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用。

在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。

悲观锁和乐观锁的区别?

  • 乐观锁的思想:最乐观的估计,不一定的线程来修改共享变量,它在操作共享数据时,不先加锁,而是直接读取/修改数据;操作完成后,通过某种机制(如版本号、CAS)验证数据是否被其他线程修改过。若未被修改则提交,若被修改则重试
  • 悲观锁的思想:最悲观的估计,一定会有其它线程来修改共享变量,操作共享数据前,先主动加锁,确保在操作期间其他线程无法修改数据,从而避免数据竞争。

Java中想实现一个乐观锁,都有哪些方式?

  1. CAS(Compare and Swap)操作
  2. 版本号控制:增加一个版本号字段记录数据更新时候的版本,每次更新时递增版本号。在更新数据时,同时比较版本号,若当前版本号和更新前获取的版本号一致,则更新成功,否则失败。
  3. 时间戳:在更新数据时,在比较时间戳。如果当前时间戳大于数据的时间戳,则说明数据已经被其他线程更新,更新失败

CAS 有什么缺点?

  • ABA问题 :ABA的问题指的是在CAS更新的过程中,逻辑是"当前值是否等于预期旧值",但如果数据被修改为其他值后又改回原值(如A→B→A),CAS会误以为"数据未被修改" ,导致操作错误。Java中有AtomicStampedReference来解决这个问题,加入了预期标志更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
  • 循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
  • 只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现

为什么不能所有的锁都用CAS?

CAS操作是基于循环重试的机制,如果CAS操作一直未能成功,线程会一直自旋重试,占用CPU资源。在高并发情况下,大量线程自旋会导致CPU资源浪费。

CAS 有什么问题,Java是怎么解决的?

CAS 操作中增加版本号(Stamp)或标记,每次修改都更新版本号,使验证时不仅检查值,还要检查版本号是否匹配。

voliatle关键字有什么作用?

  • 保证变量对所有线程的可见性。
  • 禁止指令重排序优化:volatile关键字在Java中主要通过内存屏障来禁止特定类型的指令重排序

指令重排序的原理是什么?

在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,但是重排序要满足下面 2 个条件才能进行:

• 在单线程环境下不能改变程序运行的结果

• 存在数据依赖关系的不允许重排序。

volatile可以保证线程安全吗?

volatile关键字可以保证可见性 ,但不能保证原子性 ,因此不能完全保证线程安全 。volatile关键字用于修饰变量,当一个线程修改了volatile修饰的变量的值,其他线程能够立即看到最新的值,从而避免了线程之间的数据不一致

但是,volatile并不能解决多线程并发下的复合操作问题,比如i++这种操作不是原子操作,如果多个线程同时对i进行自增操作,volatile不能保证线程安全。对于复合操作,需要使用synchronized关键字或者Lock来保证原子性和线程安全。

volatile和sychronized比较?

Synchronized解决了多线程访问共享资源时可能出现的竞态条件和数据不一致的问题,保证了线程安全性。Volatile解决了变量在多线程环境下的可见性和有序性问题,确保了变量的修改对其他线程是可见的。

  • Synchronized: Synchronized是一种排他性的同步机制,保证了多个线程访问共享资源时的互斥性,即同一时刻只允许一个线程访问共享资源
  • Volatile: Volatile是一种轻量级的同步机制,用来保证变量的可见性和禁止指令重排序。

什么是公平锁和非公平锁?

  • 公平锁: 指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
    • 优点:在于各个线程公平平等,每个线程等待一段时间后,都有执行的机会
    • 缺点:就在于整体执行速度更慢,吞吐量更小。
  • 非公平锁: 多个线程加锁时直接尝试获取锁,能抢到锁到直接占有锁,抢不到才会到等待队列的队尾等待。
    • 优点:就在于整体执行速度更快,吞吐量更大
    • 缺点:同时也可能产生线程饥饿问题,也就是说如果一直有线程插队,那么在等待队列中的线程可能长时间得不到运行。

非公平锁吞吐量为什么比公平锁大?

  • 公平锁执行流程 :获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态 ,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
  • 非公平锁执行流程 :当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作 ,这样就加速了程序的执行效率。

Synchronized是公平锁吗?

Synchronized不属于公平锁,ReentrantLock是公平锁。

ReentrantLock是怎么实现公平锁的?

ReentrantLock公平锁与非公平锁的 lock() 方法唯一的区别就在于公平锁在获取锁时多了一个限制条件:hasQueuedPredecessors() 为 false。

公平锁和非公平锁的核心区别,如果是公平锁,那么一旦已经有线程在排队了,当前线程就不再尝试获取锁;对于非公平锁而言,无论是否已经有线程在排队,都会尝试获取一下锁,获取不到的话,再去排队。

  • tryLock() 方法,它不遵守设定的公平 原则。线程执行 tryLock() 方法的时候,一旦有线程释放了锁,那么这个正在 tryLock 的线程就能获取到锁,即使设置的是公平锁模式,就是tryLock 可以插队 。底层调用的就是 nonfairTryAcquire() ,表明了是不公平的。

什么情况会产生死锁问题?如何解决?

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件 :互斥条件是指多个线程不能同时使用同一个资源。
  • 持有并等待条件 :持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态 ,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
  • 不可剥夺条件 :不可剥夺条件是指,当线程已经持有了资源在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
  • 环路等待条件:环路等待条件指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链

避免死锁问题就只需要破环其中一个条件就可以

线程池

介绍一下线程池的工作原理

线程池为了减少频繁的创建线程销毁线程 带来的性能损耗

线程池分为核心线程池线程池的最大容量 ,还有等待任务的队列,提交一个任务,如果核心线程没有满,就创建核心线程执行,如果满了,就是会加入等待队列,如果等待队列满了,判断是否达到最大线程数量,若没有,就会创建空闲线程执行,如果都达到最大线程数量,就会按照丢弃策略进行处理。

线程池的参数有哪些?

  • corePoolSize :线程池核心线程数量
  • maximumPoolSize :限制了线程池能创建的最大线程总数(包括核心
  • keepAliveTime空闲时间,当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了 keepAliveTime,那么这个线程就会被销毁。
  • unit:空闲时间的单位。
  • workQueue:工作队列。
  • threadFactory:线程工厂。用来给线程取名字。
  • handler:拒绝策略。

线程池工作队列满了有哪些拒接策略?

常用的四种拒绝策略包括:CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy

1.AbortPolicy:直接抛出异常,默认策略;

2.CallerRunsPolicy:用调用者所在的线程来执行任务;

3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

4.DiscardPolicy:直接丢弃任务;

有线程池参数设置的经验吗?

参考回答

① 高并发、任务执行时间短 -->( CPU核数+1 ),减少线程上下文的切换

② 并发不高、任务执行时间长

  • IO密集型的任务 --> (CPU核数 * 2 + 1)
  • 计算密集型任务 --> ( CPU核数+1 )

③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)

核心线程数设置为0可不可以?

可以,当核心线程数为0的时候,会创建一个非核心线程进行执行。

当核心线程数为 0 时,来了一个任务之后,会先将任务添加到任务队列,同时也会判断当前工作的线程数是否为 0,如果为 0,则会创建空闲线程来执行任务。

线程池种类有哪些?

参考回答

在jdk中默认提供了4种方式创建线程池

第一个是:newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回 收空闲线程,若无可回收,则新建线程。

第二个是:newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列 中等待。

第三个是:newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行。

第四个是:newSingleThreadExecutor 创建一个单线程化的线程池(单例的线程池),它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

线程池一般是怎么用的?

  1. 资源风险:应该手动 new ThreadPoolExecutor 来创建线程池,Executors的快捷方法(如newFixedThreadPool的无界队列、newCachedThreadPool的无限扩线程)易导致内存溢出(OOM);
  2. 可控性:手动new ThreadPoolExecutor可自定义核心参数(核心/最大线程数、有界队列、拒绝策略),适配业务场景;
  3. 可观测性:需为线程池 命名,方便排查问题;建议监控队列积压、线程数膨胀等状态,提前预警风险。

线程池中shutdown (),shutdownNow()这两个方法有什么作用?

  1. shutdown():置SHUTDOWN状态,执行中任务继续,不再接受新任务(加任务会抛RejectedExecutionException)。
  2. shutdownNow():置STOP状态,中断所有正在执行的线程(用Thread.interrupt()),丢弃队列待执行任务并返回未执行任务列表;但因线程可能不响应中断(如无sleep/wait),未必立即退出。

提交给线程池中的任务可以被撤回吗?

提交给线程池的任务可以撤回,核心通过Future.cancel(boolean mayInterruptIfRunning)实现:

  • 未开始的任务:直接取消,不会执行;
  • 正在执行的任务:
    • 若mayInterruptIfRunning=true:调用Thread.interrupt()尝试中断(需任务响应中断,如含sleep()/wait(),否则无法停止);
    • 若mayInterruptIfRunning=false:不中断,任务继续执行完;
  • 已完成的任务:取消无效。

场景

多线程打印奇偶数,怎么控制打印的顺序

利用wait()和notify()来控制线程的执行顺序。

复制代码
public class PrintOddEven {
    private static final Object lock = new Object();
    private static int count = 1;
    private static final int MAX_COUNT = 10;

    public static void main(String[] args) {
        Runnable printOdd = () -> {
            synchronized (lock) {
                while (count <= MAX_COUNT) {
                    if (count % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + ": " + count++);
                        lock.notify();
                    } else {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };

        Runnable printEven = () -> {
            synchronized (lock) {
                while (count <= MAX_COUNT) {
                    if (count % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ": " + count++);
                        lock.notify();
                    } else {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };

        Thread oddThread = new Thread(printOdd, "OddThread");
        Thread evenThread = new Thread(printEven, "EvenThread");

        oddThread.start();
        evenThread.start();
    }
}

单例模型既然已经用了synchronized,为什么还要在加volatile?

复制代码
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }    
            }
        }
        return instance;
    }
}

volatile 确保了对象引用的可见性和创建过程的有序性,避免了由于指令重排序而导致的错误。

instance = new Singleton(); 这行代码并不是一个原子操作,它实际上可以分解为以下几个步骤:

• 分配内存空间。

• 实例化对象。

• 将对象引用赋值给 instance。

没有 volatile 的情况下,可能会出现重排序,例如先将对象引用赋值给 instance,但对象的实例化操作尚未完成。

这样,其他线程在检查 instance == null 时,会认为单例已经创建,从而得到一个未完全初始化的对象,导致错误。

3个线程并发执行,1个线程等待这三个线程全部执行完在执行,怎么实现?

可以使用 CountDownLatch 来实现 3 个线程并发执行,另一个线程等待这三个线程全部执行完再执行的需求。以下是具体的实现步骤:

• 创建一个 CountDownLatch 对象,并将计数器初始化为 3,因为有 3 个线程需要等待。

• 创建 3 个并发执行的线程,在每个线程的任务结束时调用 countDown 方法将计数器减 1。

• 创建第 4 个线程,使用 await 方法等待计数器为 0,即等待其他 3 个线程完成任务。

复制代码
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) {
        // 创建一个 CountDownLatch,初始计数为 3
        CountDownLatch latch = new CountDownLatch(3);

        // 创建并启动 3 个并发线程
        for (int i = 0; i < 3; i++) {
            final int threadNumber = i + 1;
            new Thread(() -> {
                try {
                    System.out.println("Thread " + threadNumber + " is working.");
                    // 模拟线程执行任务
                    Thread.sleep((long) (Math.random() * 1000));
                    System.out.println("Thread " + threadNumber + " has finished.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 任务完成后,计数器减 1
                    latch.countDown();
                }
            }).start();
        }

        // 创建并启动第 4 个线程,等待其他 3 个线程完成
        new Thread(() -> {
            try {
                System.out.println("Waiting for other threads to finish.");
                // 等待计数器为 0
                latch.await();
                System.out.println("All threads have finished, this thread starts to work.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

假设两个线程并发读写同一个整型变量,初始值为零,每个线程加 50 次,结果可能是什么?

在没有任何同步机制的情况下,两个线程并发对同一个整型变量进行 50 次加 1 操作,最终结果可能是 100,也可能小于 100,最坏的结果是 50,也就是最终的结果可能是在 [50, 100] 。

小于 100 情况的分析,由于对整型变量的 num++ 操作不是原子操作,它实际上包含了三个步骤:读取变量的值、将值加 1、将新值写回变量。在多线程环境下,可能会出现线程安全问题。例如,线程 1 和线程 2 同时读取了变量的当前值,然后各自将其加 1,最后都将相同的新值写回变量,这就导致了一次加 1 操作的丢失。这种情况会多次发生,最终结果就会小于 100。

复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerAddition {
    private static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                num.incrementAndGet();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                num.incrementAndGet();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("最终结果: " + num.get());
    }
}

解决方式:通过 synchronized 关键字或 ReentrantLock 确保操作的互斥性,

相关推荐
ArabySide1 小时前
【Spring Boot】基于MyBatis的条件分页
java·spring boot·后端·mybatis
z***y8621 小时前
后端服务限流配置,Nginx与Spring Cloud Gateway
java·服务器·nginx
熙客2 小时前
Java集合框架概述
java·开发语言
一只会写代码的猫2 小时前
深度解析 Java、C# 和 C++ 的内存管理机制:自动 vs 手动
java·jvm·算法
我命由我123452 小时前
Java 开发 - 简单消息队列实现、主题消息队列实现
java·开发语言·后端·算法·java-ee·消息队列·intellij-idea
绝无仅有2 小时前
电商大厂技术面试:分布式扩展与系统设计问题解析
后端·面试·架构
唐青枫2 小时前
C#.NET DateTime 最强入门到进阶:格式化、转换、UTC、时区全覆盖
c#·.net
float_六七2 小时前
SQL中=与IS的区别:关键用法解析
java·数据库·sql
rit84324992 小时前
配置Spring框架以连接SQL Server数据库
java·数据库·spring