Java并发编程常见面试题夺命追问

1. 守护线程(Daemon)与用户线程(User)的区别

守护线程是程序运行时在后台提供服务的线程,当所有非守护线程结束后,也即程序终止时,所有守护线程都将一起被杀死。

JVM的垃圾回收线程就是一个守护线程,main()却是用户线程

通过Thread类中的setDaemon(boolean on)方法,true则把该线程设置为守护线程,反之为用户线程。

Thread.setDaemon()必须在Thread.start()之前调用,否则抛出异常。

2. 线程与进程的区别

进程是操作系统资源分配的最小单元,线程时操作系统调度的最小单元
一个进程可以有多个线程,它们共享进程资源

QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。

区别

Ⅰ 拥有资源

进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。

Ⅱ 调度

线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。

Ⅲ 系统开销

由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。

Ⅳ 通信方面

线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。

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

多任务系统采用时间片轮转的方式使多个任务得以在同一颗CPU上执行,在多任务的切换过程中,任务的状态保存及再加载过程就叫做上下文切换。

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

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

例如,线程T1占有资源A同时请求资源B,线程T2占有资源B同时请求资源A,就造成了相互等待得情况。

活锁:活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样资源在多个线程之间跳动而又得不到执行,这就是活锁。

活锁和死锁的区别: 死锁互相争夺资源,而活锁互相谦让资源;活锁有可能自行解开,死锁则不能。

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

Java中导致饥饿的原因:

  • 高优先级线程吞噬所有的低优先级线程的CPU时间。

  • 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。

  • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法),因为其他线程总是被持续地获得唤醒。

5. Java中用到的线程调度算法

采用时间片轮转进行线程调度。可以设置线程的优先级,让优先级高的线程先执行,如非特别需要,尽量不要设置优先级,防止出现线程饥饿。

6. 为什么推荐使用Executor框架

Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。

7. 谈一谈Executors工具类

主要有三种 Executor:

  • newCachedThreadPool:(corePoolSize=0,workQueue=SynchronousQueue,maximumPoolSize=Integer.MAX_VALUE)
    一个任务创建一个线程

  • newFixedThreadPool:(corePoolSize=默认大小,workQueue=LinkedBlockingQueue,maximumPoolSize=corePoolSize)
    所有任务只能使用固定大小的线程;

  • newSingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。

8. 什么是原子操作?在Java Concurrency API中有哪些原子类(Atomic classes)?

原子操作是指不可被中断得一个或一系列操作。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

9. Java Concurrency API中的Lock接口(Lock interface)是什么?对比Synchronized关键字

Lock是synchronized的扩展版,Lock提供更灵活的锁操作。(ReentrantLock实现了Lock接口)

  • 锁的实现
    synchronized是JVM实现的,而ReentrantLock是JDK实现的

  • 性能
    新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

  • 等待可中断
    当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其它事情。
    ReentrantLock可中断,而synchronized不可中断

  • 公平锁
    公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
    synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,非公平锁效率更高,但是也可以是公平的。

  • 锁绑定多个条件
    一个 ReentrantLock 可以同时绑定多个 Condition 对象。

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

阻塞队列的特点:

  • 队列为空时,获取元素的线程会等待队列变成非空。
  • 队列为满时,存储元素的线程会等待队列变成非满。

JDK7提供了7个阻塞队列(了解即可)。分别是:

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

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

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

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

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

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

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

使用 BlockingQueue 实现生产者消费者问题

public class ProducerConsumer {

    private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);

    private static class Producer extends Thread {
        @Override
        public void run() {
            try {
                queue.put("product");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("produce..");
        }
    }

    private static class Consumer extends Thread {

        @Override
        public void run() {
            try {
                String product = queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("consume..");
        }
    }
}
public static void main(String[] args) {
    for (int i = 0; i < 2; i++) {
        Producer producer = new Producer();
        producer.start();
    }
    for (int i = 0; i < 5; i++) {
        Consumer consumer = new Consumer();
        consumer.start();
    }
    for (int i = 0; i < 3; i++) {
        Producer producer = new Producer();
        producer.start();
    }
}
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..

11. 谈一谈并发集合CurrentHashMap的实现

链接点击

12. 多线程同步和互斥有几种实现方法,都是什么?

链接点击
线程同步:指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

线程互斥:当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。

线程间的同步方法大体可分为两类:用户模式和内核模式。

顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态。而用户模式就是不需要切换到内核态,只在用户态完成操作。

用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。
内核模式下的方法有:事件,信号量,互斥量。

13. 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法

调用start()方法时将new一个新的线程,并且执行在run()方法里的代码。

但是直接调用run()方法,它不会创建新的线程,只会在原来的线程中把run()方法当成普通方法来执行。

14. Java中有几种方法可以实现一个线程

  1. 继承Thread类(Thread类实现了Runnable接口)
  2. 实现Runnable接口,需要实现run()方法,并将其放入到Thread中执行
  3. 实现Callable接口,需要实现call()方法(有返回值),并将使用FutureTask包装后放入Thread中执行

15. Java中如何唤醒一个阻塞的线程?

阻塞状态是指因为某些原因线程放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU运行时间。直到线程重新进入就绪状态,它才有机会转到运行状态。

调用任意Object.wait()方法会导致当前线程阻塞,Java虚拟机会将当前线程放入到这个对象的等待池中,阻塞的同时也将释放该对象的锁。

相应地,调用任意Object.notify()方法则从该对象的等待池中选取一个线程放入到该对象的就绪池中(取消阻塞状态),等待重新获取对象的锁继续执行;调用对象Object.notifyAll()方法则将该对象的等待池中所有线程放入到该对象的就绪池中,等待获取对象的锁继续执行。

其次,wait、notify方法必须在synchronized块或方法中被调用,并且要保证同步块或方法的锁对象与调用wait、notify方法的对象是同一个,如此一来在调用wait之前当前线程就已经成功获取某对象的锁,执行wait阻塞后当前线程就将之前获取的对象锁释放。

16. 在Java中CycliBarriar、CountdownLatch和Semaphore怎么使用

链接点击

17. 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

  • 悲观锁:每次拿数据的时候都悲观地认为别人会修改,所以每次拿数据时都会上锁。
    传统关系型数据库里就大量使用了悲观锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前上锁。Java中的同步原语synchronized关键字的实现也是悲观锁。
  • 乐观锁:每次拿数据时都乐观地认为别人不会修改,所以不会上锁,只会在更新数据的时候判断一下在此期间别人有没改动过这个数据,针对ABA问题可以通过版本号控制解决。
    乐观锁用于多读的情况,例如数据库提供的类似于write_condition机制,其实也是乐观锁实现。Java中J.U.C包下面的原子变量类和JVM对Synchronized进行优化加入的轻量级锁和偏向锁都采用了乐观锁的一种实现方式CAS实现。

CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。

18. CopyOnWriteArrayList可以用于什么应用场景?

  • 为什么要使用CopyOnWriteArrayList?

在多线程环境下,在对集合进行迭代时,假如有其他线程对集合进行修改操作,会抛出java.util.ConcurrentModificationException异常。

  • copyOnWriteArrayList集合怎么解决并发环境下的集合修改

对原集合进行复制,线程对于集合的并发写操作在新集合上进行(使用synchronize同步,保证同一时间只有一个线程对集合进行写操作),完成写操作之后,将指向原集合的指针指向新的集合,原来的集合失效,实现并发环境下的集合修改。

  • 应用场景

    • 读多写少,因为写的时候会进行集合复制操作。
    • 实时性要求不高的场景,因为读的时候有可能读到旧的集合数据。

19. volatile有什么用?能否用一句话说明下volatile的应用场景?

volatile关键字可以保证有序性和可见性,不能保证原子性。被volatile关键字修饰的共享变量可以保证线程对于该变量的修改立即写入本地内存(而不是先写入工作内存再写入本地内存),同时让其它线程的工作内存中该共享变量的内存失效,其它线程要读取该共享变量的时候需要从本地内存中读取,从而保证了可见性。

volatile主要应用在运算结果并不依赖变量的当前值的场景。(例如作为标志符,执行shutdown()、start()等类似操作,来停止或者启动另外一个线程中的任务)

20. 为什么代码会重排序?

在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,重排序需要满足两个条件:

  • 在单线程环境下不能改变程序运行的结果。
  • 存在数据依赖关系的不允许重排序。

需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义

21. 在Java中wait和sleep方法的不同?

wait()方法是任何Object的方法,而sleep()是线程Thread的方法

调用wait()方法会挂起当前线程并且释放对象锁,所以wait()通常被用于线程交互

调用sleep()方法会挂起当前线程,但并不会释放锁,所以sleep()方法通常用于暂停线程的执行。

注意:

  • wait()方法会释放CPU执行权 和 占有的锁。

  • sleep(long)方法仅释放CPU使用权,锁仍然占用;线程被放入超时等待队列,与yield相比,它会使线程较长时间得不到运行。

  • yield()方法仅释放CPU执行权,锁仍然占用,线程会被放入就绪队列,会在短时间内再次执行。

  • wait和notify必须配套使用,即必须使用同一把锁调用;

  • wait和notify必须放在同步块中调用,wait和notify的对象必须是他们所处同步块的锁对象。

22. 一个线程运行时发生异常会怎样?

如果当前线程出现异常并且没有被本线程捕获,由于异常不能跨线程抛出,所以当前线程将会停止运行。

Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。

23. 为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object 类里?

Java的每个对象都有一个锁(monitor,在底层操作系统采用Mutex Lock实现)。调用wait()方法、notify()方法是用于释放当前对象锁或者用于通知其他等待获取对象锁的线程对象锁可用。

在Java的线程中并没有可供任何对象使用的锁和同步器。

24. 什么是ThreadLocal变量,为什么ThreadLocal变量不存在线程安全问题?

ThreadLocal变量提供了线程局部变量,往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

每个Thread维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象

ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响。

25. Java中interrupt() 和 isInterrupted()方法的区别?

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

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

  • isInterrupted :仅仅是查询当前线程的中断状态

26. 怎么检测一个线程是否拥有锁?

java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。

27. Java中堆和栈有什么不同?

每个线程都有自己的栈内存,用于存储本地变量、方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。因此栈不存在线程安全问题。而堆是所有线程共享的一片公用内存区域,有可能出现线程安全问题。

28. Thread类中的yield方法有什么作用?

yield()方法可以暂停当前正在执行的线程对象(即让出当前线程的CPU执行时间),让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

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

  • 相同点:两个方法都是向线程池提交任务
  • 不同点:execute()方法的返回类型是void,定义在Executor接口中。
    而submit()方法可以提交callable任务,并且返回持有计算结果的FutureTask对象。

30. 什么是阻塞式方法?

阻塞式方法:程序会一直等待该方法完成,等待期间不做任何操作。ServerSocket的accept()方法会一直等待客户端连接。

这里的阻塞指的是调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。

31. Java中的ReadWriteLock是什么?

一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。
Java中的ReadWriteLock是Java 5 中新增的一个接口,一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用JDK中的ReentrantReadWriteLock来实现这个规则,它最多支持65535个写锁和65535个读锁。

32. 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?

线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

猜你喜欢

转载自blog.csdn.net/tubro2017/article/details/87905159