面试题系列三:多线程(必问)
1.创建线程有哪些方法(4种)?
-
继承Thread类,重写run方法(其实Thread类本身也实现了Runnable接口)
-
实现Runnable接口,重写run方法
-
实现Callable接口,重写call方法(有返回值)
-
使用线程池(有返回值)
2.线程的生命周期
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。
-
新建:就是刚使用new方法,new出来的线程;
-
就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
-
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
-
阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
-
销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
完整的生命周期图如下:
3.Runnable和Callable的区别?
-
相同点:两者都需要调用Thread.start启动线程;
-
不同点:
Callable接口的call()方法有返回值;而实现Runnable接口的run()方法没有返回值;
Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
4.start()和run()方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法;
run( )方法是一个普通方法,当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动。
-
start()方法功能介绍
start()方法来启动线程,真正实现了多线程运行。
start方法的作用就是将线程由创建状态,变为就绪状态。当线程创建成功时,线程处于创建状态,如果你不调用start( )方法,那么线程永远处于创建状态。调用start( )后,才会变为就绪状态,线程才可以被CPU运行。
-
start()执行时间
调用start( )方法后,线程的状态是就绪状态,而不是运行状态(关于线程的状态详细。线程要等待CPU调度,不同的
JVM有不同的调度算法,线程何时被调度是未知的。因此,start()方法的被调用顺序不能决定线程的执行顺序。
注意:
由于在线程的生命周期中,线程的状态由创建到就绪只会发生一次,因此,一个线程只能调用start()方法一次,多次
启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
5.线程池有哪些参数
-
最常用的三个参数:
corePoolSize:核心线程数
queueCapacity:任务队列容量(阻塞队列)
maxPoolSize:最大线程数
-
三个参数的作用:
当线程数小于核心线程数时,创建线程;
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列;
当线程数大于等于核心线程数,且任务队列已满
-
若线程数小于最大线程数,创建线程
-
若线程数等于最大线程数,抛出异常,拒绝任务
-
6.线程池有几种(5种)?拒绝策略有几种(4种)?阻塞队列有几种(3种)?
-
五种线程池:
-
1. ExecutorService threadPool = null; 2. threadPool = Executors.newCachedThreadPool();//有缓冲的线程池,线程数 JVM 控制 3. threadPool = Executors.newFixedThreadPool(3);//固定大小的线程池 4. threadPool = Executors.newScheduledThreadPool(2);//一个能实现定时、周期性任务的线程池 5. threadPool = Executors.newSingleThreadExecutor();//单线程的线程池,只有一个线程在工作 6. threadPool = new ThreadPoolExecutor();//默认线程池,可控制参数比较多
-
四种拒绝策略:
-
1. RejectedExecutionHandler rejected = null; 2. rejected = new ThreadPoolExecutor.AbortPolicy();//默认,队列满了丢任务抛出异常 3. rejected = new ThreadPoolExecutor.DiscardPolicy();//队列满了丢任务不异常 4. rejected = new ThreadPoolExecutor.DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列 5. rejected = new ThreadPoolExecutor.CallerRunsPolicy();//如果添加到线程池失败,那么主线程会自己去执行该任务
-
三种阻塞队列:
-
1. BlockingQueue<Runnable> workQueue = null; 2. workQueue = new ArrayBlockingQueue<>(5);//基于数组的先进先出队列,有界 3. workQueue = new LinkedBlockingQueue<>();//基于链表的先进先出队列,无界 4. workQueue = new SynchronousQueue<>();//无缓冲的等待队列,无界
7.死锁
7.1 什么叫死锁?
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们
都将无法再向前推进。举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有
另外一个线程B,按照先锁b再锁a的顺序获得锁。
7.2 产生死锁的原因?
-
因为系统资源不足。
-
进程运行推进的顺序不合适。
-
资源分配不当等。
7.3 死锁产生的4个必要条件?
-
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
-
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
-
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
-
环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。
7.4 预防和处理死锁的方法?
(1)尽量不要在释放锁之前竞争其他锁 一般可以通过细化同步方法来实现,只在真正需要保护共享资源的地方去拿锁,并尽快释放锁,这样可以有效降低 在同步方法里调用其他同步方法的情况
(2)顺序索取锁资源 如果实在无法避免嵌套索取锁资源,则需要制定一个索取锁资源的策略,先规划好有哪些锁,然后各个线程按照一个顺序去索取,不要出现上面那个例子中不同顺序,这样就会有潜在的死锁问题
(3)尝试定时锁 在索取锁的时候可以设定一个超时时间,如果超过这个时间还没索取到锁,则不会继续堵塞而是放弃此次任务
解除死锁的方法?
(1)剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态; (2)撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
如何检测死锁?
(1)利用Java自带工具JConsole (2)Java线程死锁查看分析方法
8.volatile底层是怎么实现的?
当一个变量定义为volatile后,它将具备两种特性:1. 可见性,2. 禁止指令重排序。
可见性:编译器为了加快程序运行速度,对一些变量的写操作会现在寄存器或CPU缓存上进行,最后写入内存。而在这个过程中,变量的新值对其它线程是不可见的。当对volatile标记的变量进行修改时,先当前处理器缓存行的数据写回到系统内存,然后这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效。
处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。如果一个正在共享的状态的地址被嗅探到其他处理器打算写内存地址,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。
9.volatile与synchronized有什么区别?
-
volatile仅能使用在变量上,synchronized则可以使用在方法、类、同步代码块等等。
-
volatile只能保证可见性和有序性,不能保证原子性。而synchronized都可以保证。
volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
10.wait()和sleep()的区别?
- sleep()不释放锁,wait()释放锁
- sleep()在Thread类中声明的,wait()在Object类中声明
- sleep()是静态方法,是Thread.sleep(); wait()是非静态方法,必须由“同步锁”对象调用
- sleep()方法导致当前线程进入阻塞状态后,当时间到或interrupt()醒来;wait()方法导致当前线程进入阻塞状态后,由notify或notifyAll()
11.乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
乐观锁的实现方式:
1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
2、java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。
12.什么是可重入锁(ReentrantLock)?
ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。
在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1. 重入性的实现原理;2. 公平锁和非公平锁。
重入性的实现原理
要想支持重入性,就要解决两个问题:
-
在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
-
由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
ReentrantLock支持两种锁:公平锁和非公平锁
何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。

浙公网安备 33010602011771号