多线程面试题总结

多线程的优缺点

优点:

  1. 多线程技术使程序的响应速度更快。
  2. 当前没有进行处理的任务可以将处理器时间让给其它任务。
  3. 占用大量处理时间的任务可以定期将处理器时间让给其它任务。
  4. 可以随时停止任务。
  5. 可以分别设置各个任务的优先级以及优化性能。

缺点

  1. 等候使用共享资源时造成程序的运行速度变慢。
  2. 对线程进行管理要求额外的cpu开销。
  3. 可能出现线程死锁情况。即较长时间的等待或资源竞争以及死锁等症状。

在java中守护线程和本地线程区别

java中的线程分为两种:守护线程(Daemon)和用户线程(User)。

任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。

两者的区别:

唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经撤离,Daemon 没有可服务的线程,JVM撤离。也可以理解为守护线程是JVM自动创建的线程(但不一定),用户线程是程序创建的线程;比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。

扩展:Thread Dump打印出来的线程信息,含有daemon字样的线程即为守护进程,可能会有:服务守护进程、编译守护进程、windows下的监听Ctrl+break的守护进程、Finalizer守护进程、引用处理守护进程、GC守护进程。

线程与进程的区别

进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。一个程序至少有一个进程,一个进程至少有一个线程。

什么是多线程中的上下文切换

多线程会共同使用一组计算机上的CPU,而线程数大于给程序分配的CPU数量时,为了让各个线程都有执行的机会,就需要轮转使用CPU。不同的线程切换使用CPU发生的切换数据等就是上下文切换。

死锁与活锁的区别,死锁与饥饿的区别

死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁的必要条件:

  • 互斥条件:所谓互斥就是进程在某一时间内独占资源。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

Java中导致饥饿的原因:

  • 高优先级线程吞噬所有的低优先级线程的CPU时间。
  • 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
  • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法),因为其他线程总是被持续地获得唤醒。

start()方法和run()方法的区别

start()方法:

  • 用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。
  • 通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到CPU时间片,就开始执行run()方法。

run()方法:

  • run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条。

总结:

  1. 调用start方法方可启动线程。
  2. run方法只是thread的一个普通方法调用,还是在主线程里执行。
  3. 把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用run()方法,这是由jvm的内存机制规定的。
  4. run()方法必须是public访问权限,返回值类型为void。

Runnable接口和Callable接口的相同点和不同点

相同点:

  1. Callable和Runnable都是接口;
  2. Callable和Runnable都一样应用于Executors;

不同点:

  1. Callable要实现call()方法,Runnable要实现run()方法;
  2. call()方法可以有返回值,run()方法不能有返回值;
  3. call()方法可以抛出Checked Exception,run()方法不可以;
  4. Runnable接口在Jdk1.1中就有了,Callable在JDK1.5才有;

voliate关键字的作用

  • 多线程使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。
  • Java代码执行中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会禁止语义重排序,当然这也一定程度上降低了代码执行效率。

CyclicBarrier和CountDownLatch的区别

CountDownLatch CyclicBarrier
减计数方式 加计数方式
计数为0时唤醒所有等待的线程 计数达到指定值时唤醒所有等待的线程
计数为0无法重置 计数达到指定值时,计数置为0重新开始
调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没有影响 调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞
不能重复利用 可重复使用

voliate和synchronized对比

  • volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别,synchronized则可以使用在变量,方法以及类级别。
  • volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞。

怎么唤醒一个阻塞的线程

  1. 如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;
  2. 如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

sleep方法和wait方法的相同点和不同点

相同点:

二者都可以让线程处于阻塞;

不同点:

  1. 首先sleep方法是Thread类中定义的方法,而wait方法是Object类中定义的方法。
  2. sleep方法必须人为地为其指定休眠时间。wait方法既可以指定时间,也可以不指定时间。
  3. sleep方法时间到了,线程处于临时阻塞状态或者运行状态。wait方法如果没有被设置时间,就必须要通过notify或者notifyAll来唤醒。
  4. sleep方法不一定非要定义在同步中。wait方法必须定义在同步中。
  5. 当二者都定义在同步中时,线程执行到sleep,不会释放锁。线程执行到wait,会释放锁。

生产者和消费者模型的作用

  1. 通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用。
  2. 解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约。

Executor.submit()和Executor.execute()的区别

前者返回一个 Future对象,可以用于找到工作线程的运行结果。

在异常处理上也不一样,在任务抛出异常时,如果是通过 execute()提交的,会抛出无需捕获的异常(如果你没有特殊处理,会打印错误栈道System.err)。如果是通过 submit()提交的,任何异常,无论是不是checked exception,都是返回的一部分,Future.get将把异常包在 ExecutionExeption中,向上层抛出。

ThreadLocal的作用

  1. ThreadLocal用来解决多线程程序的并发问题。
  2. ThreadLocal并不是一个Thread,而是Thread的局部变量,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
  3. 从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
  4. 线程局部变量并不是Java的新发明,Java没有提供在语言级支持(语法上),而是变相地通过ThreadLocal的类提供支持。

wait方法和notify/notifyAll方法在放弃对象监视器时的区别

wait()方法立即释放对象监视器;

notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

Lock和synchronized对比

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。
  6. 在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞式的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。
    但是,JDK1.6,发生了变化,对synchronized加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronized的性能并不比Lock差。因此。提倡优先考虑使用synchronized来进行同步。

ReadWriteLock是什么

ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

FutureTask是什么

FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

Java中用到的线程调度算法

抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

乐观锁和悲观锁

乐观锁:对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-设置这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
悲观锁:对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,直接对操作资源上了锁。

编写一个死锁程序

死锁现象描述:

线程A和线程B相互等待对方持有的锁导致程序无限死循环下去。

死锁的实现步骤:

  • 两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;
  • 线程1的run()方法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,100毫秒差不多了,然后接着获取lock2的对象锁。这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象的对象锁;
  • 线程2的run)(方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁持有,线程2肯定是要等待线程1释放lock1的对象锁的
    这样,线程1″睡觉”睡完,线程2已经获取了lock2的对象锁了,线程1此时尝试获取lock2的对象锁,便被阻塞,此时一个死锁就形成了。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class DeadLock {
public void run() {
TestDeadLock tl = new TestDeadLock();
new Thread(tl, "线程A").start();
new Thread(tl, "线程B").start();
}

class TestDeadLock implements Runnable {
private Object objA = new Object();
private Object objB = new Object();
private boolean flag = true;

@Override
public void run() {
if (flag) {
flag = false;
synchronized (objA) {
System.out.println(Thread.currentThread().getName() + "锁住资源A,等待资源B");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objB) {
System.out.println(Thread.currentThread().getName() + "获得资源B");
}
}
} else {
flag = true;
synchronized (objB) {
System.out.println(Thread.currentThread().getName() + "锁住资源B,等待资源A");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objA) {
System.out.println(Thread.currentThread().getName() + "获得资源A");
}
}
}
}
}

public static void main(String[] args) {
new DeadLock().run();
}
}

输出结果是:
线程A锁住资源A,等待资源B
线程B锁住资源B,等待资源A

为什么使用Executor框架

  1. 每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。
  2. 调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
  3. 直接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
  4. 能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。
  5. 可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争。
  6. 框架中已经有定时、定期、单线程、并发数控制等功能。

在Java Concurrency API中有哪些原子类

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

解决ABA问题的原子类:AtomicMarkableReference(通过引入一个boolean来反映中间有没有变过),AtomicStampedReference(通过引入一个int来累加来反映中间有没有变过)

Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?

Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

它的优势有:

  • 可以使锁更公平
  • 可以使线程在等待锁的时候响应中断
  • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
  • 可以在不同的范围,以不同的顺序获取和释放锁

整体上来说Lock是synchronized的扩展版,Lock提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition方法)锁操作。另外Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。

这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

JDK7提供了7个阻塞队列。分别是:

  • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

  • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

  • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

  • DelayQueue:一个使用优先级队列实现的无界阻塞队列。

  • SynchronousQueue:一个不存储元素的阻塞队列。

  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

Java 5之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要的技术就是用好,wait ,notify,notifyAll,sychronized这些关键字。而在java 5之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。

BlockingQueue接口是Queue的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性,当生产者线程试图向BlockingQueue放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向BlockingQueue中放入元素,取出元素,它可以很好的控制线程之间的通信。

阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。

什么叫线程安全?servlet是线程安全吗?

线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

Servlet不是线程安全的,servlet是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。

Struts2的action是多实例多线程的,是线程安全的,每个请求过来都会new一个新的action分配给这个请求,请求完成后销毁。

SpringMVC的Controller是线程安全的吗?不是的,和Servlet类似的处理流程

Struts2好处是不用考虑线程安全问题;Servlet和SpringMVC需要考虑线程安全问题,但是性能可以提升不用处理太多的gc,可以使用ThreadLocal来处理多线程的问题。

Java中interrupted 和 isInterrupted方法的区别

interrupt

interrupt方法用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。

注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。

interrupted

查询当前线程的中断状态,并且清除原状态。如果一个线程被中断了,第一次调用interrupted则返回true,第二次和后面的就返回false了。

isInterrupted

仅仅是查询当前线程的中断状态。

本文标题:多线程面试题总结

文章作者:王洪博

发布时间:2018年09月29日 - 20:09

最后更新:2020年02月18日 - 06:02

原始链接:http://whb1990.github.io/posts/e3e73bdc.html

▄︻┻═┳一如果你喜欢这篇文章,请点击下方"打赏"按钮请我喝杯 ☕
0%