当前位置: 首页 > news >正文

java八股文之并发编程

1、线程和进程的区别

  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

2、并行和并发的区别

现在都是多核CPU,在多核CPU下

  • 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU,CPU需要进行切换
  • 并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程互不干扰,CPU不需要进行切换

3、创建线程的方式有哪些

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 线程池创建线程(项目中使用方式)

3.1 Runnable和Callable的区别是什么

  1. Runnable接口run方法没有返回值
  2. Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  3. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

3.2 线程的run()和start()有什么区别?

start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
run(): 封装了要被线程执行的代码,可以被调用多次。

4、线程有哪些状态,是如何切换的

4.1 线程的状态

  • 新建(NEW)
  • 可执行(RUNNABLE)
  • 阻塞(BLOCKED)
  • 等待(WAITING)
  • 计时等待(TIMED_WALTING)
  • 终止(TERMINATED)

4.2 线程状态的切换

  • 创建线程对象是新建状态
  • 调用了stat()方法转变为可执行状态
  • 线程获取到了CPU的执行权,执行结束是终止状态
  • 如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
  • 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
  • 如果线程调用了sleep()方法,进入计时等待状态,到时间后可切换为可执行状态

5、说一说join,notify,notifyAll方法

  • join:等待线程执行结束。例如:某个方法调用t.join();就会阻塞调用此方法的线程进入timed_waiting,直到线程t执行完成后,此线程再继续执行
  • notify:只随机唤醒一个wait线程
  • notifyAll:唤醒所有wait的线程

6、说一说wait和sleep方法的异同点

6.1 共同点

  • wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态
  • wait(long)和sleep(long)都可以等待一段时间后自己醒来

6.2 不同点

  1. sleep(long)是Thread的静态方法;而wait(),wait(long)都是Object的成员方法,每个对象都有
  2. wait()方法可以被notify和notifyAll唤醒,sleep()不行
  3. wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
  4. wait方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,你们可以用);而sleep如果在synchronized代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了)

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

有三种方式可以停止线程

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程
    – 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
    – 打断正常的线程,可以根据打断状态来标记是否退出线程,这个方法类似使用标志

8、synchronized关键字的底层原理

  • Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
  • 它的底层由monitor实现的,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor
  • 在monitor内部有三个属性,分别是owner、entrylist、waitset
    – 其中owner是关联的获得锁的线程,并且只能关联一个线程;
    – entrylist关联的是处于阻塞状态的线程,当owner关联锁后,所有再来的线程都会进入阻塞状态并关联entrylist;
    – waitset关联的是处于Waiting状态的线程,类似entrylist

9、说一说锁升级

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
markword: Mark Word 是 JVM 为对象设计的一个“多功能存储区”,负责记录对象的锁状态、年龄、哈希码等信息,帮助 JVM 实现高效同步、内存管理和垃圾回收。

偏向锁

一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断markword中是否是自己的线程id即可,而不是开销相对较大的CAS命令

轻量级锁

线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性

重量级锁

底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。

10、说一说JMM

  • JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规侧来规范对内存的读写操作从而保证指令的正确性
  • JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
  • 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存

11、说一说CAS

  • CAS的全称是:Compare And Swap(比较再交换):它体现的一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性。
  • CAS使用到的地方很多:AQS框架、AtomicXXX类
  • 在操作共享变量的时候使用的自旋锁,效率上更高一些
  • CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现

12、说一说乐观锁和悲观锁的区别

乐观锁:假定这个数据不受欢迎,会先进性数据操作,在最后才会进行校验。如CAS就是乐观锁,会先进行操作,最后才会判断
悲观锁:假定这个数据很受欢迎,会先对数据进行锁定,锁定成功才会进行操作,例如synchronized就是悲观锁。

13、说一说对volatile的理解

共享变量: 在类里面定义,可以多个方法公共访问的属性,就是共享变量
指令重排序: 指令重排序是Java中为了提升程序性能,让CPU或编译器在不影响单线程结果的前提下,调整代码执行顺序的行为。比如,你写的代码步骤是A→B→C,但实际执行可能是B→A→C,只要最终结果不变。

  • 保证线程的可见性: 用volatile修饰共享变量,能够防止因为及时编译器(JIT)对代码的优化,导致一个线程对共享变量的修改对另一个线程不可见
  • volatile禁止指令重排序: 用volatile修饰的共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止指令重排序的效果

14.解释一下公平锁和非公平锁

  • 公平锁:线程A先申请→B后申请→按A→B顺序拿锁。
  • 非公平锁:线程B可能突然抢到锁,插队到A前面(比如A刚释放锁时,B立刻抢到)。

15.说一说AQS

  • 是一种锁机制类似synchronized,是多线程中的队列同步器。它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的
  • AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
  • 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源
  • 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性
  • AQS既可以实现公平锁,也可以实现非公平锁

16.ReentrantLock的实现原理

  • ReentrantLock表示支持重新进入的锁,调用lock方法获取了锁之后,再次调用Iock,是不会阻塞的
  • ReentrantLock主要利用CAS+AQS队列来实现
  • 支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参一个布尔值设置为公平锁

17、synchronized和Lock有什么异同?

相同:
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
不同:

  1. synchronized是关键字,源码在jvm中,用c++语言实现Lock是接口,源码由jdk提供,用java语言实现
  2. 使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁
  3. Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量(类似awit和notify)
  4. Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock(读写锁)
  5. 在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,会表现更好一点;在竞争激烈时,LOCK的实现通常会提供更好的性能

18.说一说死锁

  • 死锁产生必须同时满足以下条件
    1. 互斥条件: 资源不可共享,必须由一个进程/线程独占使用。
    2. 请求与保持条件: 进程已持有资源,但仍请求新资源,且不释放已有资源。
    3. 不可剥夺条件: 资源必须由持有它的进程主动释放,不能被强制剥夺。
    4. 循环等待条件: 存在一个进程链,每个进程都在等待下一个进程持有的资源,最终形成环路。
  • 诊断是否发生死锁的方法
    1. 使用jdk自带的工具:jps和jstack。
      jps: 输出JVM中运行的进程状态信息;
      jstack: 查看java进程内线程的堆栈信息,查看日志,检查是否有死锁,如果有死锁现象,需要查看具体代码分析后,可修复)
    2. 可视化工具jconsole、VisualVM也可以检查死锁问题

19、聊一下ConcurrentHashMap

  1. 底层数据结构:
    – DK1.7底层采用分段的数组+链表实现
    – JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
  2. 加锁的方式
    – JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
    – JDK1.8采用CAS添加新节点,采用synchronizeds锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好

20、java程序中,如何保证多线程安全

java并发编程中有三大特征:

  1. 原子性:一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行。可以用synchronized、lock保证。
  2. 内存可见性:让一个线程对共享变量的修改对另一个线程可见。可以用。可以用volatile、synchronized、lock方式保证。
  3. 有序性:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顶序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执的结果是一致的。可以用volatile保证

21、说一说线程池的核心参数

  1. 核心线程数(corePoolSize): 线程池中常驻的线程数量,即使空闲也不会被销毁(除非设置允许核心线程超时)。
  2. 最大线程数(maximumPoolSize): 线程池允许存在的最大线程数,当任务量激增且队列已满时,会创建临时线程直到达到此值。
  3. 空闲线程存活时间(keepAliveTime): 非核心线程(临时线程)在空闲时的存活时间,超时后会被销毁。
  4. 工作队列(workQueue): 存放等待执行任务的队列,队列的类型影响会线程池的行为,例如:
    有界队列(如ArrayBlockingQueue):限制任务数量,防止内存溢出。
    无界队列(如LinkedBlockingQueue):一般用的就是这个,但是使用时一般会指定最大参数。如果不指定直接创建,任务就会堆到int最大值。
    特殊队列(如SynchronousQueue(无容量),直接触发创建新线程。
  5. 线程工厂(threadFactory): 自定义线程的创建方式,例如设置线程名称、优先级或线程组。
  6. 拒绝策略(handler): 当任务无法提交(线程数已达maximumPoolSize且队列已满)时的处理方式:
    AbortPolicy(默认):直接抛出RejectedExecutionException异常。
    CallerRunsPolicy:由提交任务的线程自己执行(避免新任务被丢弃)。
    DiscardPolicy:直接丢弃任务,不报错。
    DiscardOldestPolicy:丢弃队列中最老的任务,尝试提交新任务。
  7. 时间单位(unit): 与keepAliveTime配合,指定存活时间的单位(如秒、分钟等)。

22、如何确定核心线程数

  1. 高并发、任务执行时间短→(CPU核数+1),减少线程上下文的切换
  2. 并发不高、任务执行时间长
    – IO密集型的任务→(CPU核数*2+1):文件读写,DB读写,网络请求等
    – 计算密集型任务→(CPU核数+1):计算型代码、Bitmap转换、Gson转换等
// 获取CPU核数的代码int core = Runtime.getRuntime().availableProcessors();

23、线程池种类有哪些

注:所有种类本质为线程池的默认创建,得到不同的侧重的线程池。

  1. newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
  2. newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行
  3. newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
  4. newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行

24、为什么不建议用Executorst创建线程池

  1. FixedThreadPool、SingleThreadPool:
    允许的请求队列长度为Integer.MAX VALUE,可能会堆积大量的请求,从而导致OOM。
  2. CachedThreadPool、newScheduledThreadPool:
    允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

25、如何控制某个方法允许并发访问线程的数量

在多线程中提供了一个工具类Semaphore,信号量。在并发的情况下,可以控制方法的访问量

  1. 创建Semaphore对象,可以给一个容量
  2. acquire()可以请求一个信号量,这时候的信号量个数-1
  3. release()释放一个信号量,此时信号量个数+1

26、谈谈你对ThreadLocal的理解

内存泄漏 : 无用对象因被引用而无法回收,最终撑爆内存。

  1. ThreadLocal可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】避免竞争用引发的线程安全问题
  2. ThreadLocal同时实现了线程内的资源共享
  3. 每个线程内有一个ThreadLocalMap类型的成员变量,用来存储资源对象
    – 调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
    – 调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值
    – 调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值
  4. ThreadLocalp会有内存泄漏问题。ThreadLocalMap中的key是弱引用,值为强t引用;key会被GC释放内存,关联value的内存并不会释放。解决方式是主动remove释放key,value

http://www.mrgr.cn/news/95619.html

相关文章:

  • springboot自定义工具
  • Pytest的Fixture使用
  • 施耐德 Unity Pro 项目配置
  • 综合章节:游戏网络化、模组化与深度扩展
  • 红宝书第十一讲:超易懂版「ES6类与继承」零基础教程:用现实例子+图解实现
  • React 中useMemo和useCallback Hook 的作用,在什么场景下使用它们?
  • 未初始化引用检测工具UBITech
  • 架构思维:如何设计一个支持海量数据存储的高扩展性架构
  • 快速入手:Nacos融合SpringCloud成为注册配置中心
  • kotlin知识体系(三) : Android Kotlin 中的函数式编程实践指南
  • 通往自主智能之路:探索自我成长的AI
  • UDP套接字编程(代码)
  • SpringMVC_day02
  • 分布式系统设计陷阱,白话CAP理论
  • 运行时智控:PanLang 开发者指南(一)运行时系统核心模块实现——PanLang 原型全栈设计方案与实验性探索5
  • nacos未经授权创建用户漏洞
  • 快速入手-基于Django的Form和ModelForm操作(七)
  • SAP-ABAP:SAP BW模块架构与实战应用详解
  • 网心云OEC/OEC-turbo刷机问题——刷机教程、救砖方法、技术要点及下载boot失败异常解决尝试
  • 银河麒麟桌面版包管理器(二)