java八股文之并发编程
1、线程和进程的区别
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
2、并行和并发的区别
现在都是多核CPU,在多核CPU下
- 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU,CPU需要进行切换
- 并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程互不干扰,CPU不需要进行切换
3、创建线程的方式有哪些
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池创建线程(项目中使用方式)
3.1 Runnable和Callable的区别是什么
- Runnable接口run方法没有返回值
- Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- 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 不同点
- sleep(long)是Thread的静态方法;而wait(),wait(long)都是Object的成员方法,每个对象都有
- wait()方法可以被notify和notifyAll唤醒,sleep()不行
- wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
- 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有什么异同?
相同:
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
不同:
- synchronized是关键字,源码在jvm中,用c++语言实现Lock是接口,源码由jdk提供,用java语言实现
- 使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁
- Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量(类似awit和notify)
- Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock(读写锁)
- 在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,会表现更好一点;在竞争激烈时,LOCK的实现通常会提供更好的性能
18.说一说死锁
- 死锁产生必须同时满足以下条件
- 互斥条件: 资源不可共享,必须由一个进程/线程独占使用。
- 请求与保持条件: 进程已持有资源,但仍请求新资源,且不释放已有资源。
- 不可剥夺条件: 资源必须由持有它的进程主动释放,不能被强制剥夺。
- 循环等待条件: 存在一个进程链,每个进程都在等待下一个进程持有的资源,最终形成环路。
- 诊断是否发生死锁的方法
- 使用jdk自带的工具:jps和jstack。
– jps: 输出JVM中运行的进程状态信息;
– jstack: 查看java进程内线程的堆栈信息,查看日志,检查是否有死锁,如果有死锁现象,需要查看具体代码分析后,可修复) - 可视化工具jconsole、VisualVM也可以检查死锁问题
- 使用jdk自带的工具:jps和jstack。
19、聊一下ConcurrentHashMap
- 底层数据结构:
– DK1.7底层采用分段的数组+链表实现
– JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树 - 加锁的方式
– JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
– JDK1.8采用CAS添加新节点,采用synchronizeds锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
20、java程序中,如何保证多线程安全
java并发编程中有三大特征:
- 原子性:一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行。可以用synchronized、lock保证。
- 内存可见性:让一个线程对共享变量的修改对另一个线程可见。可以用。可以用volatile、synchronized、lock方式保证。
- 有序性:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顶序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执的结果是一致的。可以用volatile保证
21、说一说线程池的核心参数
- 核心线程数(corePoolSize): 线程池中常驻的线程数量,即使空闲也不会被销毁(除非设置允许核心线程超时)。
- 最大线程数(maximumPoolSize): 线程池允许存在的最大线程数,当任务量激增且队列已满时,会创建临时线程直到达到此值。
- 空闲线程存活时间(keepAliveTime): 非核心线程(临时线程)在空闲时的存活时间,超时后会被销毁。
- 工作队列(workQueue): 存放等待执行任务的队列,队列的类型影响会线程池的行为,例如:
– 有界队列(如ArrayBlockingQueue):限制任务数量,防止内存溢出。
– 无界队列(如LinkedBlockingQueue):一般用的就是这个,但是使用时一般会指定最大参数。如果不指定直接创建,任务就会堆到int最大值。
– 特殊队列(如SynchronousQueue(无容量),直接触发创建新线程。 - 线程工厂(threadFactory): 自定义线程的创建方式,例如设置线程名称、优先级或线程组。
- 拒绝策略(handler): 当任务无法提交(线程数已达maximumPoolSize且队列已满)时的处理方式:
– AbortPolicy(默认):直接抛出RejectedExecutionException异常。
– CallerRunsPolicy:由提交任务的线程自己执行(避免新任务被丢弃)。
– DiscardPolicy:直接丢弃任务,不报错。
– DiscardOldestPolicy:丢弃队列中最老的任务,尝试提交新任务。 - 时间单位(unit): 与keepAliveTime配合,指定存活时间的单位(如秒、分钟等)。
22、如何确定核心线程数
- 高并发、任务执行时间短→(CPU核数+1),减少线程上下文的切换
- 并发不高、任务执行时间长
– IO密集型的任务→(CPU核数*2+1):文件读写,DB读写,网络请求等
– 计算密集型任务→(CPU核数+1):计算型代码、Bitmap转换、Gson转换等
// 获取CPU核数的代码int core = Runtime.getRuntime().availableProcessors();
23、线程池种类有哪些
注:所有种类本质为线程池的默认创建,得到不同的侧重的线程池。
- newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
- newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行
- newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
- newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行
24、为什么不建议用Executorst创建线程池
- FixedThreadPool、SingleThreadPool:
允许的请求队列长度为Integer.MAX VALUE,可能会堆积大量的请求,从而导致OOM。 - CachedThreadPool、newScheduledThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
25、如何控制某个方法允许并发访问线程的数量
在多线程中提供了一个工具类Semaphore,信号量。在并发的情况下,可以控制方法的访问量
- 创建Semaphore对象,可以给一个容量
- acquire()可以请求一个信号量,这时候的信号量个数-1
- release()释放一个信号量,此时信号量个数+1
26、谈谈你对ThreadLocal的理解
内存泄漏 : 无用对象因被引用而无法回收,最终撑爆内存。
- ThreadLocal可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】避免竞争用引发的线程安全问题
- ThreadLocal同时实现了线程内的资源共享
- 每个线程内有一个ThreadLocalMap类型的成员变量,用来存储资源对象
– 调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
– 调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值
– 调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值 - ThreadLocalp会有内存泄漏问题。ThreadLocalMap中的key是弱引用,值为强t引用;key会被GC释放内存,关联value的内存并不会释放。解决方式是主动remove释放key,value