春招面试高频考点
-----------------------java线程相关---------------------------
一.java线程基本概念:
1.进程与线程的区别:(重点掌握)
· 进程是一个“执行中的程序”,是系统进行资源分配和调度的一个独立单位
· 线程是进程的一个实体,一个进程中一般拥有多个线程。线程之间共享地址空间和其它资源(所以通信和同步等操作,线程比进程更加容易)
· 线程一般不拥有系统资源,但是也有一些必不可少的资源(使用ThreadLocal存储)
· 线程上下文的切换比进程上下文切换要快很多。
那么线程可以拥有独属于自己的资源吗?
答:可以的,通过ThreadLocal(这个在接下来”java线程间通信”会给出解释)可以存储线程的特有对象,也就是属于当前线程的资源。
2. 线程的状态有哪些?(掌握)
线程的状态包括新建状态(NEW),运行状态(RUNNABLE),阻塞(BLOCKED)等待状态和消亡状态(TERMINATED)。其中阻塞等待状态又分为BLOCKED, WAITING和TIMED_WAITING状态。
· NEW:属于一个已经创建的线程,但是还没有调用start方法启动的线程所处的状态。
· RUNNABLE:该状态包含两种可能。有可能正在运行,或者正在等待CPU资源。总体上就是当我们创建线程并且启动之后,就属于Runnable状态
· BLOCKED: 当线程准备进入synchronized同步块或同步方法的时候需要申请一个监视器锁而进行的等待,会使线程进入BLOCKED状态。
· WAITING 该状态的出现是因为调用了Object.wait()或者Thread.join()或者LockSupport.park().处于该状态下的线程在等待另一个线程执行一些其余action来将其唤醒。
· TIMED_WAITING 该状态和WAITING差不多
· TERMINATED:线程执行结束了,run方法执行结束表示线程处于消亡状态了。
二.java线程间通信:
(这部分知识不管面试哪个大厂后端都是必备的知识点,即使没有真正的项目场景也要学会和理解,我是总结了多线程编程的时候线程间通信会用到的关键字以及期间所涉及到的知识)
1. java中线程间的通信方式包括一下几个关键字:
Synchronized,Threadlocal,ReentrantLock,Lock,CountDownLatch
① Synchronized? 与Lock对比?
首先那些说看过synchronized源码的基本都是大神,synchronized根本点不进去,你用CTRL键也进不去,想弄懂它的实现原理,我们只能通过看编译好的字节码文件;
我们搞个测试类
public class SynchronizedTest {
public void get() {
synchronized (this) {
System.out.println("小莉莉!");
}
}
}
然后看字节码文件(怎么看?? 这样看啦 idea => view => Show ByteCode)
基于对象的监视器(ObjectMonitor),我们在字节码文件里面可以看到,在同步方法执行前后,有两个指令,进入同步方法前monitorenter,方法执行完成后monitorexit;
我的理解是对象都有一个监视器ObjectMonitor,这个监视器内部有很多属性,比如当前等待线程数、计数器、当前所属线程等;
其中计数器属性就是用来记录是否已被线程占有,方法执行到monitorenter时,计数器+1,执行到monitorexit时,计数器-1,线程就是通过这个计数器来判断当前锁对象是否已被占用(0为未占用,此时可以获取锁);
加注释:一个synchronize锁会有两个monitorexit,这是保证synchronize能一定释放锁的机制,一个是方法正常执行完释放,一个是执行过程发生异常时虚拟机释放;(下图是在idea中通过idea => view => Show ByteCode 找到的)
--Synchronized与lock对比:
1、synchronized不需要手动释放锁,lock需要在锁用完后进行unlock;
2、synchronized只能是默认的非公平锁,lock可以指定使用公平锁或者非公平锁(解释什么是公平锁?什么是非公平锁? 面试官一定会问的,不理解公平锁就不能完全回答好synchronized);
3、lock提供的Condition(条件)可以指定唤醒哪些线程,而synchronized只能随机唤醒一个或者全部唤醒;
.公平调度方式:
按照申请的先后顺序授予资源的独占权;
当前获得锁的线程释放锁后,其它所有等待中的线程会按照来的顺序执行,不会造成锁竞争;
.非公平调度方式:
在该策略中,资源的持有线程释放该资源的时候,等待队列中一个线程会被唤醒,而该线程从被唤醒到其继续执行可能需要一段时间。在该段时间内,新来的线程(活跃线程)可以先被授予该资源的独占权。
如果新来的线程占用该资源的时间不长,那么它完全有可能在被唤醒的线程继续执行前释放相应的资源,从而不影响该被唤醒的线程申请资源。
当前获得锁的线程释放锁后,其它所有等待中的线程会全部参与锁竞争;
JVM对synchronized内部锁的调度:
JVM对内部锁的调度是一种非公平的调度方式,JVM会给每个内部锁分配一个入口集(Entry Set),用于记录等待获得相应内部锁的线程。当锁被持有的线程释放的时候,该锁的入口集中的任意一个线程将会被唤醒,从而得到再次申请锁的机会。被唤醒的线程等待占用处理器运行时可能还有其他新的活跃线程与该线程抢占这个被释放的锁.
我想说到这里,关于Synchronized,包括公平调度和非公平调度应该说清楚了吧?有没有懂啊,你说话啊你.
②Threadlocal?与Synchronized的不同
ThreadLocal中文名叫线程变量,它底层维护了一个map,key就是当前的ThreadLocal对象(可以理解为当前执行该段代码的线程),value就是你set的值,这个map保证了各个线程的数据互不干扰;
下面是源码:
--ThreadLocal与Synchronized的不同点:
ThreadLocal为解决并发编程提供了新的思路,synchronized是共享线程间的数据,而ThreadLocal是隔离线程间的数据
synchronized是利用锁的机制,使变量或代码块在某一时该只能被桶一个线程访问;而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
------------------But--------------------
使用ThreadLocal不当可能会引起内存泄漏:
造成原因:ThreadLocal没有外部强引用,在发生垃圾回收的时候,ThreadLocal会被当成垃圾给干掉,而ThreadLocal对象又是Map中的key,map的key没了,那对应的entry永远不会被访问到,就无法被回收,进而造成内存泄漏
解决方案:每次用完ThreadLocal都调用它的remove()方法清除数据
③ ReentrantLock?它与Synchronized的区别
概述:ReentrantLock是Lock的一个子类;
作用:用来给资源加锁,避免高并发造成的数据异常问题;
--与synchronized主要区别--
使用ReentrantLock 中的lock需要手动释放锁,可以指定公平锁或者非公平锁
ReentrantLock是显示锁,其提供了一些内部锁不具备的特性,但并不是内部锁的替代品。显式锁支持公平和非公平的调度方式,默认采用非公平调度。
synchronized内部锁简单,但是不灵活。显示锁(ReentrantLock)支持在一个方法内申请锁,并且在另一个方法里释放锁。显示锁(ReentrantLock)定义了一个tryLock()方法,尝试去获取锁,成功返回true,失败并不会导致其执行的线程被暂停而是直接返回false,即可以避免死锁。
下面是自己的理解和测试代码(不然我觉得只是把理论放在这里,你还是不懂,跟我说是不是这样啊,反正我自己是这样):
补充1:Volatile? --volatile关键字解决了什么问题?实现原理是什么?
----解决了什么问题:
1、保证了变量的可见性
2、禁止指令重排
比如i=i+1,单线程操作没问题,如果使用多线程,比如两个线程,执行这段代码后(i初始值为0),i应该等于2,但是如果不用volatile修饰变量i,结果会等于1,初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存
----实现原理
基于内存屏障(关于内存屏障,了解就行);
它确保指令重排序时不会把其后面的指令排到内存屏障前面,也不会把前面的指令排到内存屏障后面,总之一句话,他能保证指令按照我们希望的顺序执行;
补充2:CountDownLatch ??
CountDownLatch是JUC包下的一个并发编程工具,主要有两个方法,countDown和await,
* CountDownLatch底层维护了一个计数器,在实例化的时候设置,
* 当调用countDown方法时,计数器减一,如果计数器在减一前已经为0,那么什么都不会发生,
* 如果减一后变成0,则唤醒所有等待的线程;
* await方***使当前线程等待,直到计数器为0;
跟上面一样,我怕你不理解,我把我写的实例代码粘贴出来就理解了,就告诉我看懂了没有就行了,我问你懂了没啊?
2.多线程编程中的常用函数的比较和特性总结如下。
sleep和wait的区别:
sleep方法:
· 是Thread类的静态方法;
· 在任何地方使用;
· 睡眠不释放锁(如果有的话)。
· sleep必须捕获异常
wait方法:
· 是Object的方法;
· 必须与synchronized关键字一起使用;
· 睡眠时,会释放互斥锁。
· 而wait,notify和notifyAll不需要捕获异常
join方法:当前线程调用,则其它线程全部停止,等待当前线程执行完毕,接着执行。
yield方法:该方法使得线程放弃当前分得的CPU时间。但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得CPU时间。
--------===========线程池相关知识点&线程池实现================------
二. 线程池--相关
要理解实现原理,必须把线程池的几个参数彻底搞懂,不要死记硬背
线程池参数.
- - corePoolSize(必填):核心线程数。
- - maximumPoolSize(必填):最大线程数。
- - keepAliveTime(必填):线程空闲时长。如果超过该时长,非核心线程就会被回收。
- -unit(必填):指定keepAliveTime的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- - workQueue(必填):任务队列。通过线程池的execute()方法提交的Runnable对象将存储在该队列中。
- - threadFactory(可选):线程工厂。一般就用默认的。
- -handler(可选):拒绝策略。当线程数达到最大线程数时就要执行饱和策略。
说下核心线程数和最大线程数的区别
拒绝策略可选值
- - AbortPolicy(默认):放弃任务并抛出RejectedExecutionException异常。
- - CallerRunsPolicy:由调用线程处理该任务。
- - DiscardPolicy:放弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
- - DiscardOldestPolicy:放弃队列最早的未处理任务,然后重新尝试执行任务。
简短的总结下线程池执行流程:
1、一个任务提交到线程池后,如果当前的线程数没达到核心线程数,则新建一个线程并且执行新任务,注意一点,这个新任务执行完后,该线程不会被销毁;
2、如果达到了,则判断任务队列满了没,如果没满,则将任务放入任务队列;
3、如果满了,则判断当前线程数量是否达到最大线程数,如果没达到,则创建新线程来执行任务,注意,如果线程池中线程数量大于核心线程数,每当有线程超过了空闲时间,就会被销毁,直到线程数量不大于核心线程数;
4、如果达到了最大线程数,并且任务队列满了,就会执行饱和策略;
考点分析:
线程池几乎是一个必考的知识点,所以我们必须熟练掌握线程池的基本参数及其含义,并且对排队策略有清晰的理解。常见线程池的类型,包括其使用到的阻塞队列等。
四种现成的线程池(如果不想自己new的话)
1、定长线程池(FixedThreadPool)
特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
应用场景:控制线程最大并发数
2、定时线程池(ScheduledThreadPool)
特点:核心线程数量固定,非核心线程数量无限,执行完闲置10ms后回收,任务队列为延时阻塞队列。
应用场景:执行定时或周期性的任务。
3、可缓存线程池(CachedThreadPool)
特点:无核心线程,非核心线程数量无限,执行完闲置60s后回收,任务队列为不存储元素的阻塞队列。
应用场景:执行大量、耗时少的任务。
4、单线程化线程池(SingleThreadExecutor)
特点:只有1个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作、文件操作等。
上述四个线程池虽然方便,但是阿里巴巴规范明确说明不建议使用,因为可能会造成内存溢出,具体原因如下:
**FixedThreadPool和SingleThreadExecutor:**主要问题是堆积的请求处理队列均采用LinkedBlockingQueue,可能会耗费非常大的内存,严重的直接导致内存溢出。
**CachedThreadPool和ScheduledThreadPool:**主要问题是它们的最大线程数是Integer.MAX_VALUE,可能会创建数量非常多的线程,严重的直接导致内存溢出。
全部评论
(3) 回帖