java面试题
1、volatile关键字
-
java虚拟机提供的轻量级的同步机制,具有三大特性:
-
保证可见性
一个线程修改了主内存中的值之后通知其他线程
-
不保证原子性
原因:写覆盖
如何解决:1、方法加sync修饰 2、使用java.util.current包下面的Automic提供的类
-
禁止指令重排
多线程环境中线程交替执行,由于编译器存在优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测,因此要禁止指令重排
-
-
谈谈JMM(java内存模型)
1、JMM本身是一种抽象的概念并不存在,他描述的是一种规范,通过这种规范定义了程序中各个变量的访问方式 。
2、JMM中同步的规定
- 线程解锁钱前,必须把变量刷新会主内存
- 线程加锁前,必须读取主内存最新的值到自己的工作内存
- 加锁解锁是同一把锁
3、由于JVM运行程序的实体是线程,而每个线程创建时都会为其创建一个工作内存(栈空间),工作内存是每个线程私有的数据区域,而java内存模型中规定所有的变量存储在主内存,主内存是共享区域,所有线程都可以访问,但是线程对变量的操作必须在自己的工作内存中进行,首先将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回内存,不能直接操作内存中的变量,各个线程的工作内存中存储的是主内存中变量的副本,因此不同的线程无法访问对方的工作内存,线程间的通信必须通过主内存来完成。
4、要求保证三种特性:可见性、原子性、有序性
-
哪些地方用到过?
在双重检查模式下的单例模式中为单例对象添加volatile关键字禁止指令重排以提高安全性
public Single{ private static volatile Sibgle instance = null; private Single(){ } public static Single getInstance(){ if(instance == null){ Synchronized(Single.class){ if(instance == null){ instance = new Single(); } } } return instance; } }
2、CAS
-
什么是CAS?
AutomicInteger类的一个方法,即比较交换
当一个线程要修改主内存中的值时会先将自己方法区的值和主内存中的值进行比较,如果一致则执行更新操作,如果不一致则不更新,同时把自己方法区的值更新为主内存中的值
AutomicInteger A = new AutomicInteger(); A.compareAndSet(expect,uodate);
-
CAS的底层原理?
CAS调用UnSafe类的方法进行自选,JVM会帮我们实现汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E7E2Yshn-1589639835282)(C:\Users\ps\AppData\Roaming\Typora\typora-user-images\image-20191207204543677.png)]
-
CAS的缺点?
1、循环时间长,开销大。getAndAddInt()方法执行是有个do while循环,如果CAS失败,会一直进行尝试。
2、只能保证一个共享变量的共享操作。对多个共享变量操作时,循环CAS无法保证操作的原子性,这个时候可以使用锁来保证原子性
3、引来ABA问题。CAS算法一个重要实现前提需要取出内存中某个时刻的数据并在当下时刻进行比较替换,那么在这个时间差内会导致主内存数据的变化,而当前线程对这个变化是透明的。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程没问题
解决办法:时间戳原子引用,AutomicStampedReference类
-
AutomicIntege为什么用CAS而不是sync?
sync加锁之后只允许一个线程访问内存中的资源,降低了并发效率,而CAS通过原子操作实现,不存在效率问题。
3、集合类不安全之并发修改异常
-
异常名称
java.util.concurrentModificainException
-
导致原因
并发争抢修改导致,参考生活中的例子:手写签到
-
解决方案
解决ArrayList不安全
1、ArrayList<>()使用new Vector<>();类进行解决
2、Collections.synchronizeList(new Arraylist<>());
3、new CopyOnWriteArrayList<>();
解决hashSet不安全
1、Collections.synchronizedSet(new HashSet<>())
2、new CopyOnWriteArrayAet<>();
面试题:hashSet的底层是hashMap,当value为null时默认为present常量
解决Map不安全
1、Collections.synchronizedMap(new HashMap<>())
2、ConcurrentHashMap<>()
-
优化建议
4、java锁
-
公平锁和非公平锁
公平锁:
非公平锁:
-
可重入锁(递归锁)
同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,再进入内层方法会自动获取锁,也就是说:线程可以进入任何一个他已经拥有的锁所同步着的代码块
常见的可重入锁;
ReentrantLock/Synchronized
作用:避免死锁
-
自旋锁
尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文的切换,缺点是会消耗CPU。自旋锁的基础是CAS。
-
独占锁(写锁)/共享锁(读锁)
独占锁:该锁一直只能被一个线程所持有,ReentrantLock/Synchronized都是独占锁
共享锁:该锁可以被多个线程持有
5、CountDownLatch、CyclicBarrier、Semaphore
6、阻塞队列
当阻塞队列是空的时候,从队列中获取元素的操作将会被阻塞
当阻塞队列是满的时候,往队列中插入元素的操作将会被阻塞
-
架构
Collection --> BlockingQueue(接口,拥有七大实现类)
-
核心方法
四种不同的操作类型,返回值和操作效果都不一样,具体如第二张表
-
用在哪里
1、生产者消费者
2、线程池
3、消息中间件
7、Sync和Lock的区别
-
原始构成
Sync是关键字,输入JVM层面,底层通过monitor对象来完成
Lock是具体的类,是 API层面
-
使用方法
Sync不需要用户手动释放锁,当sync代码执行完成后系统会自动释放
ReentrantLock需要用户手动释放锁,否则会出现死锁的现象
-
等待是否可中断
Sync不可中断,除非抛出异常或者运行完成
ReentrantLock可中断,既可以通过设置 超时方法,又可以调用interrupt()方法
-
加锁是否公平
Sync默认是非公公平锁
ReentrantLock两者都可以,根据传入的参数变化
-
绑定多个条件condition
Sync没有
ReentrantLock用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而Sync要么随机唤醒一个,要么全部唤醒
8、线程池
-
线程池的优势?/为什么使用线程池?
1、降低资源消耗。通过重复利川己创建的线秤降低线程创建和销毁造成的消耗。
2、提高响应速度。当仟务到达时,仃务可以不耑要的等到线程创建就能立即执行。
3、提高线秤的可管理性。线秤足稀缺资源,如果尤限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使川线池可以进
行统一的分配,调优和监控 -
如何使用?
public class Pool { public static void main(String[] args) { // 1、创建一个单线程化的线程池,它只会用唯- -的工作线程来执行任务,保证所有任务按照指定顺序执行。 // ExecutorService th = Executors.newFixedThreadPool(5); // 1、创建一个单线程化的线程池,它只会用唯一的线程执行任务,保证任务按照顺序执行 // ExecutorService th = Executors.newSingleThreadExecutor(); // 1、创建一个可缓存的线程池,如果线程池超过处理需求,可灵活回收空闲线程,若无回收,则新建线程 ExecutorService th = Executors.newCachedThreadPool(); try { for (int i = 0; i <10; i++) { th.execute(() -> { System.out.println(Thread.currentThread().getName() + "来了" ); }); } }catch (Exception e){ e.printStackTrace(); }finally { th.shutdown(); } } }
-
线程池的七大参数
1、corePoolSize:线程池中的常驻核心线程数,当线程池中的线程数目到达corePoolSize后,就会把到达的任务放到缓存队列中
2、maxNumPoolSize:线程池能够容纳同时执行的最大的线程数
3、keepAliveTime:多余线程的存活时间
4、unit:时间的单位
5、workQueue:阻塞队列,已提交但尚未被执行的任务
6、threadFactory:生成线程池中工作线程的工厂,用于创建线程
7、handler:拒绝策略,表示当阻塞队列满了并且工作线程大于等于线程池最大线程数时要采取的处理方式
-
底层工作原理
1、在创建了线程池后,等待提交过来的任务请求。
2、当调用execute()方法添加一个请求任务时,线程池会做如下判断:
2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务; 2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
2.3如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务; 2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做超过一-定的时间(keepAliveTime) 时,线程池会判断:如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。 -
拒绝策略(四种)
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
CallerRunPloicy:既不抛弃任务,也不会 抛出异常,而是将任务退回到调用者
DiscardOldestPolicy:抛弃队列中等待时间最长的任务,然后把当前任务加入队列尝试提交
DiscardPolicy:直接丢弃任务,不进行处理也不抛出异常
-
为什么要手写线程池?
具体查看阿里巴巴开发手册
-
如何合理配置参数?(最大线程数)
首先获取当前PC的核心数
1、CPU密集型
最大线程数:CPU核数+1个
2、IO密集型
最大线程数:CPU核数*2个
-
9、JVM
-
前提复习(详见JVM文档)
1、JVM内存结构
2、GC的作用域
3、常见的垃圾回收算法
-
怎么判断队对象是不是垃圾?
1、引用计数法
给对象添加一个计数器,有一个地方引用它计数器值+1,失去一个引用计数器-1,计数器为0时就是可回收对象。缺点:难以解决对象相互引用的问题
2、枚举根结点做可达性分析系(待配图)
通过一系列名为“GC Root”的对象为起点,从这里开始向下搜索,如果一个对象到GC Root没有任何引用链相连时,则说明此对象可回收
哪些节点对象可以作为GC Root?
- 虚拟机栈(栈针中的局部变量区)引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
-
JVM参数类型
1、标配参数
2、X参数(了解)
3、XX参数
-
布尔类型
+:添加某个参数
-:去除某个参数
-
KV设值类型
公式:-XX:Key=Value
-
-
如何查看当前程序的运行配置
1、第一种方式
查看进程号:jps -l
查看单个配置项:jinfo -flag 配置类型 进程号
查看所有配置项:jinfo -flags 进程号
2、第二种方式
查看所有初始参数:java -XX+:PrintFlagsInitial -version
查看被修改更新的参数:java -XX:PrintFlagFinal -version
-
面试中常答的参数
1、-Xms:初始内存大小,默认为物理内存的1/64
2、-Xmx:最大分配内存:默认为物理内存的1/4
3、-Xss:设置单个线程栈的大小,一般默认为512K-1024K
4、-Xmn:设置年轻代大小
5、-XX:MetaspaceSize:设置元空间大小(默认为20M)
6、-XX:PrintGCDetails:输出GC的详细收集日志信息
10、强、软、弱、虚引用
-
强引用(默认)
1、当内存不足,JVM开始垃圾回收,但是对于强引用对象,就算是出现了OOM也不会对该对象回收
2、强引用是我们最常见的普通对象引用,只要还有一个强引用指向一个对象,就标明对象还活着,垃圾回收器不会碰这类对象。在java中最常见的就是强引用,当把一个对象赋值给一个引用变量,这个引用变量就是一个强引用。
-
软引用
1、软引用是一种相对强引用弱化了的引用,需要时使用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。对于若引用来说:
当内存充足时-不会被回收
当内存耗尽时-会被回收
2、软引用通常用在对内存敏感的程序中,比如高速缓存
-
弱引用
无论内存是否够用,只要是弱引用,都会被回收
-
虚引用
1、虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,那么他就和没有引用一样,在任何时候都可能被回收,它不能单独使用也不能通过他访问对象,虚引用必须和引用队列一起使用。
2、虚引用的主要作用是跟踪对象被垃圾回收的状态。
11、OOM
-
常见的Error
1、java.lang.StatckOverflowError
方法调用过多导致
2、java.lang.OutOfMemeryError:Java hea
对象太多导致堆内存溢出
12、垃圾算法和垃圾回收器
-
GC算法(引用计数、复制、标记压缩、标记清除)是内存回收的方法论,垃圾收集器是算法的落地实现
-
目前为止没有完美的算法,更没有万能的收集器,只是针对具体应用场景进行分代收集
-
四种垃圾回收器
1、串行垃圾回收器(Serial)
它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程,不适合服务器环境
2、并行垃圾回收器(Parallel)
多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算等弱交互场景
3、并发垃圾回收器(CMS)
用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,适用于对响应时间有要求的场景
4、G1垃圾回收器
-
怎么查看服务器默认的垃圾收集器?
控制台输入:java -XX:+PrintCommandLineFlags -version
-
生产上如何配置垃圾收集器?
-
谈谈你对垃圾收集器的理解