1、简述线程、进程的基本概念。以及他们之间关系是什么
进程:是程序的一次执行的过程,是系统运行的基本单位,其中包含着程序运行过程中一些内存空间和系统资源。进程在运行过程中都是相互独立,但是线程之间运行可以相互影响。
线程:是进程的更小的单位。一个进程中包含着多个线程。和进程不同的是线程是共享着进程中的内存空间和系统资源的。所以在切换线程过程中开销要比进程小的多。
2、线程池
2.1 线程池的七个参数
- corePoolSize: 线程池核心线程数最大值
- maximumPoolSize: 线程池最大线程数大小
- keepAliveTime: 线程池中非核心线程空闲的存活时间大小
- unit: 线程空闲存活时间单位
- workQueue: 存放任务的阻塞队列
- threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
- handler: 线程池的饱和策略事件,主要有四种类型。
2.2 线程池的执行过程
当提交一个线程执行任务时候 ,先看有没有空闲的核心线程,如果有就执行,如果没有就看阻塞队列中有没有满的状态,如果没有放满则放入队列中,否则就直接看线程数量是否大于非核心线程数量,如果没有就直接创建一个非核心线程,否则就是按照指定的拒绝策略给处理。如果一个非核心线程在某个一段时间类是空闲的那么线程池就会把这个线程自动销毁掉。
2.3 四种拒绝策略
- AbortPolicy(抛出一个异常,默认的)
- DiscardPolicy(直接丢弃任务)
- DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
- CallerRunsPolicy(交给线程池调用所在的线程进行处理)
2.4 五种阻塞队列
- ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
- LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
- DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
- PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列;
- SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。
2.4 五种创建线程的方式池的方式
- newFixedThreadPool (固定数目线程的线程池)
- newCachedThreadPool(可缓存线程的线程池)
- newSingleThreadExecutor(单线程的线程池)
- newScheduledThreadPool(定时及周期执行的线程池)
- new ThreadPoolExecutor() 自定义的方式创建
2.5 使用无界队列的线程池会导致内存飙升吗?
会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升,最终导致OOM。
3、线程、进程、协程的区别
进程:进程是操作系统分配系统资源和内存空间的最小单位。进程是独立的一块空间,所以资源和内存空间的切换是特别消耗资源的。
线程:线程也叫做轻量级的进程,是操作系统调用执行的最小单位。线程的资源是依赖于他的父进程,所以他的资源是共享的,线程的切换需要转换到内核态开销相对于小一些。
协程:协程是一种轻量级的线程,协程是直接在用户态就可以控制,具有对内核态来说是不可见的,所以协程的上下文切换更加的节约资源消耗。
4、什么是上下文的切换
上下文的切换指的是CPU寄存器和程序计数器存储的数据进行切换,而内存数据的改变只有在内核态才能进行。所以上下文切换对系统来说是巨大的成本。
- 先是暂停原有的进程,保存好进程中寄存器中的数据在某个地方
- 内存中获取下一个进程上下文,存储到寄存器中
5、Java有几种创建线程的方式
- new 一个Thread
- 继承Runnable类
- 使用Callable
- 使用线程池
6、sleep和wait区别
sleep 属于 Thread类,wait属于Object类
wait会释放掉锁,sleep不会释放锁
wait必须在同步代码方法和同步代码块中,sleep没有这一个限制
wait()要调用notify()或notifyall()唤醒,sleep()自动唤醒
7、Java的内存模型
JMM屏蔽了各种硬件和操作系统的内存访问差异,实现让Java程序在各平台都能够达到一致的内存访问效果,它定义了Java如何将程序中的变量在主存中读取
具体定义:所有变量都在主存中,主存是线程的共享区域,每个线程都有自己独有的工作内存,线程想要操作变量必须从主存中copy一份到自己的工作区域,每个独立内存区域相互隔离。
所以这个时候读写存在延迟,且不是原子操作,所以就出现了一些列的线程安全操作。比如 原子性、可见性、有序性。
8、保证并发安全的三大特性?
- 原子性:一次或多次操作在执行期间不被其他线程影响
- 可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道(利用内存原子操作解决或者内存屏障或者lock前缀)
- 有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排(内存屏障)
9、ThreadLocal原理
每一个线程创建变量的副本,不同线程间是不可见的,保证线程安全。每个线程都维护一个ThreadLocalMap,key为threadlocal实例,value是保存的副本。
但是使用ThreadLocal会存在内存泄漏问题,因为key 是弱引用,value是强引用,每次GC时key都会回收,而value不会被回收。所以为了解决内存泄漏问题,可以在每次使用完后删除value或者使用static修饰ThreadLocal,可以随时的获取value。
第二个会出现内存溢出问题,如果使用的是线程池的方式去使用ThreadLocal话,那么就会出现存储的value一直存在,这样就一直堆积。
10、什么是CAS锁
CAS锁可以保证原子性,思想是更新内存是会判断其内存的值是否被修改了,如果没有被修改就直接更新,如果被修改了,就得重新去获取值,知道更新为止。这样是有缺点的:
- 只能支持一个变量的原子操作,不能保证整个代码块的原子操作
- CAS频繁的失败会造成CPU的开销打
- 会出现ABA问题
解决ABA问题,可以通过加入版本控制
11、Synchronized锁原理和优化
在JDK1.6以后Synchronized引入了偏向锁、轻量级锁、重量级锁、锁的粗化、锁消除的优化。并发性能基本和Lock持平的。
- 偏向锁:是不存竞争情况下,从而在后续的代码块中没有加锁解锁的开销
- 轻量级锁:轻量级锁所适应的场景是线程交替执行同步块的场合
- 重量级锁:重量级锁首先会经过一定次数的CAS自旋操作获取锁,如果获取失败,存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。是在竞争激烈的情况下创建一个monitor对象,并且将线程挂起,而挂起就要切换到内核状态执行。从而开销非常大。
轻量级锁:当锁是偏向锁时,有另外一个线程来访问,会撤销掉偏向锁,然后升级为轻量级锁,这个线程会通过自旋方式不断获取锁,不会阻塞,提高性能
重量级锁:轻量级锁尝试去获取到锁,如果获取失败线程就会进入阻塞状态,该锁会升级为重量级锁,重量级锁时,来竞争锁的所有线程都会阻塞,性能降低
注意,锁只能升级不能降级
12、synchronized和ReentrantLock的区别
实现方面:一个是JVM层次通过,(监视器锁)monitor实现的。一个是通过AQS实现的
相应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
锁的类型不同:synchronized 是非公平的锁,而ReentrantLock即可以是公平的也可以是非公平的
获取锁的方式不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
13、为什么AQS使用的双向链表
因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候就会用到prev和next方便删除掉中间的节点
14、有哪些常见的AQS类
- ReentrantLock 独占锁
- Semaphore 共享锁(限流有限资源)
- CountDownLatch 共享锁(合并汇总)
- CyclicBarrier 独占锁 + 条件队列
- ReentrantReadWriteLock 读写锁
15、死锁产生的4个必要条件
- 互斥条件:进程要求对所匹配的资源进行排他性控制,即是一段时间内某资源仅为一进程所占有
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源在未使用完之前不,不能剥夺,只能在使用完时有自己释放
- 环路等待条件:在发生死锁时,必须存在一个进程–资源得环星链
16、预防死锁
- 资源一次性得分配:一次性分配所有得资源,这样就不会有请求了(破坏请求条件);只要一个资源得不到分配,也不给这个进程分配其他得资源(破坏请求保持条件)
- 可剥夺条件:即当某个进程获得部分资源,得不到其他得资源,则释放已有得资源(破坏补课剥夺得条件)
- 资源有序分配法:系统分配资源编号,每一个进程按编号递增的顺序请求资源,释放则反之(环路破坏)
17、解除死锁
- 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
- 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
18、CPU密集型和IO密集型 的线程数设置
- CPU 密集型:对于这样的任务最佳的线程数为CPU核心数的1~2倍,如果设置过多的线程,就会造成不必要的CPU上下文切换,性能相对于比较低
- IO密集任务:由于IO读写是比较消耗时间的,并不会特别的消耗CPU的资源。对于这种任务最大的线程数一般大于CPU核心数的很多倍。
- 线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间