1.创建线程的方式
继承Thead,实现Runnable或者Callable(有返回值)接口,通过线程池的方式创建。
常见有那些线程池的创建方式
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
自己提到了这几种线程池的创建方式都不安全,会发生内存溢出的问题。
解析链接:https://blog.csdn.net/qq_34707456/article/details/103066406
线程池的核心参数
ThreadPoolExecutor的构造方法了解一下这个类:上面四种创建线程池的方式最后都会调用new ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
构造参数比较多,一个一个说下:
corePoolSize:线程池中的核心线程数;
maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程;
keepAliveTime:线程池中非核心线程闲置超时时长(准确来说应该是没有任务执行时的回收时间,后面会分析);
一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉
如果设置allowCoreThreadTimeOut(boolean value),则也会作用于核心线程
TimeUnit:时间单位。可选的单位有分钟(MINUTES),秒(SECONDS),毫秒(MILLISECONDS) 等;
workQueue:任务的阻塞队列,缓存将要执行的Runnable任务,由各线程轮询该任务队列获取任务执行。可以选择以下几个阻塞队列。
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
ThreadFactory:线程创建的工厂。可以进行一些属性设置,比如线程名,优先级等等,有默认实现。
RejectedExecutionHandler:任务拒绝策略(饱和策略),当运行线程数已达到maximumPoolSize,队列也已经装满时会调用该参数拒绝任务,默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
AbortPolicy:直接抛出异常。
CallerRunsPolicy:这提供了一个简单的反馈控制机制,可以减慢提交新任务的速度。
DiscardOldestPolicy:丢弃队列里最老的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。
当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
jdk1.8中新增了那种线程池,还可以怎么创建线程?
新增线程池:newWorkStealingPool
适合使用在很耗时的操作,但是newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中。
新增创建线程方式:使用lambda方式创建
new Thread(()-> {
print.printNum();
}).start();
2.ThreadLocal了解吗,谈下你的理解
java就是通过ThreadLocal
来实现线程本地存储的,ThreadLocal
就是每个线程自己的本地变量。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
可以看到ThreadLocalMap
里面有个Entry
数组,只有数组没有像HashMap
那样有链表,因此当hash冲突的之后,ThreadLocalMap
是采用线性探测的方式解决hash冲突。
线性探测,就是先根据初始key
的hashcode
值确定元素在table
数组中的位置,如果这个位置上已经有其他key
值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次直至找到能够存放的位置。在ThreadLocalMap
步长是1。
用这种方式解决hash冲突的效率很低,因此要注意ThreadLocal的数量。
而且这个Entry
把ThreadLocal
的弱引用作为key。那为什么要搞成弱引用(只要发生了GC弱引用对象就会被回收)呢?
首先ThreadLocal
内部没有存储任何的值,它的作用只是当我们的ThreadLocalMap的key
,让线程可以拿到对应的value
。当我们不需要用这个key的时候我们,我们把fooLocal=null
这样强引用就没了。假设Entry里面也是强引用的话,那等于这个ThreadLocal
实例还有个强引用在,那么我们想让GC回收fooLocal
就回收不了了。那可能有人想,你弄成弱引用不是很危险啊,万一GC一下不是没了?别怕只要fooLocal
这个强引用在这个ThreadLocal
实例就不会回收的。
因此弄成弱引用,主要是让没用的ThreadLocal
得以GC清除。
这里可能还有人问那key清除掉了,value咋办,这个Entry还在的呀。是的,当在使用线程池的情况下,由于线程的生命周期很长,某些大对象的key被移除了之后,value一直存在的就可能会导致内存泄漏。
不过java考虑到这点了。当调用get()、set()
方法时会去找到那个key被干掉的entry然后干掉它。并且提供了remove()
方法。虽然get()、set()
会清理key
为null的Entry
,但是不是每次调用就会清理的,只有当get
时候直接hash没中,或者set
时候也是直接hash没中,开始线性探测时候,碰到key为null的才会清理。
ThreadLocal本质就是避免共享,在使用中注意内存泄露问题和hash碰撞问题即可。
3.volatile的作用
保证线程间的可见性,禁止重排序
volatile怎么保证线程间的可见性
线程直接读写主内存中的变量,不去读缓存中的变量。
volatile可以保证线程间的原子性吗,为什么
不能,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。
所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。
4.java的内存结构
堆,虚拟机栈,本地方法栈,程序计数器,元空间。
说下怎么判断对象是否存活
引用计数法(无法解决对象相互依赖的问题) 根搜索算法
说下常用的垃圾回收算法
新生代 :复制算法 老年代: 标记清除 标记整理
标记清除和标记整理的区别:
标记清除是判断一个对象不可达后就删除这个对象,是一个一个的删除,这样会产生大量的内存碎片,影响程序的效率。
标记整理会对不可达对象进行排序,把不可达对象放在一片连续的内存中,然后整体删除这片内存。
说下常用的垃圾回收器
cms g1
cms那些阶段会发生STW
初始标记 重新标记
g1收集器为什么可以设定回收的时间
g1回收器中,将新生代,老年代都划分成了很多的小块内存,可以根据设定的回收时间来回收一定数量的内存快。
5.java中的类加载机制
加载 链接 初始化 ,其中链接分为 验证 准备 解析
提到类加载时采用双亲委派模型,如果父类加载器可以加载就尝试交给父类加载器加载,然后父类加载器在尝试交给父类的父类加载器加载(这样依次往上,直到根加载器为止),如果父类加载器不能加载,子类加载器才尝试加载,这样可以保证一个类只被加载一次。
6.orcale和mysql的分页
mysql中通过使用limit m ,n 来实现分页,表示查询 m+1--m+n+1行 ,为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为 -1: limit m,-1
orcale中使用rownum来实现分页:rownum不支持大于,等于号,只支持小于号,如果想要查询中间的数据需要使用子查询
select * from (select rownum rm, a.* from (select * from table) a where rownum < 11) b where b.rm > 5
7.说下jdk1.8中的Stream流,怎么把array转化为流(直接回答不知道--说这个在工作中用的多)
8.说下rabbitmq的工作原理,用到了那些设计模式
谈了下rabbitmq的几种工作模式,主要谈了其中的路由模式,生产者将消息发给交换机,然后交换机根据路由键将 消息发给指定的消费队列。
用到了观察者模式,发布订阅模式。
9.说下令牌桶算法
这个主要是用来做服务的限流的,就是令牌桶中按一定的速率产生令牌(可以设置,m/s),服务请求的时候选取令牌桶中获取令牌,如果获取到就执行,在指定的时间没有获取到令牌就走服务的降级。
10.redis的持久化机制
谈了下aof和rdb的区别,aof实时记录,rdb是m时间n个key发生变化才记录一次。