转转一面
-
你了解线程池吗?都有哪几种线程池?线程池初始化都有哪些参数?有几种拒绝策略?默认是哪一种?
-
1、newCachedThreadPool(),它是用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置时间超过60秒,则被终止并移除缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列。
2、newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动线程数目,将在工作队列中等待空闲线程出现;如果工作线程退出,将会有新的工作线程被创建,以补足指定数目nThreads。
3、newSingleThreadExecutor(),它的特点在于工作线程数目限制为1,操作一个无界的工作队列,所以它保证了所有的任务都是被顺序执行,最多会有一个任务处于活动状态,并且不予许使用者改动线程池实例,因此可以避免改变线程数目。
4、newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
5、newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
-
int corePoolSize => 该线程池中核心线程数最大值
int maximumPoolSize 该线程池中线程总数最大值
long keepAliveTime 该线程池中非核心线程闲置超时时长
TimeUnit unit keepAliveTime的单位
BlockingQueue<Runnable> workQueue 该线程池中的任务队列:维护着等待执行的Runnable对象
ThreadFactory threadFactory 创建线程的方式,这是一个接口,你new他的时候需要实现他的Thread newThread(Runnable r)方法,
RejectedExecutionHandler handler 抛出异常专用的
拒绝策略
-
第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出异常 RejectedExecutionException (属于RuntimeException),让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
-
第二种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
-
第三种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
-
第四种拒绝策略是
CallerRunsPolicy相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
- 第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
- 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
四种拒绝策略
1.AbortPolicy: 丢弃任务,并抛出异常信息,线程池默认地拒绝策略,适用于比较关键地业务。(默认)
2.DiscardPolicy:丢弃任务,不抛出异常,啥也不干,适用于无关紧要业务,如文章阅读量。
3.DiscardOldestPolicy: 丢弃阻塞队列中最老的任务,并将新任务加入,是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
4.CallerRunsPolicy: 由调用线程直接运行该任务,适用于并发量比较小,性能要求不高,不允许失败业务。
-
java中有哪几种锁?分别有什么特点?
-
偏向锁/轻量级锁/重量级锁,这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态
偏向锁
如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好轻量级锁
JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞重量级锁
重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态锁升级的路径:无锁→偏向锁→轻量级锁→重量级锁
综上所述,偏向锁性能最好,可以避免执行 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差
-
可重入锁/非可重入锁
可重入锁指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。同理,不可重入锁指的是虽然线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取对于可重入锁而言,最典型的就是 ReentrantLock 了,正如它的名字一样,reentrant 的意思就是可重入,它也是 Lock 接口最主要的一个实现类
共享锁/独占锁
共享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有公平锁/非公平锁
公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。而非公平锁就不那么“完美”了,它会在一定情况下,忽略掉已经在排队的线程,发生插队现象悲观锁/乐观锁
悲观锁的概念是在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我。而乐观锁恰恰相反,它并不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改自旋锁/非自旋锁
自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。相反,非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等可中断锁/不可中断锁
在 Java 中,synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开
-
-
数据库索引都有哪些?索引回表和覆盖是什么?
-
唯一索引:在创建唯一索引时要不能给具有相同的索引值。
主键索引:在我们给一个字段设置主键的时候,它就会自动创建主键索引,用来确保每一个值都是唯一的。
聚集索引:我们在表中添加数据的顺序,与我们创建的索引键值相同,而且一个表中只能有一个聚集索引。
普通索引:它的结构主要以B+树和哈希索引为主,主要是对数据表中的数据进行精确查找。
全文索引:它的作用是搜索数据表中的字段是不是包含我们搜索的关键字,就像搜索引擎中的模糊查询。 -
InnoDB有两大类索引:
- 聚集索引(clustered index)
- 普通索引(secondary index)
-
InnoDB聚集索引的叶子节点存储行记录,因此, InnoDB必须要有,且只有一个聚集索引:
-
InnoDB普通索引的叶子节点存储主键值
-
普通索引无法直接定位行记录,需要扫码两遍索引树
-
(1)先通过普通索引定位到主键值id=5;
(2)在通过聚集索引定位到行记录;
这就是所谓的回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。
-
- 索引覆盖:只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。
- 都能够命中索引覆盖,无需回表。
- 能够命中name索引,索引叶子节点存储了主键id,但sex字段必须回表查询才能获取到,不符合索引覆盖,需要再次通过id值扫码聚集索引获取sex字段,效率会降低。
-
- java中synchronized锁升级是什么?
- 同上
5.序列化和反序列化
1.可序列化的方法有多少?如果没有方法,那么可序列化接口的用途是什么?
可序列化Serializalbe接口存在于java.io包中,构成了Java序列化机制的核心。它没有任何方法,在Java中也称为标记接口。当类实现java.io.Serializable接口时,它将在Java中变得可序列化,并指示编译器使用Java序列化机制序列化此对象
2.什么是serialVersionUID?如果你不定义这个,会发生什么?
它通常是对象的哈希码,SerialVerionUID用于对象的版本控制。也可以在类文件中指定serialVersionUID。不指定serialVersionUID的后果是,当你添加或修改类中的任何字段时,则已序列化类将无法恢复,因为为新类和旧序列化对象生成的serialVersionUID将有所不同。
3.序列化时,你希望某些成员不要序列化?你如何实现它?
声明它静态或瞬态根据你的需要,这样就不会是在Java序列化过程中被包含在内。
4.如果类中的一个成员未实现可序列化接口,会发生什么情况?
如果尝试序列化实现可序列化的类的对象,但该对象包含对不可序列化类的引用,则在运行时将引发不可序列化异常NotSerializableException
5.如果类是可序列化的,但其超类不是,则反序列化后从超级类继承的实例变量的状态如何?
Java序列化过程仅在对象层次都是可序列化结构中继续,即实现Java中的可序列化接口,并且从超级类继承的实例变量的值将通过调用构造函数初始化,在反序列化过程中不可序列化的超级类。一旦构造函数链接将启动,就不可能停止,因此,即使层次结构中较高的类实现可序列化接口,也将执行构造函数
6.是否可以自定义序列化过程,或者是否可以覆盖Java中的默认序列化过程?
答案是肯定的,你可以。对于序列化一个对象需调用ObjectOutputStream.writeObject(saveThisObject),并用ObjectInputStream.readObject()读取对象,但Java虚拟机为你提供的还有一件事,是定义这两个方法。如果在类中定义这两种方法,则JVM将调用这两种方法,而不是应用默认序列化机制。你可以在此处通过执行任何类型的预处理或后处理任务来自定义对象序列化和反序列化的行为。
7.假设新类的超级类实现可序列化接口,如何避免新类被序列化?
为了避免Java序列化,你需要在类中实现writeObject()和readObject()方法,并且需要从该方法引发不序列化异常NotSerializableException
8.我们可以通过网络传输一个序列化的对象吗?
是的,你可以通过网络传输序列化对象,因为Java序列化对象仍以字节的形式保留,字节可以通过网络发送。你还可以将序列化对象存储在磁盘或数据库中作为Blob。
9.在Java序列化期间,哪些变量未序列化?
静态和瞬态变量,由于静态变量属于类,而不是对象,因此它们不是对象状态的一部分,因此在Java序列化过程中不会保存它们。由于Java序列化仅保留对象的状态,而不是对象本身。瞬态变量也不包含在Java序列化过程中,并且不是对象的序列化状态的一部分
10逃逸分析技术
使用逃逸分析,编译器可以对代码做如下优化:
一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。