java多线程部分问题汇总

本文详细介绍了并发、并行和串行的区别,强调了线程安全的重要性,特别是原子性、可见性和有序性。讨论了ThreadLocal的工作原理和内存泄露问题,以及如何避免内存泄露。此外,还涵盖了线程状态、线程池的原理和优势,包括线程复用、线程池参数解释和拒绝策略。最后,文章提到了Java中的一些锁机制,如synchronized和ReentrantLock的对比,以及死锁的避免方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

1并发,并行,串行区别

串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着

并行在时间上重叠,两个任务在同一时刻相互不干扰的同时执行

并发允许两个任务相互干扰,统一时间点,只能有一个任务运行,交替执行

2并发的三大特性

原子性 要么全部成功,要么全部失败 synchronized

可见性 当多个线程访问读取,一个线程修改了变量,其他线程可见,关键字 synchronized volatile final

有序性 虚拟机在代码编译对于那些改变代码顺序不会影响执行结果的代码有可能将他们重排下,虽然值最后一样,但是可能造成线程安全问题,关键字 synchronized volatile(禁止指令重拍)

i++底层执行 

将i从主内存读取到工作内存 +1运算 将结果写入工作内存 最后将工作内存刷新到主内存

3对线程安全的理解

 不应该树线程安全,应该是内存是否安全,堆是共享内存,可以被所有线程访问(当多个线程访问一个对象,如果没有额外同步控制处理,调用这个对象获取的值是一样的,那么就是线程安全)

  堆是进程和线程共有空间,分为全局堆和局部堆,全局堆是所有没有分配的空间,局部是用户分配了的空间,堆在操作系统初始化分配(堆的目的存放对象实例,几乎所有实例和数组都在这里分配内存)

   栈是线程私有 保存运行状态局部自动变量,在线程开始初始化,每个线程栈相互独立,线程安全,操作系统都是多任务的,每个进程只能访问自己的内存空间,在每个进程内存空间都会有一块特殊的公共区域就是堆内存,进程内所有线程都可以访问到改区域,这就造成问题的潜在原因

4thread和runnable区别

 thread和runnable实质区别是实现关系,无论用runnable还是thread 都会new thread 然后执行run方法,用发复杂的线程操作需求用thread,如果只是简单执行一个任务,那就用runnable,thread就是比runnable多了些api 做了一些扩展,本质没有区别,用runnable也需要new出thread 把runnable往里面传值

5threadlocal底层原理

 threadlocal是java提供的线程本地存储机制,可以利用改机制将数据缓存在某个线程内部,该线程可以在任意时刻获取缓存数据

threadlocal底层通过threadlocalmap来实现,每个thread对象(注意不是threadlocal)中都存在一个threadlocalmap,map的key为threadlocal对象,map的value为需要缓存的值

如果线程池使用threadlocal会造成内存泄露,因为当threadlocal对象使用完成之后,应该要把设置的key value也就是entry对象进行回收,但是线程池中的线程不会回收,而线程对象通过强引用指向threadlocalmap,也是通过强引用指向entry对象,线程不被回收,entry对象也不会回收,出现内存泄露,解决方法,使用了threadlocal对象后手动调用threadlocal的remove方法,清除entry对象

threadlocal经典应用场景连接管理(一个线程持有一个连接,该连接对象可以在不同方法直接进行传递,线程直接不共享同一个连接)

6threadlocal原理和场景

 每一个thread对象含有一个threadlocalmap类的成员变量threadlocal,存储本线程中所有threadlocal对象和其他对应的值,threadlocal由一个个entry对象构成,entry继承了weakreference<?> 一个entry由thread对象和object构成,entry的key是threadlocal对象,并且是一个弱引用,当没有指向key的强引用,key会被垃圾回收期回收,执行set方法,threadlocal首先获取当前线程对象,然后获取当前线程的threadlocalmap对象,以当前的threadlocal对象为key将值存储进threadlocalmap对象中。

get方法执行,threadlocal首先获取当前线程对象,然后获取当前线程threadlocalmap对象,再以当前threadlocal对象为key,获取value,由于每一个线程含有各自的threadlocalmap容器,相互独立,不存在线程安全问题,

使用场景,进行对象夸阶层传递,使用threadlocal可以避免多次传递,打破层级的约束,

线程间数据隔离,进行事务操作用于存储线程事务信息,数据库连接。

 

7threadlocal内存泄露原因,如何避免

不再被使用的对象或者变量占用的内存不能被回收,就是内存泄露

强引用使用最普通的new来创建对象,一个对象具有强引用,不会被垃圾回收器回收,当空间不足,java虚拟机抛出oom,如果想取消强引用和某个对象关联可以显示的将引用赋值为null,

弱引用 jvm进行垃圾回收,无论内存是否充足都会回收被弱引用关联的对象,在java中weakreference类表示弱引用,常用于缓存。

threadlocal原理每一个thread维护一个threadlocalmap ,key为弱引用的threadlocal实例,value是线程变量的副本。

threadlocalmap 使用threadlocal的弱引用作为key,如果一个threadlocal不存在外部强引用,key会被gc回收,这样会导致threadlocalmap中key为null,value还存在着强引用,只有thread线程退出,value强引用才会断掉,如果线程不结束,这些key为null的entry的value就会一直存在一个强引用。

 

key使用强引用,当threadlocalmap的key为强引用回收threadlocal,因为threadlocalmap持有的threadlocal的强引用,如果没有手动删除,threadlocal不会被回收,导致entry内存泄露

key使用弱引用,由于threadlocalmap持有的threadlocal的弱引用,即使没有手动删除,threadlocal也会被回收,当key为null,在下一次threadlocalmap调用set get remove 方法时候会被清除value值,

因此threadlocal内存泄露根源是由于threadlocalmap的生命周期和thread一样,如果没有手动删除对应的key会导致内存泄露,而不是因为弱引用。

threadlocal正确用法,每次用完都要调用remove方法清除数据

将threadlocal变量定义为private static 这样一直存在threadlocal的强引用,也能保证任何时候都能通过threadlocal的弱引用访问到entry的value值 用完清除掉。

8线程的生命周期 线程有哪些状态

 线程有五种状态 创建,就绪,运行,阻塞,死亡

阻塞分为三种,等待阻塞运行线程执行wait方法,该线程会释放占用的资源,不能自动唤醒,需要其他线程调用notify方法才能被唤醒,wait是object类的方法

同步阻塞,运行线程获取对象的同步锁,若该线程被别的线程占用,jvm会把该线程放入池锁中

其他阻塞 运行线程执行sleep join方法 当sleep状态超时 join等待终止 线程重新变成就绪状态

  新建状态(new) 创建一个线程

  就绪状态(runnable)线程对着创建后其他线程调用了该对象的start方法,就是可运行线程

   运作状态(running) 就绪状态线程回去cpu执行程序代码

   阻塞状态(blocked)阻塞状态时线程因为一些原因放弃cpu使用,暂时停止运行,直到线程进入就绪状态

     死亡状态(dead) 线程执行完成了或者因为异常退出run ,线程结束生命周期

9线程池线程复用的原理

 线程池将线程和任务进行解藕,线程是线程,任务是任务,摆脱了之前通过thread创建线程时一个线程必须对应一个任务的限制,在线程池中,同一个线程可以从阻塞队列中不断获取新的任务来执行,其核心在于线程池对thread进行封装,并不是每次执行任务都会调用thread start来创建线程,而是让每一个线程去执行一个循环任务,在这个循环任务不断检查是否有任务需要被执行,有直接执行 也就是调用任务run方法,将run方法当成一个普通方法执行,通过这个方法只使用固定线程,就将所有任务的run方法串联起来了

10线程池底层工作原理11线程池处理流程

 线程池内部通过队列和线程实现的 当我们利用线程池执行任务

 如果线程池中线程树小于corepoolsize(核心线程数),即使线程池线程都处于空闲状态也需要创建新的线程来处理被添加的任务

  线程数等于corepoolsize但是缓冲队列workqueue没有满,那么任务放入缓冲队列

  线程数大于核心线程数,缓冲队列满了,并且线程池数量小于maxmunpoolsize(最大线程数),创建新的线程来处理被添加的任务

线程数大于核心数程数,缓冲队列满了,并且线程池数量等于maxmunpoolsize(最大线程数),通过handler指定嗯拒接策略来处理此任务

  当线程池中线程数大于corepoolsize,如果某个线程的空闲时间超过keepalivetime(空闲过期时间,超时时间),线程将被终止,这样线程池可以动态调整池中的线程数

 

12为什么用线程池,参数有哪些

 降低资源消耗提高线程利用率,降低创建和销毁线程的消耗

  提高响应速度,任务来了,直接有线程可以用,而不是先创建线程

  提高线程的管理,使用线程池可以统一分配调优监控

 corepoolsize核心线程数 这些线程创建后不会销毁 而是常驻线程

 maxnumpoolsize 最大线程数 它于核心线程数相对应,表示最大允许创建的线程数

 keepalivetime unit 表示超过核心线程数之外的线程的空闲存活时间,也就是核心线程不会销毁,超出的线程如果到达空闲时间就会销毁,可以通过setkeepalivetime设置超时时间

 workqueue 用来存放等待执行的任务

 threadfactory 线程工厂,用来生成线程执行任务

 handel任务拒绝策略,

 CallerRunsPolicy:由调用execute方法提交任务的线程来执行这个任务

AbortPolicy:抛出异常RejectedExecutionException拒绝提交任务

DiscardPolicy:直接抛弃任务,不做任何处理

DiscardOldestPolicy:去除任务队列中的第一个任务,重新提交

自定义拒绝策略RejectedExecutionHandler接口

 shutdown 只会中断空闲线程  

 shutdownnow 会中断所有线程

 

 13守护线程理解

守护线程为所有非守护线程提供服务的线程,守护线程终止自身无法控制

作用 gc垃圾回收 thread setdaemon(true) 必须先设置是否是守护线程 在启动线程,要不会报错,一个非法的线程。守护线程有自动结束自己生命周期的特点,非守护线程没有,设置为守护线程jvm退出时,线程会自动关闭结束,主线程运行结束,程序就会停止,用户线程执行完成,守护线程也会结束

 

14如何理解volatile关键字

 在并发中,存在三大特性 原子 有序 可见,volatile用来修饰对象属性可以保证这个属性的可见性,在修改时候,会直接将cpu缓存的数据写回主内存,对这个变量读取会从主内存读取,保证了可见性,底层通过操作系统内存屏障实现,会禁止指令重排,同时保证有序

 

15synchronized和reentrantlock区别

 syn是关键字 lock是一个类

syn会自动加锁和释放锁 lock需要手动释放

syn底层是jvm层面的锁,lock是api层面的锁

syn是非公平锁 lock可以选择公平和非公平

syn锁的是对象 锁信息在对象头中,lock通过代码中int类型的state标示来识别锁状态

syn底层有一个锁升级过程  

它们都是可重入锁

 

16syn的偏向锁 轻量级锁 重量级锁

 偏向锁 ,在锁对象的对象头记录一下当前获取到锁的线程id ,该线程下次再来直接获取锁。默认开启偏向锁,是懒加载,要是直接加载需要启动添加配置

 轻量级锁 由偏向锁升级,当一个线程获取到锁,此时这把锁是偏向锁,另一个线程来竞争,偏向锁会升级到轻量级锁,轻量级锁底层用自旋来实现,不会阻塞线程,自旋是线程通过cas获取预期的一个标识,

 重量级锁 如果自旋到一定次数还没有获取到锁,会升级到重量级锁,阻塞线程

 

17sleep wait join yield 区别

 sleep是thread类的静态本地方法,wait是object类本地方法

sleep不会释放锁,wait会释放,而且会加入等待队列

sleep方法不依赖同步器synchronized,但是wait需要

sleep不需要被唤醒,wait需要

sleep一般用于当前线程休眠,或者轮询暂停操作,wait多用于多线程之间通信

sleep会让出cpu执行时间并且强制上下午切换,wait不一定,可能还有机会重新竞争到锁继续执行

 yield方法执行后线程直接进入就绪状态,马上释放cpu执行权,但是保留cpu执行资格,有可能cpu下次进行线程调用还是会让这个线程获取到执行权继续执行

join方法 执行后线程进入阻塞,例如线程b调用线程a的join,线程b会进入阻塞队列,直到线程a结束或者中断线程

18reentrantlock中公平锁和非公平锁底层实现

  底层都会用aqs来进行排队,都是可重入锁,区别在于线程在使用lock方法加锁,如果是公平锁,会先检查aqs队列是否存在线程在排队,如果有线程排队,当前线程也进行排队,如果是非公平锁不会去检查是否有线程排队,而是直接竞争锁,不管公平还是非公平,一旦没有竞争到锁,都会进行排队,当锁释放掉,都是唤醒排队在最前面的线程,所以非公平锁只是体现了线程加锁阶段,没有体现线程唤醒阶段。

 

19reentrantlock的trylock和lock区别

 reentrantlock的lock 没有返回值 阻塞线程

 trylock有返回值boolean 非阻塞线程,尝试加锁,自旋锁一般用 

20countdownlatch 和semaphore区别实现

 countdownlatch 表示计数器,可以给count downlatch设置一个数字,一个线程调用count downlatch的await方法将会阻塞,其他线程可以调用count downlatch的count down方法数字减一,当数字减到0所有await方法都会唤醒,底层用aqs排队,一旦数字减到0,aqs排队线程依次唤醒

 semaphore信号量,可以设置许可个数,表示同时允许最多多少个线程使用该信号量,通过acquire方法获取许可,如果没有许可可阻塞线程,通过aqs排队,通过release方法释放许可,当线程释放某个许可,会从aqs中正在排队的第一个线程开始依次唤醒,直到没有空闲许可

21线程池阻塞队列作用,为什么先添加队列而不是先创建最大线程

 一般队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前任务,阻塞队列通过阻塞可以保留住当前想要继续入队的任务,阻塞队列可以保证任务队列中没有任务时候阻塞获取任务的线程,使得线程进入wait状态,释放cpu,阻塞队列自带阻塞和唤醒功能,不需要额外出力,没有任务时候,线程池利用阻塞队列的take方法挂起,维持核心线程存活,不用一直占用cpu。在创建新的线程时候,需要获取全局锁,这时需要阻塞,影响整体效率

22并发问题

 22.1java如何开启线程和保证线程安全

  线程和进程区别,进程是操作系统进行资源分配的最小单元,线程是操作系统进行任务分配的最小单元,线程属于进程。

开启线程 1继承thread类 重写run方法,2实现runnable接口实现run方法,没有返回值3实现callable接口,实现call方法,有返回值,通过futruetask创建一个线程,获取线程执行的返回值,4通过线程池开启线程

保证线程安全 通过加锁 方式,1synchronized jvm级别锁2lock jdk锁

22.2volatile和synchronized区别 volatile能保证线程安全吗 dcl双重检查单利为什么加volatile

 1syn关键字用来加锁,volatile只是保证变量的线程可见性,通常适用一个线程写,多个线程读

2不能保证线程安全,volatile只能保证线程可见性,不能保证原子性,因为用于一个线程写,多个线程读,一个线程修改先把主内存读取到工作内存,之后会把修改的值刷新到主内存,其他线程获取主内存

3volatile禁止指令重排,在dcl中防止高并发下,指令重排造成线程安全问题,涉及到对象的创建,1分配空间2初始化3创建一个指针对应关系。如果是132就没有创建对象

 

22.3java线程锁机制是怎样的。怎么升级的

 java锁就是在对象的markword中记录一个锁的状态无锁 偏向 轻量级 重量级 对应不同的锁状态,java锁机制就是根据资源竞争过程不断升级,new一个对象 偏向锁开启(默认开启)-偏向锁-轻量级锁-自旋锁(wait)-重量级锁

22.4aqs的理解 aqs实现可重入锁(需要完善)

 aqs是一个java线程同步框架,是jdk很对锁工具的实现,在aqs中维护了一个state信号量和一个线程组成的双向链表队列,其中这个线程队列就是给线程排队的 而state用来控制线程排队还是放行。

在可重入锁用state状态表示加锁次数,0无锁 释放锁就减一 

 

22.5ABC三个线程,如何保证三个线程同时执行,怎么在并发下保证依次执行,怎么保证交叉执行

count downlatch 递减计数器 控制一个线程等待多个线程 调用await 方法等待 count down递减 作用多个线程同时到达某一个地点,之后同时执行,不能重复使用

cylicbarrier循环栅栏 加法计数器,可以让一组线程等待到某一个状态之后同时执行,循环因为等待线程被释放后可以调用reset方法重用,和count downlatch区别是count down方法后会继续执行自己任务,而cylibarrier会在所有线程任务到达栅栏之后才会执行后续任务。

semaphore信号量 许可 acquire方法获取令牌没有许可的话阻塞

release方法释放许可 avaailablepermits获取当前信号可用许可

保证三个线程同时执行 使用count downlatch

在并发下保证依次执行,使用一个变量 A=1执行完把state变量(用volatile修饰)改成2

 B线程等于2执行完改成3

 保证交叉执行 使用semaphore信号量 ,先把信号量占用执行完一个之后,释放下一个要执行的信号量

22.5如何对字符串快速进行排序(了解)

 fork join框架 分而治理(拆分阶段 汇总阶段)

22.6java死锁怎么避免

 造成死锁原因 

一个资源每次只能被一个线程使用,

一个线程再阻塞等待某个资源,不释放已经占用的资源

一个线程已经获取到的资源在没有使用完之前不能被强制剥夺

很多线程形成头尾相接的循环等待资源关系

造成死锁必须满足以上条件 如果避免死锁 只需要不满足其中一个,而前三个是作为锁的符合条件,所以要打破第四个条件,不出现循环等待锁的关系

在开发中 注意加锁顺序 保证每个线程按照同样的顺序进行加锁

注意加锁时限,可以设置超时时间

注意死锁检查 这是一个预防机制保证发现死锁并解决

22.7如果提交任务 线程队列满了会怎样

 如果使用无界队列 可以继续添加

 如果使用有界队列 ,队列满了如果核心线程数没有达到上限,那么增加线程 ,如果线程数达到最大线程值,使用拒绝策略

22.8 如何查看线程死锁(了解)

 可以通过jstack命令 查看 会显示发生死锁的线程

 jps 查看进程pid  

jstack -i pid 查看其中线程信息

使用阿里的(arthas)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值