虐哭了!22 道 Java 高频面试题,你能抗得住面试官的几个灵魂拷问?
今日分享开始啦,请大家多多指教~
1. 并行和并发有什么区别?
并行:多个处理器或多核处理器同时处理多个任务。
并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
并发 = 两个队列和一台咖啡机。 并行 = 两个队列和两台咖啡机。
2. 线程和进程的区别?
一个程序下至少有一个进程,一个进程下至少有一个线程,一个进程下也可以有多个线程来增加程序的执行速度。
3. 守护线程是什么?
守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。 在 Java 中垃圾回收线程就是特殊的守护线程。
4. 实现多线程的方式有哪些?
继承 Thread 类:Java 单继承,不推荐;
实现 Runnable 接口:Thread 类也是继承 Runnable 接口,推荐;
实现 Callable 接口:实现 Callable 接口,配合 FutureTask 使用,有返回值;
使用线程池:复用,节约资源;
5. 说一下 runnable 和 callable 有什么区别?
runnable 没有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的补充。
6. sleep() 和 wait() 有什么区别?
两者都可以暂停线程的执行。
类的不同:sleep() 来自 Thread,wait() 来自 Object。
释放锁:sleep() 不释放锁;wait() 释放锁。
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。
7. 线程有哪些状态?
NEW 尚未启动
RUNNABLE 正在执行中
BLOCKED 阻塞的(被同步锁或者 IO 锁阻塞)
WAITING 永久等待状态
TIMED_WAITING 等待指定的时间重新被唤醒的状态
TERMINATED 执行完成
NEW 状态是指线程刚创建, 尚未启动;
RUNNABLE 状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO 等待的操作/CPU 时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep 等。
BLOCKED 这个状态下, 是在多个线程有同步操作的场景, 比如正在等待另一个线程的 synchronized 块的执行释放, 或者可重入的 synchronized 块里别人调用 wait() 方法, 也就是这里是线程在等待进入临界区。
WAITING 这个状态下是指线程拥有了某个锁之后, 调用了他的 wait 方法, 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面 wait 等待别人 notify, 线程调用了 join 方法 join 了另外的线程的时候, 也会进入 WAITING 状态, 等待被他 join 的线程执行结束。
TIMED_WAITING 这个状态就是有限的(时间限制)的 WAITING, 一般出现在调用 wait(long), join(long)等情况下, 另外一个线程 sleep 后, 也会进入 TIMED_WAITING 状态。
TERMINATED 这个状态下表示 该线程的 run 方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)。
8. notify()和 notifyAll()有什么区别?
notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。 而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
9. 线程的 run() 和 start() 有什么区别?
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。 run() 可以重复调用,而 start() 只能调用一次。
10. 创建线程池有哪几种方式?
线程池创建有七种方式,最核心的是最后一种:
newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列, 所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用, 当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。 这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
newScheduledThreadPool(int corePoolSize):和 newSingleThreadScheduledExecutor()类似, 创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法, 其内部会构建 ForkJoinPool,利用 Work-Stealing 算法,并行地处理任务,不保证处理顺序;
ThreadPoolExecutor():是最原始的线程池创建,上面 1-3 创建方式都是对 ThreadPoolExecutor 的封装。
11. 线程池中 submit() 和 execute() 方法有什么区别?
execute():只能执行 Runnable 类型的任务,无返回值
submit():可以执行 Runnable 和 Callable 类型的任务,有返回值
12. Java 程序中怎么保证多线程的运行安全?
方法一:使用安全类,比如 Java. util. concurrent 下地类。
方法二:使用自动锁 synchronized。
方法三:使用手动锁 Lock。
在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字 synchronized 可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时 synchronized 可以保证一个线程的变化可见(可见性),即可以代替 volatile。
synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程 A),运行到这个方法时,都要检查有没有其它线程 B(或者 C、 D 等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用 synchronized 方法的线程 B(或者 C 、D)运行完这个方法后再运行此线程 A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法和 synchronized 块。
线程安全在三个方面体现
1.原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
2.可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before 原则)。
13. 多线程中 synchronized 锁升级的原理是什么?
ynchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id, 再次进入的时候会先判断 threadid 是否与其线程 id 一致。
如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后, 如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
14. 什么是死锁?
当线程 A 持有独占锁 a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下, 就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
在 Java 中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,不再程序往下执行。我们只能通过中止并重启的方式来让程序重新执行。
这是我们非常不愿意看到的一种现象,我们要尽可能避免死锁的情况发生!
造成死锁的原因可以概括成三句话:
当前线程拥有其他线程需要的资源
当前线程等待其他线程已拥有的资源
都不放弃自己拥有的资源
15. 死锁的必要条件?怎么防止死锁?
互斥:一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进程的资源。
占有且等待:当一个进程在等待分配得到其他资源时,其继续占有已分配得到的资源。
非抢占:不能强行抢占进程中已占有的资源。
循环等待:存在一个封闭的进程链,使得每个资源至少占有此链中下一个进程所需要的一个资源。
防止:
死锁检测
加锁顺序
尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
尽量使用 Java. util. concurrent 并发类代替自己手写锁。
尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。 尽量减少同步的代码块。
16. ThreadLocal 是什么?有哪些使用场景?
ThreadLocal,即线程本地变量。如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。常见的 ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。
17. 说一下 synchronized 底层实现原理?
synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。 在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。 但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现, 也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
18. synchronized 和 volatile 的区别是什么?
volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
19. synchronized 和 Lock 有什么区别?
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
20. 说一下 atomic 的原理?
atomic 主要利用 CAS (Compare And Wwap) 和 volatile 和 native 方法来保证原子操作, 从而避免 synchronized 的高开销,执行效率大为提升。
21. synchronized 和 ReentrantLock 区别是什么?
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大, 但是在 Java 6 中对 synchronized 进行了非常多的改进。
主要区别如下:
ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁; ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
ReentrantLock 提供了内置锁类似的功能和内存语义。
此外,ReetrantLock 还提供了其它功能,包括定时的锁等待、可中断的锁等待、公平性、以及实现非块结构的加锁、Condition,对线程的等待和唤醒等操作更加灵活,一个 ReentrantLock 可以有多个 Condition 实例,所以更有扩展性,不过 ReetrantLock 需要显示的获取锁,并在 finally 中释放锁,否则后果很严重。
ReentrantLock 在性能上似乎优于 Synchronized,其中在 jdk1.6 中略有胜出,在 1.5 中是远远胜出。那么为什么不放弃内置锁,并在新代码中都使用 ReetrantLock?
在 java1.5 中, 内置锁与 ReentrantLock 相比有例外一个优点:在线程转储中能给出在哪些调用帧中获得了哪些锁,并能够检测和识别发生死锁的线程。Reentrant 的非块状特性仍然意味着,获取锁的操作不能与特定的栈帧关联起来,而内置锁却可以。
因为内置锁时 JVM 的内置属性,所以未来更可能提升 synchronized 而不是 ReentrantLock 的性能。例如对线程封闭的锁对象消除优化,通过增加锁粒度来消除内置锁的同步。
22. LinkedBlockingQueue 与 ArrayBlockingQueue 的区别?
队列大小有所不同,ArrayBlockingQueue 是有界的初始化必须指定大小,而 LinkedBlockingQueue 可以是有界的也可以是无界的(Integer.MAX_VALUE),(而且不会初始化就占用一大片内存)对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
数据存储容器不同,ArrayBlockingQueue 采用的是数组作为数据存储容器,而 LinkedBlockingQueue 采用的则是以 Node 节点作为连接对象的链表。
由于 ArrayBlockingQueue 采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而 LinkedBlockingQueue 则会生成一个额外的 Node 对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于 GC 可能存在较大影响。
两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue 实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个 ReenterLock 锁,而 LinkedBlockingQueue 实现的队列中的锁是分离的,其添加采用的是 putLock,移除采用的则是 takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
两者的 size 都是强一致的。但是实现有区别,Array~使用全局锁 Linked~使用原子变量实现。
阻塞队列与我们平常接触的普通队列(LinkedList 或 ArrayList 等)的最大不同点,在于阻塞队列支出阻塞添加和阻塞删除方法。
阻塞添加
所谓的阻塞添加是指当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作。
阻塞删除
阻塞删除是指在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素)
插入方法:
add(E e) : 添加成功返回 true,失败抛 IllegalStateException 异常
offer(E e) : 成功返回 true,如果此队列已满,则返回 false。
put(E e) :将元素插入此队列的尾部,如果该队列已满,则一直阻塞
删除方法:
remove(Object o) :移除指定元素,成功返回 true,失败返回 false
poll() : 获取并移除此队列的头元素,若队列为空,则返回 null
take():获取并移除此队列头元素,若没有元素则一直阻塞。
检查方法
element() :获取但不移除此队列的头元素,没有元素则抛异常
peek() :获取但不移除此队列的头;若队列为空,则返回 null。
今日份分享已结束,请大家多多包涵和指点!
评论