Java常见面试题(三)多线程

三、多线程     

1.并行和并发有什么区别?

并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。

并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。

2.线程和进程的区别?

进程是系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是CPU调度和分派的基本单位

一个程序至少一个进程,一个进程至少一个线程

每个进程都有独立的内存地址空间;系统不会为线程分配内存,线程组之间只能共享所属进程的资源

程序之间的切换会有较大的开销;线程之间切换的开销小

 

1、地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。

2、通信:进程间通信IPC(管道,信号量,共享内存,消息队列),线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。

3、调度和切换:线程上下文切换比进程上下文切换要快得多。

3.什么时候用进程?什么时候用线程?

1、需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。

2、线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应

3、因为对CPU系统的效率使用上线程更占优,所以多机分布的用进程,多核分布用线程;

4、并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;

5、需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。

https://www.cnblogs.com/renzhuang/articles/6733461.html

4.守护线程是什么?

Java线程分为用户线程和守护线程。

守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。

Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法。

注意:

setDaemon(true) 必须在 start() 之前设置,否则会抛出IllegalThreadStateException异常,该线程仍默认为用户线程,继续执行

守护线程创建的线程也是守护线程

守护线程不应该访问、写入持久化资源,如文件、数据库,因为它会在任何时间被停止,导致资源未释放、数据写入中断等问题

5.创建线程有哪几种方式?

一、继承Thread类创建线程类

(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

(2)创建Thread子类的实例,即创建了线程对象。

(3)调用线程对象的start()方法来启动该线程。

二、通过Runnable接口创建线程类

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

(3)调用线程对象的start()方法来启动该线程。

三、通过Callable和Future创建线程

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

6.说一下 runnable 和 callable 有什么区别?

相同点:

两者都是接口;(废话)

两者都可用来编写多线程程序;

两者都需要调用Thread.start()启动线程;

 

不同点:

两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;

Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

注意点:

Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

7.线程有哪些状态?

  • 新建(NEW):新建线程对象,未调用 start 方法
  • 可运行(RUNNABLE):线程对象创建后,被调用 start 方法。此状态的线程位于可运行线程池中,等待获取 CPU 的使用权
  • 运行中(RUNNING):线程获取了 CPU 的使用权,执行程序代码
  • 阻塞(BLOCKED):线程因为某种原因放弃了 CPU 的使用权,暂时停止运行,直到线程进入可运行状态,才有机会再次获取 CPU 的使用权进入运行状态
  • 消亡(DEAD):线程已经执行完毕。主线程 main 方法结束或因异常退出;子线程 run 方法结束或因异常退出

8.sleep() 和 wait() 有什么区别?

  • sleep() 是 Thread 类的静态本地方法;wait() 是Object类的成员本地方法
  • sleep() 方法可以在任何地方使用;wait() 方法则只能在同步方法或同步代码块中使用,否则抛出异常Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
  • sleep() 会休眠当前线程指定时间,释放 CPU 资源,不释放对象锁,休眠时间到自动苏醒继续执行;wait() 方法放弃持有的对象锁,进入等待队列,当该对象被调用 notify() / notifyAll() 方法后才有机会竞争获取对象锁,进入运行状态
  • JDK1.8 sleep() wait() 均需要捕获 InterruptedException 异常

9.notify()和 notifyAll()有什么区别?

先解释两个概念。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池,等待池中的线程不会去竞争该对象的锁。

锁池:只有获取了对象的锁,线程才能执行对象的 synchronized 代码,对象的锁每次只有一个线程可以获得,其他线程只能在锁池中等待

区别:

notify() 方法随机唤醒对象的等待池中的一个线程,进入锁池;notifyAll() 唤醒对象的等待池中的所有线程,进入锁池。

10.线程的 run()和 start()有什么区别?

调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。

一个线程对线的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run() 方法没有限制。

11.创建线程池有哪几种方式?

我们可以通过Executors的静态方法来创建线程池。

newFixedThreadPool(int nThreads)

创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程

newCachedThreadPool()

创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制

newSingleThreadExecutor()

这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行

newScheduledThreadPool(int corePoolSize)

创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Time

12.线程池都有哪些状态?

线程池的5种状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。

1.RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。

2.SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。

3.STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4.TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。 

当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。

当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

5.TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

13.线程池中 submit()和 execute()方法有什么区别?

  • execute() 参数 Runnable ;submit() 参数 (Runnable) 或 (Runnable 和 结果 T) 或 (Callable)
  • execute() 没有返回值;而 submit() 有返回值
  • submit() 的返回值 可以通过Future 调用get方法捕获处理异常

14.在 java 程序中怎么保证多线程的运行安全?

线程的安全性问题体现在:

原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性

可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到

有序性:程序执行的顺序按照代码的先后顺序执行

 

导致原因:

缓存导致的可见性问题

线程切换带来的原子性问题

编译优化带来的有序性问题

 

解决办法:

JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题

synchronized、volatile、LOCK,可以解决可见性问题

Happens-Before 规则可以解决有序性问题

 

Happens-Before 规则如下:

程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作

管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作

volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作

线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作

线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测

线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始

15.多线程锁的升级原理是什么?

锁的级别从低到高:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

锁分级别原因:

没有优化以前,sychronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 sychronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。

无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。

偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。

偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;

如果线程处于活动状态,升级为轻量级锁的状态。

轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。

重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
 

https://blog.csdn.net/meism5/article/details/90321826

16.什么是死锁?

多个进程因竞争资源而相互等待,若无外力作用,这些进程都无法向前推进

 

系统资源的竞争   当不可剥得资源的数量不足以满足进程的需要时,使得进程会因为争得资源而陷入僵局

进程推进顺序不当   进程在运行过程中,请求和释放资源的顺序不当,也会造成死锁。

 

互斥条件   一个资源只能被一个进程所占用,此时若其它进程请求该资源,则请求进程必须等待

不剥得条件   进程使用的资源在未完成使用之前,不能被其它进程强行得走。

请求和保持条件   一个进程因为请求资源而阻塞时,对已获资源保持不放

循环等待条件   若干进程形成了一种头尾相接环状等待资源的关系

17.怎么防止死锁?

  • 加锁顺序(线程按照一定的顺序加锁)
  • 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  • 死锁检测

18.ThreadLocal 是什么?有哪些使用场景?

        ThreadLocal是线程Thread中属性threadLocals的管理者

ThreadLocal三个方法get, set , remove以及内部类`ThreadLocalMap

 

ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。

 

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection

https://blog.csdn.net/meism5/article/details/90413860

https://blog.csdn.net/zzg1229059735/article/details/82715741

19.说一下 synchronized 底层实现原理?

比较长,请耐心看https://blog.csdn.net/javazejian/article/details/72828483

20.synchronized 和 volatile 的区别是什么?

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

21.synchronized 和 Lock 有什么区别?

  1. 实现层面不一样。synchronized 是 Java 关键字,JVM层面 实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁
  2. 是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要再 finally {} 代码块显式地中释放锁
  3. 是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时
  4. 获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功
  5. 功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可判断、可公平和不公平、细分读写锁提高效率

22.synchronized 和 ReentrantLock 区别是什么?

  1. synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果
  2. synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
  3. synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
  4. synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
  5. synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
  6. synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放

补充一个相同点:都可以做到同一线程,同一把锁,可重入代码块。

23.说一下 atomic 的原理?

通过CAS乐观锁保证原子性,通过自旋保证当次修改的最终修改成功,通过降低锁粒度(多段锁)增加并发性能。

https://blog.csdn.net/wuzhiwei549/article/details/82621947

参考:

1、 Java 并发编程

2、全面理解Java内存模型

3、《深入理解 Java 内存模型》读书笔记

猜你喜欢

转载自blog.csdn.net/qq_36366757/article/details/105121918