Java多线程面试题①

说点什么

小编最近通过面试发现,几乎每个面试官都热衷于问多线程方面的知识点,不管是出于项目的需求,还是作为Java基础,多线程是每个程序员应该要掌握的东西。小编整理了一些多线程方面的面试题,一方面是记录下常见的面试问题,也希望对正在面试的你有所帮助吧。
废话不多说了,开始看题吧。

进入正题

1、进程和线程的区别?
  • 进程是系统资源分配的基本单位,线程是CPU分配资源的基本单位。
  • 进程间是相互独立的,同一个进程内的多个线程是可以共享的。
  • 进程切换比线程切换对系统的开销大,线程切换得快。
    注意:进程开销大线程开销小的原因:每个进程都有独立的代码和数据空间(即程序上下文);而同一类线程共享代码和数据空间(即程序上下文),每个线程都有独立的运行栈和程序计数器。
2、线程创建的几种方式?

线程创建有以下4种方式:

  • 继承Thread类。(重写run(),创建子类的实例,通过start()启动该线程)
  • 实现Runnable接口。(重写run(),创建实现类的实例作为参数,创建Thread对象,通过start()启动线程)
  • 实现callable接口。(重写call()并有返回值可抛异常,创建实现类的实例作为FutureTask类的包装对象,使用FutureTask对象作为参数创建Thread对象,通过start()启动线程,通过FutureTask对象的get()可获取子线程德返回值)
  • 使用通过Executor提供的线程池。
3、线程池的种类有哪些?运行流程是怎样的?使用线程池有何优势?

线程池的种类有4种:

  • 单线程化的线程池(newSingleThreadExecutor ):保证池中只有一个线程执行任务。
  • 定长的线程池(newFixedThreadPool ):创建固定数目线程,超出的线程会在队列等待。
  • 调度型的线程池(newScheduledThreadPool ):指定调度规则,可定时和周期性的执行。
  • 可缓存的线程池(newCachedThreadPool):超过长度,先判断有无空闲的线程,有则回收,无则新建。

线程池的运行流程如下图:
Java多线程面试题①
线程池的优点:

  • 降低了资源的消耗
  • 提高响应速度
  • 便于管理,高效使用
4、简要说一下线程的基本状态及其状态间的关系?

如下图所示:
Java多线程面试题①

5、线程同步和调度的方法有哪些?说一下各自的功能和区别?

Thread类:

  • start():启动新创建的线程。
  • run():执行任务代码。
  • sleep():使正在运行的线程休眠,不会释放锁。
  • yield():放弃CPU的使用权,退出运行状态,进入可运行状态。
  • join():让后面的某个线程在当前线程前面先执行,再执行当前线程。

Object类:

  • wait():使当前运行的线程进入阻塞状态,会释放对象锁,会抛异常。
  • notify():唤醒处于阻塞状态的线程,但不确定是哪个,由JVM决定。
  • notifyAll():唤醒所有处于阻塞状态的线程,让它们竞争,只有获取锁的线程才能进入可运行状态。

Lock类:

  • lock():获取锁(互斥锁)。
  • unlock():释放锁。
  • tryLock():判断是否获取了锁。
  • newCondition():创建与Lock对象关联的Condition对象。

Condition类:

  • await():同wait()。
  • signal():同notify()。
  • signalAll():同notifyAll()。
6、同步方法和同步代码块的区别?

为什么要同步?
Java允许多线程并发控制,多个线程操作同一个资源变量时,会出现数据不准确,互相冲突等现象。加入同步锁可以避免线程不安全的情况,保证变量的准确性和唯一性。
语法上
同步方法是在方法上加入synchronized关键字,而同步代码块是synchronized(){}。
锁使用
同步方法用this(非静态方法)或当前类的class对象(静态方法)作为锁,同步代码块的粒度比同步方法更细。

7、synchronized与Lock类的区别?

Lock类和java内置的synchronized关键字的目的都是为了解决多线程同步问题,处理资源争夺技术。而是还是有些区别的(解决synchronized无法实现的):

  • synchronized无法控制加锁和释放锁,Lock类必须手动释放锁。
  • Lock类可以实现并发读文件操作。
  • synchronized无法知道是否获取了锁,Lock类通过tryLock()可以判断。
  • 结合Condition类使用。性能更好,可以实现等待通知的功能。

ps:顺带总结下Lock经常使用的场景

  • 通过ReentrantLock实现线程同步
  • 通过Condition实现等待通知的功能
  • 通过ReentrantReadWriteLock实现读写锁
8、死锁的必要条件是什么及怎么避免死锁?

死锁:多个线程在竞争资源的过程中出现相互等待的情况,无外力作用,无法推进下去。
死锁四个条件:

  • 互斥使用
  • 不可抢夺
  • 保持和请求
  • 循环等待
    避免死锁的技术:
  • 顺序加锁
  • 限时加锁
  • 死锁检测
9、悲观锁和乐观锁的区别?

悲观锁:认为每次拿数据都会被修改,在操作数据之前都会加把锁。常见的数据库中的行锁,表锁,读锁等,就是操作前都会先上锁。synchronized和ReentrantLock也是基于这种思想实现的。
乐观锁:认为每次拿数据都不会被修改,不会加锁。适用于多读场景,提高吞吐量,通常使用版本号机制和cas算法实现。

10、高级多线程控制中经常用到原子类,对原子类进行一些简单的总结?

java.util.concurrent.atomic包中的类及其方法
该包有12个类,原子更新方式有4种:

  • 基本类型(AtomicBoolean/AtomicLong/AtomicInteger)
  • 数组(AtomicLongArray/AtomicIntegerArray/AtomicReferenceArray)
  • 引用(AtomicReference/AtomicReferenceFieldUpdater/AtomicMarkableReference)
  • 字段(AtomicIntegerFieldUpdater/AtomicLongFieldUpdater/AtomicStampedReference)
    注意:一般常用的AtomicInteger、AtomicReference和AtomicStampedReference。
    以AtomicInteger类为例,说一下它里面有哪些常用的方法?
  • boolean compareAndSet(int e,int n):比较预期值与修改值,相等则设置修改值
  • int addAndGet(int delta):将输入值与实例的值想加,返回结果
  • int getAndIncrement():实现当前值加1,返回自增前的值
  • int getAndSet(int newValue):设置新值,返回旧值
  • void lazySet(int newValue):最终设置为新的值
    i++问题
    对于这种复合类的操作,可以使用并发包下的原子操作类通过循环CAS方式保证原子性。
    cas以及ABA问题
    cas:一种乐观锁,实现思路是内存值v,预期值e,修改值n,当v等于e时,说明其他线程没有修改该内存值,将n赋给v。
    乐观锁不足的地方:出现ABA问题。
    解决方法:使用带版本号的引用类型:AtomicStampedReference

结尾

理解多线程的面试题,需要做到循环渐进,先把基础的问题搞清楚了才是最重要的。像Thread类、Lock类、原子类等都是多线程开发中需要知道的,还有些锁相关的问题了解其设计思想和区别更不用多说了。
以上整理的内容,如有不妥或错误,敬请指正。请继续期待下一篇面试题,一起学习~