2022最新JUC+多线程面试题

Java 线程面试题总结2022 对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的 class 对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个 class 对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。 有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存 在的,类锁其实锁的是每个类的对应的 class 对象。类锁和对象锁之间也是互不干扰的。 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到 阅读详情

Java中实现多线程有几种方法

创建线程的常用的几种方式:

  1. 继承Thread类

  2. 实现Runnable接口 (重写run方法,无返回值)

  3. 实现Callable接口( JDK1.5>=,重写call方法,可以自定义返回值 )

  4. 线程池方式创建

线程的几个基本状态

  1. 新建状态(New):新创建了一个线程对象。

  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  5. 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)

  6. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

  7. 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

  8. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

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

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

  2. 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作 废的方法。

  3. 使用interrupt方法中断线程。

  4. 名称解释public void interrupt()该方法只是设置当前线程的中断状态为true,发起一个协商而不会立刻停止线程public static boolean interrupted()返回当前线程的中断状态,将当前线程的中断状态清空并重新设置为falsepublic boolean interrupted()判断当前线程是否被中断,通过检查中断标志位

notify()和notifyAll()有什么区别?

  1. notify可能会导致死锁,而notifyAll则不会 任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码 使用notifyall,可以唤醒 所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。

  2. wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调 用notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。

  3. notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事 项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到 WaitSet中.

sleep()和wait() 有什么区别?

  1. 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类 中的。

  2. sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象 锁。

  3. 当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。

Thread 类中的start() 和 run() 方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的 效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start() 方法才会启动新线程。

为什么wait, notify 和 notifyAll这些方法不在thread类里面?

明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线 程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所 以把他们定义在Object类中因为锁属于对象。

synchronized 和 ReentrantLock 有什么不同?

  1. 用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。

  2. 获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。

  3. 锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。

  4. 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。

  5. import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockInterrupt { static Lock lockA = new ReentrantLock(); static Lock lockB = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { // 线程 1:先获取 lockA 再获取 lockB Thread t1 = new Thread(() -> { try { // 先获取 LockA lockA.lockInterruptibly(); // 休眠 10 毫秒 TimeUnit.MILLISECONDS.sleep(100); // 获取 LockB lockB.lockInterruptibly(); } catch (InterruptedException e) { System.out.println("响应中断指令"); } finally { // 释放锁 lockA.unlock(); lockB.unlock(); System.out.println("线程 1 执行完成。"); } }); // 线程 2:先获取 lockB 再获取 lockA Thread t2 = new Thread(() -> { try { // 先获取 LockB lockB.lockInterruptibly(); // 休眠 10 毫秒 TimeUnit.MILLISECONDS.sleep(100); // 获取 LockA lockA.lockInterruptibly(); } catch (InterruptedException e) { System.out.println("响应中断指令"); } finally { // 释放锁 lockB.unlock(); lockA.unlock(); System.out.println("线程 2 执行完成。"); } }); t1.start(); t2.start(); TimeUnit.SECONDS.sleep(1); // 线程1:执行中断 t1.interrupt(); } } 复制代码

  6. 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。

谈谈Lock锁底层实现原理

底层基于AQS+CAS+LockSupport锁实现

AQS是什么

AQS抽象的队列同步器,是整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型的变量表示持有锁的状态;当有线程获取不到锁时,就将线程加入该队列中,通过CAS自旋和LockSupport.part()的方式,维护state变量,达到并发同步的效果。

AQS的核心原理

AQS使用一个violatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要抢占资源的线程封装成一个Node节点来完成锁的分配,通过CAS完成对state值的修改;核心就是state+CLH带头节点的双端队列

ReentrantLock获取锁的过程

  1. 公平锁:当一个线程在尝试获得锁时,如果锁的state=0,且等待队列为空则获得锁,否则进入队列尾,当持有资源的线程释放锁时唤醒队首线程

  2. 非公平锁:当一个线程在尝试获得锁时,直接尝试CAS,成功则占有锁,否则进入队列尾,当持有资源的线程释放锁时唤醒队首线程,如果队首线程获得锁成功则弹出,否则不弹出

有三个线程T1,T2,T3,如何保证顺序执行

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。

Thread类中的yield方法有什么作用?

Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法 而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可 能在进入到暂停状态后马上又被执行。

线程池创建需要的那几个核心参数的含义

ThreadPoolExecutor 最多包含以下七个参数:

  1. corePoolSize:线程池中的核心线程数

  2. maximumPoolSize:线程池中最大线程数

  3. keepAliveTime:闲置超时时间

  4. unit:keepAliveTime 超时时间的单位(时/分/秒等)

  5. workQueue:线程池中的任务队列

  6. threadFactory:为线程池提供创建新线程的线程工厂

  7. rejectedExecutionHandler:线程池任务队列超过最大值之后的拒绝策略

ThreadPoolExecutor 有哪些常用的方法?

ThreadPoolExecutor有如下常用方法:

  1. submit()/execute():执行线程池

  2. shutdown()/shutdownNow():终止线程池

  3. isShutdown():判断线程是否终止

  4. getActiveCount():正在运行的线程数

  5. getCorePoolSize():获取核心线程数

  6. getMaximumPoolSize():获取最大线程数

  7. getQueue():获取线程池中的任务队列

  8. allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程这些方法可以用来终止线程池、线程池监控等。

Java线程池中submit() 和 execute()方法有什么区别?

两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了 Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些 方法。

常用的线程池有哪些?

  1. newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

  2. newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

  3. newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

  4. newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

  5. newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

线程池中的workQueue有哪几种

  • 1、LinkedBlockingQueue,一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。

  • 2、SynchronousQueue,无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。拥有公平(FIFO)和非公平(LIFO)策略,使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界(Integer.MAX_VALUE),避免线程拒绝执行操作。

  • 3、ArrayBlockingQueue,一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。

  • 4、DelayedWorkQueue,其特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

shutdownNow() 和 shutdown() 两个方法有什么区别?

shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

线程池的工作原理

  1. 提交任务后,先判断当前池中线程数是否小于 corePoolSize,如果小于,则创建新线程执行这个任务。

  2. 否则,判断线程池任务队列是否已满,如果没有满,则添加任务到任务队列。

  3. 否则,判断当前池中线程数是否大于 maximumPoolSize,如果大于则执行预设拒绝策略。

  4. 否则,创建一个线程执行该任务,直至线程数达到maximumPoolSize,达到后执行预设拒绝策略。

线程池创建之后,会立即创建核心线程么

并不会立即创建核心线程,而是等到有任务提交时才会开始创建线程,除非调用了prestartCoreThread/prestartAllCoreThreads 事先启动核心线程。

prestartCoreThread: 启动一个核心线程,使其空闲等待工作。这会覆盖仅在执行新任务时启动核心线程的默认策略。如果所有核心线程都已启动,此方法将返回 false。如果线程已启动,则返回 true。

public boolean prestartCoreThread() { return workerCountOf(ctl.get()) < corePoolSize && addWorker(null, true); } 复制代码

prestartAllCoreThreads:启动所有核心线程,使它们空闲等待工作。这会覆盖仅在执行新任务时启动核心线程的默认策略。返回启动的线程数。

public int prestartAllCoreThreads() { int n = 0; while (addWorker(null, true)) ++n; return n; } 复制代码

核心线程永远不会销毁么

其实核心线程只是一个动态概念,在jdk中并没有给线程打上"core"标记。而在jdk1.6之前,线程池会尽量保证会有corePoolSize个线程存活,即使这些线程已经闲置了很长的时间,这样会造成一部分资源浪费;于是在1.6开始,jdk提供了一个allowCoreThreadTimeOut方法用于控制核心线程是否被销毁。

注意: 这种策略和corePoolSize=0是有区别的

  • corePoolSize=0:在一般情况下只使用一个线程消费任务,只有当并发请求特别多、等待队列都满了之后,才开始用多线程。

  • allowsCoreThreadTimeOut=true && corePoolSize>1:在一般情况下就开始使用多线程(corePoolSize 个),当并发请求特别多,等待队列都满了之后,继续加大线程数。但是当请求没有的时候,允许核心线程也终止。

综合来看,其实corePoolSize=0的效果基本等同于allowsCoreThreadTimeOut=true&&corePoolSize=1,只是实现细节不同。

线程池的优点,作用

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池核心线程数怎么设置呢?

分为CPU密集型和IO密集型

  • CPU这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出 来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

  • IO密集型 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占 用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们 可以多配置一些线程,具体的计算方法是 : 核心线程数=CPU核心数量*2。

线程池为什么需要使用(阻塞)队列?

  1. 因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。

  2. 创建线程池的消耗较高。

知道线程池中线程复用原理吗?

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

线程池参数keepAliveTime=0 会怎么样

keepAliveTime这个参数1.6之前控制的是非核心线程的存活时间,且该参数值不能小于0,否则在创建线程池时会抛出异常。而设置为0的含义其实是指非核心线程执行完属于自己的任务后即刻销毁。从1.6开始,若 allowsCoreThreadTimeOut=true,则keepAliveTime必须大于0,否则也会报错。

线程池的拒绝策略有哪些?

主要有4种拒绝策略:

  1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略

  2. CallerRunsPolicy:只用调用者所在的线程来处理任务

  3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务

  4. DiscardPolicy:直接丢弃任务,也不抛出异常

产生死锁的四个必要条件?

  1. 互斥条件:一个资源每次只能被一个线程使用

  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放

  3. 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺

  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

什么是多线程中的上下文切换

上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是 多任务操作系统和多线程环境的基本特征。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称 作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。

Future和CompletableFuture的区别和对比

  1. Future在执行结束后没法回调,调用get方法会被阻塞;CompletableFuture调用get方法获取结果也会被阻塞。

  2. CompletableFuture可以回调,可不通过get方法获取结果。

  3. Future模式执行批量任务,在完成任务后要想执行其它任务得通过get方法获取结果,在依次遍历Future列表时,各个get方法依赖于Future列表顺序。

  4. CompletableFuture模式通过回调可以得到执行结果,各自完成任务可以立即执行接下来的任务。

  5. 通过实现CompletionStage接口,CompletableFuture对象可以级联地执行任务

  6. 可以通过anyOf方法得到多个任务中首先完成任务的CompletableFuture对象

CompletableFuture的下一步处理的线程池使用规则

如果有使用自定义线程池,那么使用的就是自定义线程池,没有则使用的是默认的ForkJoinPool线程池

如果在下一步使用的thenRunAsync方法,且没有传入自定义的线程池,就使用的是默认的,传入线程池就使用自定义的线程池。

其他thenApply和thenAccept亦是如此。thenRun使用的是上一步的线程池,如果上一步执行的太快,也会用main线程,thenRunAsync使用的是默认线程池

CompletableFuture.supplyAsync的下一步后续处理操作

  1. thenRun():执行异步任务,没有上一步的结果也没有返回值

  2. thenApply():执行异步任务,有上一步的结果也有返回值

  3. thenAccept():执行异步任务,有上一步的结果,但没有返回值

System.out.println(CompletableFuture.supplyAsync(() -> "returnA").thenRun(() -> { // 执行异步任务,没有上一步的结果也没有返回值 }).join()); System.out.println(CompletableFuture.supplyAsync(() -> "returnA").thenApply(res -> { // 执行异步任务,有上一步的结果也有返回值 return res + " aaa"; }).join()); System.out.println(CompletableFuture.supplyAsync(() -> "returnA").thenAccept(res -> { // 执行异步任务,有上一步的结果,但没有返回值 System.out.println(res); }).join()); 复制代码

线程安全需要保证几个基本特征?

  1. 原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。

  2. 可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将 线程本地状态反映到主内存上,volatile 就是负责保证可见性的。

  3. 有序性,是保证线程内串行语义,避免指令重排等。

下线程之间是如何通信的?

线程之间的通信有两种方式:共享内存和消息传递。

共享内存 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来 隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。

例如线程 A 与 线程 B 之间如果要通信的话,那么就必须经历下面两个步骤:

  1. 线程 A 把本地内存 A 更新过得共享变量刷新到主内存中去。

  2. 线程 B 到主内存中去读取线程 A 之前更新过的共享变量。

消息传递 在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行 通信。在 Java 中典型的消息传递方式,就是 wait() 和 notify() ,或者 BlockingQueue 。

什么是乐观锁什么是悲观锁

悲观锁: 就是默认认为在本线程使用该系统资源的时候就一定会有别的线程来进行争抢,就默认加锁。适合写多读少的场景,先加锁可以保证写操作是数据正确

乐观锁: 认为自己再使用数据的时候不会有别的线程来修改数据或资源,所以不会加锁。只是在更新数据的时候去判断一下有没有别的线程更新了这个数据,如果这个数据没有被更新,就将当前线程的数据写入,如果有更新,则根据不同的实现方式来执行不同的操作,比如放弃修改,重试抢锁等,适合读多写少的情况

判断规则:

  • 版本号机制version

  • 最常采用的是CAS算法,Java原子类中的递增操作就是通过CAS自旋实现

为什么每一个对象都可以成为一个锁

Java中每一个对象都继承了Object类,Object类中有一个叫ObjectMonitor.java的监视器,java的底层是c++,在ObjectMonitor.java中又调用了ObjectMonitor.cpp,在ObjectMonitor.cpp中又调用了ObjectMonitor.hpp,在ObjectMonitor.hpp文件中有很多属性,比如_owner属性,就指向持有ObjectMonitor对象的线程

ObjectMonitor.hpp中的构造方法中的属性

属性

解释

_owner

指向持有ObjectMonitor对象的线程

_WaitSet

存放处于wait状态的线程队列

_EntryList

存在处于等待锁block状态的线程队列

_recursions

锁的重入次数

_count

用来记录该线程获取锁的次数

什么是管程monitor

管程就是锁,在虚拟机中monitor使用的是ObjectMonitor实现,每一个对象都天生带有一个对象监视器,每一个被锁住的对象都活和Monitor相关

什么是公平锁非公平锁

公平锁: 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买,后来的人在队尾排队,这是公平的ReentrantLock reentrantLock = new ReentrantLock(true); true表示公平锁,先来先得

非公平锁: 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程有限获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)

为什么会有公平锁,非公平锁的设计,为什么默认非公平

  1. 恢复挂起的线程对于CPU来说是处在时间差的,所以非公平锁能充分利用CPU的时间片,尽量减少了空闲状态时间

  2. 使用多线程的时候很重要的点就是线程切换带来的开销,当采用非公平锁的时候,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销

什么时候使用公平锁,什么时候用非公平锁

如果在业务场景上需要提高吞吐量的话,建议使用非公平锁,因为可以节省线程之间的切换时间,否则就使用公平锁,大家公平使用

什么是可重入锁(递归锁)

是指同一个线程在外层方法获取锁的时候,在进入改线程的内层方法会自动获取锁(前提,锁的是同一个对象),不会因为之前已经获取过锁还没有释放而阻塞。

例如:synchronized修饰的一个递归方法,程序在第二次调用的时候不需要再次去获取锁,避免自己阻塞自己,所以在Java中ReentrantLock 和synchronized都是可重入锁,这样的主要是在一定程度上避免了死锁

synchronized的重入实现原理

当执行monitorenter时,如果目标锁对象的计数器(_count)为0,那么就代表该锁没有被别的线程所持有,然后Java虚拟机就会吧该锁的持有线程设置为当前线程,并把计数器加1

如果计数器不为0的时候,就会去判断该锁的owner属性锁指向的线程是不是当前线程,是的话就把计数器( count,recursions)加1,否则需要等待,直至持有线程释放该锁

当执行monitorexit时,java虚拟机就会把改锁的计数器(count),可重入recursions减去1,计数器为0的时候就代表锁已经被释放

什么是LockSupport

用于创建锁和其他同步类的基本线程阻塞原语。本质上是一个线程阻塞的工具类,所有的方法是今天方法,LockSupport调用的是Unsafe中的native方法,可以在任意位置唤醒线程,其中提供可一个许可证的概念,但许可证只有一个,不会累加,可以通过park方法使其阻塞,unpark方法为某一个线程发放通行证

为什么LockSupport可以突破像类似wait和notify,lock和containal的顺序关系

因为unpark就已经为该线程发放了一张通行证,在调用park方法的时候回去检查该线程有没有通行证,有就直接放行,没有则阻塞

为什么LockSupport被唤醒两次后阻塞两次,但最终结果还是会阻塞线程

因为凭证的数量始终只有一个,在park两次之后凭证不够两次消费,所以不能放行

为什么volatile不具备原子性

举一个例子,就相当于i++这样的操作来说,在多线程的环境下,每个线程一开始都获得了i的值放回到自己的工作内存中之后,需要对i进行计算,然后再进行赋值,当在这个计算的时间中,有别的线程速度比较快,计算完成之后写入主内存,然后发出通知,当前线程收到通知的时候本次计算没有完成就又重新读取,重新计算,所以会导致计算丢失。也就是说在对一个值的取值,计算,复制的这三个步骤来说没有保证原子性,所以volatile也不具备原子性

综上所述,volatile变量不适合参加到依赖当前值的运算,如i=i+1;i++等的之类的,所以我们通常用volatile保存某个状态的boolean值或者int值

为什么我们只要加了一个volatile修饰一个变量的时候,就会去加上一个内存屏障

当我们用volatile修饰一个变量的时候,在他的class文件中我们可以发现该变量被加上了一个标签,也就是ACC_VOLATILE,然后吧字节码生成机器码的时候,就会按照JMM的规范,在相应位置插入内存屏障;

什么叫做内存屏障

是一种屏障指令,它是的CPU或者编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束,也叫做内存栅栏或者栅栏指令

CAS的原理呢?

CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:

  1. 变量内存地址,V表示

  2. 旧的预期值,A表示

  3. 准备设置的新值,B表示

当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。底层调用Unsafe类中的方法

CAS有什么缺点吗

  1. ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是 A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是 ABA的问题大部分场景下都不影响并发的最终效果(Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段, 更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。)

  2. 循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。

  3. 只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多 个可以通过AtomicReference来处理或者使用锁synchronized实现。

happen-before规则

虽然指令重排提高了并发的性能,但是Java虚拟机会对指令重排做出一些规则限制,并不能让所有 的指令都随意的改变执行位置,主要有以下几点:

  1. 单线程每个操作,happen-before于该线程中任意后续操作

  2. volatile写happen-before与后续对这个变量的读

  3. synchronized解锁happen-before后续对这个锁的加锁

  4. final变量的写happen-before于final域对象的读,happen-before后续对final变量的读

  5. 传递性规则,A先于B,B先于C,那么A一定先于C发生

LongAdder为什么比AtomicLong块

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

sum()方法会将Cell数组中的value和base累加作为返回值。

核心思想就是将AtomicLong的一个value的更新压力分散到多个vulue中去

说说CyclicBarrier和CountDownLatch的区别?

  1. CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了 这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某 个数值-1而已,该线程继续运行

  2. CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务

  3. CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再 用了

什么是AQS?

简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。

如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了, ReentrantLock、CountDownLatch、Semaphore等等都用到了它。

AQS实际上以双向队列的形式 连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队 列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。

AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可 以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。

了解Semaphore吗?

semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数, 可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待, 等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中 传入的int型整数n=1,相当于变成了一个synchronized了。

ThreadLocal原理?

ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部 副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。 ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组, Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对 的能力。

为什么ThreadLocal要使用弱引用

弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无 法被回收,弱引用则会在下一次GC的时候被回收。 但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key 为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。 但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出 现这个问题的

对象的内存布局

Object o = new Object(); 复制代码

对于我们new一个新的对象,这个对象在java的堆存储的内存布局可分为,对象头,实例数据,对齐填充(不够8的倍数,就填充)

锁的升级流程

  1. 当没有被当做锁的时候,这就是个普通对象,就是无锁的状态

  2. 当对象被当做同步锁时,一个线程A抢到锁时,此时锁升级为偏向锁,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码,这也是偏向锁的意义

  3. 当一个线程B尝试获取锁,JVM发现当前的锁处于偏向状态,并且现场ID不是B线程的ID,那么线程B会先用CAS将线程id改为自己的,这里是有可能成功的,因为A线程一般不会释放偏向锁。

  4. 偏向锁抢锁失败,则说明当前锁存在一定的竞争,偏向锁就升级为轻量级锁。

  5. 轻量级锁抢锁失败,则JVM会使用自旋锁,自旋锁并非是一个锁,则是一个循环操作,不断的尝试获取锁。自旋锁重试之后仍然未抢到锁,同步锁会升级至重量级锁,锁标志位改为10,在这个状态下,未抢到锁的线程都会被阻塞,由Monitor来管理,并会有线程的park与unpark,因为这个存在用户态和内核态的转换,比较消耗资源,故名重量级锁

什么叫锁消除

锁的对象要是同一个,而不是多个对象

什么叫锁粗化

假如方法中收尾相接,而且前后相邻的同步代码块都是锁的统一个对象,JIT编译器就会把这几个同步块合成一个大块,加粗加大范围,一次申请使用即可,避免每次的申请和释放锁,提升了性能

银河麒麟V10离线安装VLC播放器:依赖处理与3.0.9.2版本实操指南 在国产化操作系统日益普及的背景下,软件安装方式成为政企内网用户关注的重点。离线安装作为一种不依赖公网源的部署手段,常用于解决内网环境下的软件获取问题。其核心在于理解Linux包管理机制,如dpkg与apt对依赖关系的处理逻辑。掌握这一原理,不仅能应对多媒体软件的部署,也能提升系统维护效率。VLC作为跨平台多媒体播放器,凭借强大的解码能力与协议支持,广泛应用于本地视频播放、RTSP拉流及监控大屏等场景。本文从离线安装的通用概念出发,结合银河麒麟V10桌面版的实际环境,详细梳理了VLC 3.0.9.2离线包的获 阅读详情

相关推荐

随机森林文本分类

随机森林原理及代码实现 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 例如:第一章 Python 机器学习入门之pandas的使用 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录随机森林原理及代码实现前言一、集成算法是什么?二、随机森林是什么三、 数据预处理3.1 先来看一下我们的文本数据3.2 预处理3.3 统计评论情感值积极与消极的占比二、使用步骤参数介绍(4个)属性1.引入库2.参数调参方法总结 前言 提示:这里可以添加本文要记录的大概内容: 例如

m0_46579176的博客 5808

Java实习生------JUC并发编程(多线程)10道面试题打卡⭐⭐⭐

它认为多线程同时修改资源的概率比较高,很容易出现冲突,所以它在访问资源之前需要先加锁,synchronized就是基于这种思想;主要用于写比较多的情况它会先修改资源,再验证这段时间内有没有其他线程正在修改资源,如果没有,那么操作完成;如果有,就放弃本次操作;因为它全程没有加锁,所以又叫无锁编程;主要用于多读或者加锁成本比较高的场景版本号机制:在数据表中添加了version字段,表示数据被修改的次数。当数据被修改时,version加1。

每周更新,热爱coding! 425

手把手教你用ESP32驱动MAX30102心率血氧传感器(含完整代码)

本文详细介绍了如何使用ESP32微控制器驱动MAX30102心率血氧传感器,构建智能健康监测系统。内容涵盖PPG原理、硬件连接、驱动配置、信号处理算法以及Web服务器数据可视化,提供完整代码实现,帮助开发者快速上手物联网健康设备开发。

weixin_29208077的博客 587

Java 面试问题 —— 多线程模块(JUC 模块)

JUC多线程面试题——多线程高阶

baiyang2001的博客 1421

统信UOS系统盘扩容保姆级教程:用GParten-分区编辑器5分钟搞定

本文提供统信UOS系统盘扩容的详细图形化教程。针对系统盘空间不足问题,重点介绍如何使用系统自带的GParten-分区编辑器,在5-10分钟内安全、直观地完成扩容操作,无需记忆复杂命令,适合所有用户快速解决存储空间告急的困扰。

e3f4g5的博客 642

武汉理工大学数据结构综合实验——连连看游戏综合实践

文章目录实验目的主要仪器设备及耗材一、实验要求二、分析与设计(非线性结构)1.数据结构的设计2.核心算法设计一条直线消子两条直线消子三条直线消子初始化游戏地图搜寻路径提示和重排3.测试用例设计4.测试效果总结 实验目的 (1)线性结构实验目的 调研连连看游戏,了解连连看游戏的功能和规则等 掌握集成开发工具VS2019的使用和C++的基础编程 了解MFC框架 了解线性结构,掌握数组的遍历、消子和胜负判断等算法 了解企业软件开发过程,应用迭代开发思路进行项目开发,养成良好的编程习惯和培养软件工程化思维 .

mo_zhe的博客 7612

JUC多线程面试题

JUC多线程面试题 一、进程与线程的区别 进程:是系统进行资源分配和调度的基本单位,是操作系统结构的基础,在系统中正在运行的一个应用程序就是一个线程。 线程:一个线程中有多个进程,线程是进程内独立执行的一个单元执行流。 二、并发与并行的区别 并发:同一时刻多个线程在访问同一个资源, 例如:春运抢票 电商秒杀。 并行:同一个时刻多个线程访问不同的资源,例如:多项工作多人同时执行,之后再汇总。 三、wait 和 sleep 的区别 不同点: sleep 是 Thread 的静态方法,wait 是 Ob

三分强势七分暖的博客 794

深度学习笔记——前向传播与反向传播、神经网络(前馈神经网络与反馈神经网络)、常见算法概要汇总

本文简要介绍深度学习的前向传播与反向传播,以及前馈神经网络与反馈神经网络。

haopinglianlian的博客 3043

Java多线程常用面试题(含答案,精心总结整理)

Java并发编程问题是面试过程中很容易遇到的问题,提前准备是解决问题的最好办法,将试题总结起来,时常查看会有奇效。

乌枭的博客 9456

图像处理基础 || (一) python 读取图像的常用方式

图像的读取与保存是图像处理或计算机视觉领域中最基本的操作,python中有众多的库支持图像的读取、显示与存储,常用的库包括 matplotlib、skimage 和 Pillow 库。

weixin_40234309的博客 5689

超详细的多线程笔记—从Java多线程入门到JUC并发编程

一、需要掌握的前置概念与知识二、Java实现多线程三、Thread类学习四、线程不安全五、JUC中的Locks包六、进程通信与线程间通信七、死锁问题与死锁避免八、八锁现象彻底理解锁九、线程安全的集合十、Callable详解十一、JUC辅助类详解十二、线程池

1165

从0到1:OpenClaw 助力单元测试代码自动化与覆盖率飞升

OpenClaw 的横空出世,宛如一道曙光,照亮了这片黑暗的领域。它作为一款强大的本地 AI 助手,拥有着令人惊叹的能力。OpenClaw 能够深入理解代码的逻辑结构,自动分析代码中的各种功能和潜在的风险点,从而精准地生成高质量的单元测试代码。它就像是一位不知疲倦的测试专家,能够快速、准确地完成那些繁琐的测试编写工作,将开发者从重复劳动中解放出来,让他们能够将更多的精力投入到更具创造性和价值的工作中。

远程部署调试 运行安装 项目调试 二次开发 项目技术新 持续迭代 部分源码免费分享 477

最新2021-2022实习校招java面试题JUC大厂高频面试考点(下)(持续更新)

JUC面试题 一、volatile 1、了解volatile吗 volatile是Java虚拟机提供的轻量级的同步机制,有3个特性,分别是:保证可见性、不保证原子性、禁止指令重排 2、什么是指令重排 计算机在执行程序时,为了提高性能,编译器在编译java代码和处理器jvm字节码的时候常常会做指令重排多线程中使用线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测,所以需要使用轻量级的同步机制volatile。 3、在哪些地方用到过volatile? 多线程单例

yuec1998的博客 2828

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

java经典面试题JUC并发篇(持续更新)

什么是ABA:在CAS过程中,线程1、线程2分别从内存中拿到了当前值为A,同时线程2把当前值A改为B,随后又把B改回来变为A,此后线程1检查到当前值仍为A而导致执行cas成功,但这个过程却发生了ABA问题,现场资源可能和当初不一样了(线程2把当前值由A->B->A)解决方法:版本号机制,利用版本号标记线程1拿到的‘当前值’的版本,若线程2进行了A->B->A操作,则版本号会改变,那线程1再次拿到的‘当前值’的版本和第一次的肯定是不同的,从而判定cas失败;解释器JIT对热点代码的优化。

欢迎来到烟火气知识小天地!这里记录生活、工作中的 “小麻烦”,从软件故障到生活难题,我会毫无保留分享解决思路与方法,愿成为你排忧解难时的温暖参考,伴你在解决问题路上稳步前行。 5500

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

Juc_并发编程目录

只有登上山顶,才能看到那边的风光 时刻磨刀,学习提升,才能不被淘汰 更新中 感谢周阳老师的付出,将大厂学院JUC全部公开,第一次接触周阳老师的课程那个时候在学校,记得那个时候接近毕业了,看的JVM,出去准备面试。接着陆陆续续看了老师的cloud、面试第一季、第二季等,收获很大,也让自己在这个知识点上有了更多的理解。谈谈看完老师JUC后需要注意的几个小问题: (1). 不需要预习,直接看视频劲量在课上就进行消化,建议自己也总结,加上自己的认知 (2). 从CompletableFeture到JVM、到后面的

所得皆惊喜 8万+

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

【java后端学习路线5】JVM+JUC+Nginx学习指南,985本海硕自学转码

欢迎大家关注我的微信公众号:程序员城南,回复关键字获取笔记链接。这期视频是JVM+JUC+Nginx学习指南,祝愿大家都能过个好年,同时在今年的春招和秋招拿到自己心仪的offer!

985本海硕自学转码经历分享 1154

JUC第一讲:Java并发知识体系详解 + 面试题汇总(P6熟练 P7精通)

面试时常常被面试官问到这个问题,Java并发包含实际业务开发中的方方面面。本篇文章是JUC第1讲,作为开篇,讲解Java 并发相关知识体系,包含理论基础,线程基础,synchronized,volatile,final关键字,JUC框架等内容。

Jet_qi的博客 1262

2022最新高级java面试题

文章目录HashMap底层原理,扩容机制,加载因子为什么是0.75?并发修改异常解决办法?jdk8以后会使用红黑树优化?红黑树和二叉平衡树的区别,红黑树和B树,B+树的区别,Mysql二大引擎索引底层实现,HashMap在多线程环境中为何出错?ConcurrentHashMap底层实现底层实现原理:HashMap加载因子为什么是0.75?实际运用出现的问题分析:CAS,原子引用,ABA问题,volatile?什么是Spring IOC,Spring AOP?应用场景有哪些?数据库事务隔离级别,数据库的四大属

liuerchong的博客 3039

熬夜总结的2022java面试题

2022java面试题

客官不爱喝酒的博客 2098

2022最新解析最清晰 Java 系列面试题

1.int和Integer有什么区别?为什么要有包装类?2.说一下final关键字和final的4种用法?3.final、finally、finalize 有什么区别?4.重写 equals 时为什么一定要重写 hashCode?5.HashMap有几种遍历方法?推荐使用哪种?6.元素排序Comparable和Comparator有什么区别?7.如何实现 List 集合去重?8.HashSet如何保证元素不重复?9.接口和抽象类有什么区别?10.this和super有什么区别?this能调用到父类吗?11.

l688899886的博客 110

好家伙!阿里并发核心编程宝典(2022版)一夜登顶Github热榜第一

不知道大家今年的金九银十是否有出去面试过?有出去面试的朋友肯定深有感受,像我们刚入行那会面试的加分项现在卷得已经成为了面试的基础题(手动狗头)。其中最典型的就属这个Java并发编程了。之前一般只有大厂才会有高并发编程相关的面试内容,但现在只要你入了Java行业就会涉及,不管你工作中是否用到,面试就得问。So,我们这些生活在内卷时代的新生代农民工只能把Java并发知识也变成我们必修课之一,不断的提升自己的核心竞争力。

stone_tmp的博客 388

阿里、京东、拼多多、天猫技术面,面试题及答案分享(2022年Java岗)

百度(3轮技术面): 1.synchrnoized和reentrantlock的底层实现及重入的底层原理 monitorenter monitorexit AQS。 重入原理:entry count,state 2.锁的四种状态及升级过程 3.CMS和G1的异同 4.G1什么时候引发Full GC 5.除了CAS,原子类,syn,Lock还有什么线程安全的方式 final 6.HashMap和Hashtable的异同 7.允许null键的map你知道哪些 HashMap,LinkedHashMap,Weak

m0_69305074的博客 315

2023 年阿里、腾讯、百度、华为、京东、美团和滴滴最新 Java 面试题汇集

前一段时间和大牛们交流了一下,据反馈现在Java岗位也没有以前那么多了,没这么好找了,面临 2022 年寒冬季节,大量公司模仿 O2O 模式导致死掉企业的很多,在加之培训机构大量的培训人,导致供大于求,当然这不意味着饱和,只是市场更趋于合理一些(只要技术好不用怕的)。最近结合一些面试的同学的反馈,顺便整理了下一些面试题目。希望对大家有所帮助,后期会不断更新添加新的面试题。可以帮大家查漏不缺。以下是(2021、2022 、阿里、腾讯、百度、华为、京东、搜狗和滴滴面试题汇集)

m0_50180963的博客 233

精心整理200道最新Java中高级工程师高频面试题

你好,相信你一定很期待这份面试题,这份面试题是我花数个月整理出来的,内容涵盖了各方各面,特别是JVM、Mysql、多线程高并发、Redis、Spring这几个专题,是博主精心整理的亲身被面过的阿里、京东、虾皮、蚂蚁等一些大厂的面试题,学会进大厂真的不是梦! 精彩内容截图: 光看截图看不够?别急,下面即将解锁这200道面试题的题目与答案,想进大厂?靠它准没错! 文章目录1、面试题1、JVM2、集合3、多线程4、Redis5、消息队列6、Mysql7、Spring8、Mybatis9、设计模式10、

学无止境! 2462
上一篇: 这样加密SpringBoot 配置文件,更加安全!
下一篇: 抗住大批量请求的高并发架构设计经验
Java_LingFeng
博客等级 码龄4年 505粉丝 259原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值