Java面试
2020-08-28 13:46:46 0 举报
AI智能生成
Java面试题
作者其他创作
大纲/内容
简历制作
参考模板1
参考模板2
模板3
JUC多线程及并发包
1.谈谈你对volatile的理解
1.volatile是Java虚拟机提供的轻量级的同步机制
1.1保证可见性
1.2不保证原子性
1.3禁止指令重排
2.JMM你谈谈
2.1可见性
2.2原子性
number++在多线程下是非线程安全的,如何不加synchronized解决?
2.3VolatileDemo代码演示可见性+原子性代码
2.4有序性
重排1
重排2
案例
禁止指令重排小总结(了解)
3.你在哪些地方用到过volatile?
3.1 单例模式DCL代码
3.2代理模式volatile分析
2.CAS你知道吗
1.比较并交换
2.CAS底层原理?如果知道,谈谈你对UnSafe的理解
atomicInteger.getAndIncrement();
UnSafe
CAS是什么
unSafe.getAndIncrement
底层汇编
简单版小总结
3.CAS缺点
循环时间长开销很大
只能保证一个共享变量的原子性
引出来ABA问题???
3.原子类AtomicInteger的ABA问题谈谈?原子更新引用知道吗
ABA问题的产生
原子引用
AtomicReferenceDemo
时间戳原子引用
AtomicStampedReference
ABADemo
4.我们知道ArrayList是线程不安全,请编写一个不安全的案例并给出解决方案
解决方案1
ContainerNotSafeDemo
限制不可以使用vector和Collections工具类解决方案2
List线程copyOnWriteArrayList
set线程CopyOnwriteHashSet
map线程ConcurrentHashMap
5.公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁
公平锁和非公平锁
是什么
两者的区别
题外话
可重入锁(又名递归锁)
是什么
ReentrantLock/synchronized就是一个典型的可重入锁
可重入锁最大的作用就是避免死锁
ReenterLockDemo
参考1
参考2
自旋锁
独占锁(写)/共享锁(读)/互斥锁
ReadWriteLockDemo
读写锁
6.CountDownLatch/CyclicBarrier/Semaphore使用过吗?
CountDownLatch
让一些线程阻塞直到另外一些完成后才被唤醒
CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞.其他线程调用countDown方法计数器减1(调用countDown方法时线程不会阻塞),当计数器的值变为0,因调用await方法被阻塞的线程会被唤醒,继续执行
CountDownLatchDemo
关门案例
枚举的使用
秦灭六国
CyclicBarrier
CyclicBarrier的字面意思是可循环(Cyclic) 使用的屏障(barrier).它要做的事情是,让一组线程到达一个屏障(也可以叫做同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法.
CyclicBarrierDemo
集齐7颗龙珠就能召唤神龙
代码
Semaphore
信号量的主要用户两个目的,一个是用于多喝共享资源的相互排斥使用,另一个用于并发资源数的控制.
SemaphoreDemo
抢车位
代码
7.阻塞队列知道吗?
队列+阻塞队列
为什么用?有什么好处?
BlockingQueue的核心方法
架构梳理+种类分析
架构介绍
种类分析
ArrayBlockingQueue: 由数组结构组成的有界阻塞队列.
LinkedBlockingDeque: 由链表结构组成的有界(但大小默认值Integer>MAX_VALUE)阻塞队列.
PriorityBlockingQueue:支持优先级排序的无界阻塞队列.
DelayQueue: 使用优先级队列实现的延迟无界阻塞队列.
SynchronousQueue:不存储元素的阻塞队列,也即是单个元素的队列.
理论
SynchronousQueueDemo
LinkedTransferQueue:由链表结构组成的无界阻塞队列.
LinkedBlockingDeque:由了解结构组成的双向阻塞队列.
用在哪里
生产者消费者模式
传统版
ProdConsumerTraditionDemo
阻塞队列版
ProdConsumerBlockQueueDemo
线程池
消息中间件
8.线程池用过吗?ThreadPoolExecutor谈谈你的理解?
为什么使用线程池,优势
线程池如何使用?
架构实现
编码实现
了解
Executors.newCachedThreadPool();
java8新出
Executors.newWorkStealingPool(int);
java8新增,使用目前机器上可以的处理器作为他的并行级别
重点
Executors.newFixedThreadPool(int)
执行一个长期的任务,性能好很多
Executors.newSingleThreadExecutor()
一个任务一个线程执行的任务场景
Executors.newCachedThreadPool()
适用:执行很多短期异步的小程序或者负载较轻的服务器
ThreadPoolExecutor
线程池几个重要参数介绍?
7大参数
1.corePoolSize:线程池中的常驻核心线程数
2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值大于等于1
3.keepAliveTime:多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止
4.unit:keepAliveTime的单位
5.workQueue:任务队列,被提交但尚未被执行的任务.
6.threadFactory:表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可
7.handler:拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示 数(maxnumPoolSize)时如何来拒绝.
说说线程池的底层工作原理?
9.线程池用过吗?生产上你是如何设置合理参数
线程池的拒绝策略请你谈谈
是什么
JDK内置的拒绝策略
AbortPolicy(默认):直接抛出RejectedException异常阻止系统正常运行
CallerRunPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交
DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常.如果允许任务丢失,这是最好的拒绝策略
以上内置策略均实现了RejectExecutionHandler接口
你在工作中单一的/固定数的/可变你的三种创建线程池的方法,你用哪个多?超级大坑
答案是一个都不用,我们生产上只能使用自定义的
Executors中JDK给你提供了为什么不用?
你在工作中是如何创建线程池的,是否自定义过线程池使用
Case
合理配置线程池你是如何考虑的?
CPU密集型
IO密集型
1
2
10.死锁编码及定位分析
是什么
代码
解决
jps命令定位进程编号
jstack找到死锁查看
11.调用yield() 、sleep()、wait()、notify()等方法对锁有何影响?
yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。
调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。
调用notify()系列方法后,对锁无影响,线程只有在syn同步代码执行完后才会自然而然的释放锁,所以notify()系列方法一般都是syn同步代码的最后一行。
12.HashMap 和 HashTable 有什么区别?
①、HashMap 是线程不安全的,HashTable 是线程安全的;
②、由于线程安全,所以 HashTable 的效率比不上 HashMap;
③、HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable 不允许;
④、HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;
⑤、HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode
13.Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同?
ConcurrentHashMap 类(是 Java并发包 java.util.concurrent 中提供的一个线程安全且高效的 HashMap 实现)。
HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁);
而针对 ConcurrentHashMap,在 JDK 1.7 中采用分段锁的方式;JDK 1.8 中直接采用了CAS(无锁算法)+ synchronized,也采用分段锁的方式并大大缩小了锁的粒度。
HashMap & ConcurrentHashMap 的区别?
除了加锁,原理上无太大区别
另外,HashMap 的键值对允许有null,但是ConCurrentHashMap 都不允许。
在数据结构上,红黑树相关的节点类
在数据结构上,红黑树相关的节点类
14.为什么 ConcurrentHashMap 比 HashTable 效率要高?
HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞
ConcurrentHashMap
JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。
JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结点)(实现 Map.Entry)。锁粒度降低了。
15.针对 ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)?
JDK 1.7 中,采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。
①、Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶;
②、HashEntry 用来封装映射表的键-值对;
③、每个桶是由若干个 HashEntry 对象链接起来的链表。
JDK 1.8 中,采用Node + CAS + Synchronized来保证并发安全。取消类 Segment,直接用 table 数组存储键值对;当 HashEntry 对象组成的链表长度超过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。
17.ConcurrentHashMap 简单介绍?
①、重要的常量:
private transient volatile int sizeCtl;
当为负数时,-1 表示正在初始化,-N 表示 N - 1 个线程正在进行扩容;
当为 0 时,表示 table 还没有初始化;
当为其他正数时,表示初始化或者下一次进行扩容的大小。
②、数据结构:
Node 是存储结构的基本单元,继承 HashMap 中的 Entry,用于存储数据;
TreeNode 继承 Node,但是数据结构换成了二叉树结构,是红黑树的存储结构,用于红黑树中存储数据;
TreeBin 是封装 TreeNode 的容器,提供转换红黑树的一些条件和锁的控制。
③、存储对象时(put() 方法):
1.如果没有初始化,就调用 initTable() 方法来进行初始化;
2.如果没有 hash 冲突就直接 CAS 无锁插入;
3.如果需要扩容,就先进行扩容;
4.如果存在 hash 冲突,就加锁来保证线程安全,两种情况:一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入;
5.如果该链表的数量大于阀值 8,就要先转换成红黑树的结构,break 再一次进入循环
6.如果添加成功就调用 addCount() 方法统计 size,并且检查是否需要扩容。
④、扩容方法 transfer():默认容量为 16,扩容时,容量变为原来的两倍。
helpTransfer():调用多个工作线程一起帮助进行扩容,这样的效率就会更高。
helpTransfer():调用多个工作线程一起帮助进行扩容,这样的效率就会更高。
⑤、获取对象时(get()方法)
1.计算 hash 值,定位到该 table 索引位置,如果是首结点符合就返回;
2.如果遇到扩容时,会调用标记正在扩容结点 ForwardingNode.find()方法,查找该结点,匹配就返回
3.以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回 null。
18.ConcurrentHashMap 的并发度是什么?
1.7中程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。默认为 16,且可以在构造函数中设置。当用户设置并发度时,ConcurrentHashMap 会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)
1.8中并发度则无太大的实际意义了,主要用处就是当设置的初始容量小于并发度,将初始容量提升至并发度大小。
19.ConcurrentSkipListMap和TreeMap的不同
ConcurrentSkipListMap是线程安全的有序的哈希表,适用于高并发的场景
ConcurrentSkipListMap和TreeMap,它们虽然都是有序的哈希表。但是,第一,它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。
在非多线程的情况下,应当尽量使用TreeMap。此外对于并发性相对较低的并行程序可以使用Collections.synchronizedSortedMap将TreeMap进行包装,也可以提供较好的效率。对于高并发程序,应当使用ConcurrentSkipListMap,能够提供更高的并发度。
20.ConcurrentSkipListMap 和 ConcurrentHashMap 不不同
所以在多线程程序中,如果需要对Map的键值进行排序时,请尽量使用ConcurrentSkipListMap,可能得到更好的并发度。
在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。
但ConcurrentSkipListMap有几个ConcurrentHashMap 不能比拟的优点:
1、ConcurrentSkipListMap 的key是有序的。
2、ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。
所以在多线程程序中,如果需要对Map的键值进行排序时,请尽量使用ConcurrentSkipListMap,可能得到更好的并发度。
注意,调用ConcurrentSkipListMap的size时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素个数,这个操作是个O(log(n))的操作。
21.ArrayBlockingQueue实现和LinkedBlockingQueue实现的区别
1. 队列中锁的实现不同
ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁;
LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock
2. 在生产或消费时操作不同
ArrayBlockingQueue实现的队列中在生产和消费的时候,是直接将枚举对象插入或移除的;
LinkedBlockingQueue实现的队列中在生产和消费的时候,需要把枚举对象转换为Node进行插入或移除,会影响性能
3. 队列大小初始化方式不同
ArrayBlockingQueue实现的队列中必须指定队列的大小;
LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE
22.在java中守护线程和用户线程的区别?
java中的线程分为两种:守护线程(Daemon)和用户线程(User)。
任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。
两者的区别:
唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经结束,Daemon 没有可服务的线程,JVM关闭。
扩展:Thread Dump打印出来的线程信息,含有daemon字样的线程即为守护进程
23.线程与进程的区别
进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。
一个程序至少有一个进程,一个进程至少有一个线程。
24.什么是多线程中的上下文切换
多线程会共同使用一组计算机上的CPU,而线程数大于给程序分配的CPU数量时,为了让各个线程都有执行的机会,就需要轮转使用CPU。不同的线程切换使用CPU发生的切换数据等就是上下文切换。
25.死锁与活锁的区别,死锁与饥饿的区别?
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
产生死锁的必要条件:
互斥条件:所谓互斥就是进程在某一时间内独占资源。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
26.synchronized底层实现原理
synchronized (this)原理:涉及两条指令:monitorenter,monitorexit;再说同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。
JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
注意,这个问题可能会接着追问,java对象头信息,偏向锁,轻量锁,重量级锁及其他们相互间转化。
27.什么是线程组,为什么在Java中不推荐使用?
ThreadGroup类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。
1.线程组ThreadGroup对象中比较有用的方法是stop、resume、suspend等方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方废弃掉了,所以线程组本身的应用价值就大打折扣了。
2.线程组ThreadGroup不是线程安全的,这在使用过程中获取的信息并不全是及时有效的,这就降低了它的统计使用价值。
28.什么是Executors框架?为什么使用Executor框架?
Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。
每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。
调用 new Thread()创建的线程缺乏管理,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
29.在Java中Executor和Executors的区别?
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor 接口对象能执行我们的线程任务。
ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
使用ThreadPoolExecutor 可以创建自定义线程池。
30.什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?
原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作
在Java中可以通过锁和循环CAS的方式来实现原子操作。 CAS操作——Compare & Set,或是 Compare & Swap,现在几乎所有的CPU指令都支持CAS的原子操作。java.util.concurrent.atomic下提供了大量的原子操作类,比如原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference ,原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray ,原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
31.Java Concurrency API中的Lock接口(Lock interface)是什么?对比synchronized它有什么优势?
Lock接口比同步方法和同步块提供了更具扩展性的锁操作。
他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
它的优势有:可以使锁更公平,可以使线程在等待锁的时候响应中断,可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间,可以在不同的范围,以不同的顺序获取和释放锁。
整体上来说Lock是synchronized的扩展版,Lock提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition方法)锁操作。另外Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
32.什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
JDK7提供了7个阻塞队列。在实现上,主要是利用了Condition和Lock的等待通知模式。
33.什么是Callable和Future?
Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。
可以认为是带有回调的Runnable。
Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。
34.什么是FutureTask?
在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。
35.什么是并发容器的实现?
何为同步容器:可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList等方法返回的容器。
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。
36.多线程同步和互斥有几种实现方法,都是什么?
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
37.什么是竞争条件?
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件(race condition)
38.为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。
但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码,只会把run方法当作普通方法去执行。
39.在Java中CycliBarriar和CountdownLatch有什么区别?
CyclicBarrier可以重复使用,而CountdownLatch不能重复使用。
40.什么是不可变对象,它对写并发应用有什么帮助?
不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
不可变对象的类即为不可变类(Immutable Class)。Java平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。
不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法修改,这些常量永远不会变。
不可变对象永远是线程安全的。
只有满足如下状态,一个对象才是不可变的;
它的状态不能在创建后再被修改;
所有域都是final类型;并且,
它被正确创建
它被正确创建
41.notify()和notifyAll()有什么区别?
当一个线程进入wait之后,就必须等其他线程notify/notifyall,使用notifyall,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。
如果没把握,建议notifyAll,防止notigy因为信号丢失而造成程序异常。
42.什么是可重入锁(ReentrantLock)?谈谈它的实现。
线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,synchronized、ReentrantLock都是可重入的锁。在实现上,就是线程每次获取锁时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每 释放一次锁,进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。
43.当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?
如果其他方法没有synchronized的话,其他线程是可以进入的。
所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的。
所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的。
44.乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java里面的同步原语synchronized关键字的实现是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。在Java中j原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁的实现方式:
使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
45.什么是CAS操作,缺点是什么?
CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。
CAS缺点
ABA问题
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
循环时间长开销大
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
46.SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为map。
ConcurrentHashMap使用分段锁来保证在多线程下的性能。
47.写时复制容器可以用于什么应用场景?
CopyOnWrite并发容器用于对于绝大部分访问都是读,且只是偶尔写的并发场景。比如白名单,黑名单,商品类目的访问和更新场景。
读写分离,读和写分开
最终一致性
使用另外开辟空间的思路,来解决并发冲突
48.volatile有什么用?能否用一句话说明下volatile的应用场景?
volatile保证内存可见性和禁止指令重排
volatile用于多线程环境下的一写多读,或者无关联的多写。
49.为什么代码会重排序?
在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
在单线程环境下不能改变程序运行的结果;
存在数据依赖关系的不允许重排序
50.在java中wait和sleep方法的不同?
最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。
51.一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行。hread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。
52.为什么wait, notify 和 notifyAll这些方法不在thread类里面?
JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
53.什么是ThreadLocal变量?
ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。
54.Java中interrupted 和 isInterrupted方法的区别?
interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。
55.为什么wait和notify方法要在同步块中调用?
主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。
56.为什么你应该在循环中检查等待条件?
处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用wait()方法效果更好的原因
57.怎么检测一个线程是否拥有锁?
在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。
58.你如何在Java中获取线程堆栈?
kill -3 [java pid]
不会在当前终端输出,它会输出到代码执行的或指定的地方去。比如,kill -3 tomcat pid, 输出堆栈到log目录下。
Jstack [java pid]
这个比较简单,在当前终端显示,也可以重定向到指定文件中。
使用Java提供的拟机线程系统的管理接口
ManagementFactory.getThreadMXBean()。
59.Java线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中。
而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口
60.你如何确保main()方法所在的线程是Java 程序最后结束的线程?
可以使用Thread类的join()方法(或者CountDownLatch工具类)来确保所有程序创建的线程在main()方法退出前结束。
61.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
可以用join方法实现。
62.你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?
volatile关键字,读写锁,写时复制等等都可以实现。
63.用Java写代码来解决生产者——消费者问题。
阻塞队列实现即可,也可以用wait和notify来解决这个问题,或者用Semaphore
64. Java中如何停止一个线程?
使用共享变量的方式
在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。
使用interrupt方法终止线程
如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?比如当一个线程由于需要等候键盘输入而被阻塞,或者调用Thread.join()方法,或者Thread.sleep()方法,在网络中调用ServerSocket.accept()方法,或者调用了DatagramSocket.receive()方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态时,即使主程序中将该线程的共享变量设置为true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。所以应该尽量使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态。
65.JVM中哪个参数是用来控制线程的栈堆栈大小的
-Xss
66.写出3条你遵循的多线程最佳实践
给你的线程起个有意义的名字。 这样可以方便找bug或追踪。
OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。
避免锁定和缩小同步的范围 锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。
多用同步类少用wait 和 notify 首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
多用并发集合少用同步集合 这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。
比如并发编程的黄金原则,尽量无锁化编程等等……..
67.请概述锁的公平和非公平,JDK内部是如何实现的。
公平锁是指所有试图获得锁的线程按照获取锁的顺序依次获得锁,而非公平锁则是当前的锁状态没有被占用时,当前线程可以直接占用,而不需要等待。在实现上,非公平锁逻辑基本跟公平锁一致,唯一的区别是,当前线程不需要判断同步队列中是否有等待线程。
非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。
使用场景的话呢,其实还是和他们的属性一一相关,比如:如果业务中线程占用(处理)时间要远长于线程等待,那用非公平锁其实效率并不明显,但是用公平锁可以保证不会有线程被饿死。
68.请概述AQS
是用来构建锁或者其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的。它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。它是CLH队列锁的一种变体实现。它可以实现2种同步方式:独占式,共享式。
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如tryAcquire、tryReleaseShared等等。
这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与同步组件交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注的领域。
在内部,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点,构成一个双端双向链表。
同时与Condition相关的等待队列,节点类型也是Node,构成一个单向链表。
69.请概述volatile
多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。但是volatile不能保证操作的原子,对任意单个volatile变量的读/写具有原子性,但类似于++这种复合操作不具有原子性。。
代码底层在执行时为了获取更好的性能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止重排序,当然这也一定程度上降低了代码执行效率。
同时在内存语义上,当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存,当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
在Java中对于volatile修饰的变量,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序问题、强制刷新和读取。
在具体实现上,volatile关键字修饰的变量会存在一个“lock:”的前缀。它不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。
JVM + GC
1、JM垃圾回收的时候如何确定垃圾?是否知道什么是 GC Roots
什么是垃圾
简单的说就是内存中已经不再被使用到的空间就是垃圾
要进行垃圾回收,如何判断一个对象是否可以被回收
引用计数法
枚举根节点做可达性分析(根搜索路径)
case
Java 可以做GCRoots的对象
虚拟机栈(栈帧中的局部变量区,也叫做局部变量表
方法区中的类静态属性引用的对象。
方法区中常量引用的对象
本地方法栈中N( Native方法)引用的对象
2、你说你做过JVM调优和参数配置,请问如何盘点查看MM系统默认值
JVM的参数类型
标配参数
-verison
-help
java -showversion
X参数(了解)
-Xint
解释执行
-Xcomp
第一次使用就编译成本地代码
-Xmixed
混合模式
XX参数
Boolean类型
公式
-XX:+或者- 某个属性值
+表示开启
-表示关闭
Case
是否打印GC收集细节
-XX:+PrintGCDetails
-XX:-PrintGCDetails
是否使用串行垃圾收集器
-XX:-UseSerialGC
-XX:+UseSerialGC
KV设值类型
公式
-XX:属性key=属性值value
Case
-XX:MetaspaceSize=128m
-XX:MaxTenuringThreshold=15
jinfo举例,如何查看当前运行程序的配置
公式
jinfo -flag 配置项 进程编号
Case1
Case2
Case3
题外话(坑题)
两个经典参数:-Xms和-Xmx
这个你如何解释
-Xms
等价于 -XX:InitialHeapSize
-Xmx
等价于-XX:MaxHeapSize
查看JVM默认值
-XX:+PrintFlagsInitial
查看初始默认值
公式
java -XX:+PrintFlagsInitial -version
java -XX:+PrintFlagsInitial
Case
-XX:+PrintFlagsFinal
主要查看修改更新
公式
java -XX:+PirntFlagsFinal
java -XX:+PirntFlagsFinal -version
Case
PrintFlagsFinal举例,运行Java命令的同时打印出参数
-XX:+PrintCommandLineFlags
3、你平时工作用过的M常用基本配置参数有哪些?
基础知识复习
Case
常用参数
-Xms
初始大小内存,默认为物理内存1/64
等价于-XX:InitialHeapSize
-Xmx
最大分配内存,默认为物理内存1/4
等价于-XX:MaxHeapSize
-Xss
设置单个线程的大小,一般默认为512K~1024K
等价于-XX:ThreadStackSize
-Xmn
设置年轻代大小
-XX:MetaspaceSize
设置元空间大小
-Xms10m -Xmx10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal
典型设置案例
-XX:+PrintGCDetails
输出详细GC收集日志信息
GC
FullGC
-XX:SurvivoRatio
-XX:NewRatio
-XX:MaxTenuringThreshold
设置垃圾最大年龄
4、强引用、软引用、弱引用、虚引用分别是什么?
整体架构
强引用(默认支持模式)
Case
软引用
Case
弱引用
Case
软引用和弱应用的适用场景
你知道弱引用的话,能谈谈WeakHashMap吗?
虚引用
引用队列
Case
Case
GCRoots和四大引用的小总结
5、请谈谈你对ooM的认识
Java.lang.StackOverflowError
Java.lang.OutOfMemoryError:Java heap space
Java.lang.OutOfMemeoryError:GC overhead limit exceeded
Java.lang.OutOfMemeoryError:Direct buffer memory
Java.lang.OutOfMemeoryError:unable to create new native thread
非root用户登录Linux系统测试
服务器级别参数调优
Java.lang.OutOfMemeoryError:Metaspace
使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:MetaspaceSize为21810376B(约20M)
6、G垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈
GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现
因为目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集
4种主要垃圾收集器
串行垃圾回收器(Serial)
它为单线程环境设计并且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境
并行垃圾回收器(Parallel)
多个垃圾回收线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互场景
并发垃圾回收器(CMS)
用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程
互联网公司多用它,适用于对响应时间有要求的场景
G1垃圾回收器
G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
7、怎么查看服务器默认的垃圾收集器是那个?
生产上如何配置垃圾收集器的?
谈谈你对垃圾收集器的理解?
怎么查看默认的垃圾收集器是哪个?
默认的垃圾收集器有哪些
垃圾收集器
部分参数预先说明
DefNew
Default New Generation
Tenured
Old
ParNew
Parallel New Generation
PSYoungGen
Parallel Scavenge
ParOldGen
Parallel Old Generation
Server/Client模式分别是什么意思
新生代
串行GC(Serial)/(Serial Coping)
并行GC(ParNew)
并行回收GC(Parallel)/(Parallel Scavenge)
老年代
串行回收GC(Serial Old)/(Serial MSC)
并行GC(Parallel Old)/(Parallel MSC)
并发标记清除GC(CMS)
4步过程
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)和用户线程一起
重新标记(CMS remark)
并发清除(CMS concurrent sweep)和用户线程一起
优缺点
优
并发收集低停顿
缺
并发执行,对CPU资源压力大
采用的标记清除算法会导致大量碎片
垃圾收集器配置代码总结
底层代码
实际代码
如何选择垃圾收集器
8、G1垃圾收集器
以前收集器特点
年轻代和老年代是各自独立且连续的内存块
年轻代收集使用单eden+S0 +S进行复制算法
老年代收集必须扫描整个老年代区域
都是以尽可能少而快速地执行GC为设计原则
G1是什么
特点
底层原理
Region区域化垃圾收集器
最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可
回收步骤
4步过程
case案例
常用配置参数(了解)
-XX:+UseG1GC
-XX:G1HeapRegionSize=n : 设置G1区域的大小。值是2的幂,范围是1M到32M。目标是根据最小的Java堆大小划分出约2048个区域
-XX:MaxGCPauseMillis=n : 最大停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿时间小于这个时间
-XX:InitiatingHeapOccupancyPercent=n 堆占用了多少的时候就触发GC,默认是45
-XX:ConcGCThreads=n 并发GC使用的线程数
-XX:G1ReservePercent=n 设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%
和CMS相比的优势
小总结
9、生产环境服务器变慢,诊断思路和性能评估谈谈?
整机:top
uptime,系统性能命令的精简版
CPU:vmstat
查看CPU(包含不限于)
查看额外
查看所有CPU核信息
mpstat -P ALL 2
每个进程使用cpu的用量分解信息
pidstat -u 1 -p 进程编号
内存:free
应用程序可用内存数
查看额外
pidstat -p 进程号 -r 采样间隔秒数
硬盘:df
查看磁盘剩余空闲数
磁盘IO:iostat
磁盘I/O性能评估
查看额外
pidstat -d 采样间隔秒数 -p 进程号
网络IO:ifstat
默认本地没有,下载ifstat
查看网络IO
10、假如生产环境出现CPU占用过高,请谈谈你的分析思路和定位
结合Linux和JDK命令一块分析
案例步骤
1. 先用top命令找出CPU占比最高的
2. ps -ef或者jps进一步定位,得知是一个怎么样的一个后台程序
3. 定位到具体线程或者代码
ps -mp 进程 -o THREAD,tid,time
参数解释
-m 显示所有线程
-p pid进程使用cpu的时间
-o 该参数后是用户自定义格式
4. 将需要的线程ID转换为16进制格式(英文小写格式)
printf "%x\n" 有问题的线程ID
5. jstack 进程ID | grep tid(16进制线程ID小写英文) -A60
11、对于JDK自带的JVM监控和性能分析工具用过哪些?一般你是怎么用的?
是什么
性能监控工具
jps(虚拟机进程状况工具)
jinfo(Java配置信息工具)
jmap(内存映像工具)
jstat(统计信息监控工具)
0 条评论
下一页