程序员的100大Java多线程面试问题及答案(二)

简介: 程序员的100大Java多线程面试问题及答案(二)

程序员的100大Java多线程面试问题及答案(一):https://developer.aliyun.com/article/1416651

52.notify

notify()也是Object类的通用方法,也要在同步方法或同步代码块内调用,该方法用来通知哪些可能灯光该对象的对象锁的其他线程,如果有多个线程等待,则随机挑选出其中一个呈wait状态的线程,对其发出 通知 notify,并让它等待获取该对象的对象锁。

53.notify/notifyAll

notify等于说将等待队列中的一个线程移动到同步队列中,而notifyAll是将等待队列中的所有线程全部移动到同步队列中。

54.等待/通知经典范式

等待

synchronized(obj) {
    while(条件不满足) {
        obj.wait();
    }
    执行对应逻辑
}

通知

synchronized(obj) {
    改变条件
    obj.notifyAll();
}
55.ThreadLocal

主要解决每一个线程想绑定自己的值,存放线程的私有数据。

56.ThreadLocal使用

获取当前的线程的值通过get(),设置set(T) 方式来设置值。

public class XKThreadLocal {
    public static ThreadLocal threadLocal = new ThreadLocal();
    public static void main(String[] args) {
        if (threadLocal.get() == null) {
            System.out.println("未设置过值");
            threadLocal.set("Java");
        }
        System.out.println(threadLocal.get());
    }
}

输出:

未设置过值
Java

Tips:默认值为null

57.解决get()返回null问题

通过继承重写initialValue()方法即可。

代码实现:

public class ThreadLocalExt extends ThreadLocal{
    static ThreadLocalExt threadLocalExt = new ThreadLocalExt();
    @Override
    protected Object initialValue() {
        return "Java";
    }
    public static void main(String[] args) {
        System.out.println(threadLocalExt.get());
    }
}

输出结果:

Java

58.Lock接口

锁可以防止多个线程同时共享资源。Java5前程序是靠synchronized实现锁功能。Java5之后,并发包新增Lock接口来实现锁功能。

59.Lock接口提供 synchronized不具备的主要特性

60.重入锁 ReentrantLock

支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

61.重进入是什么意思?

重进入是指任意线程在获取到锁之后能够再次获锁而不被锁阻塞。

该特性主要解决以下两个问题:

一、锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取。

二、所得最终释放。线程重复n次是获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。

62.ReentrantLock默认锁?

默认非公平锁

代码为证:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

63.公平锁和非公平锁的区别

公平性与否针对获取锁来说的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。

64.读写锁

读写锁允许同一时刻多个读线程访问,但是写线程和其他写线程均被阻塞。读写锁维护一个读锁一个写锁,读写分离,并发性得到了提升。

Java中提供读写锁的实现类是ReentrantReadWriteLock。

65.LockSupport工具

定义了一组公共静态方法,提供了最基本的线程阻塞和唤醒功能。

66.Condition接口

提供了类似Object监视器方法,与 Lock配合使用实现等待/通知模式。

67.Condition使用

代码示例:

public class XKCondition {
    Lock lock = new ReentrantLock();
    Condition cd = lock.newCondition();
    public void await() throws InterruptedException {
        lock.lock();
        try {
            cd.await();//相当于Object 方法中的wait()
        } finally {
            lock.unlock();
        }
    }
    public void signal() {
        lock.lock();
        try {
            cd.signal(); //相当于Object 方法中的notify()
        } finally {
            lock.unlock();
        }
    }
}

68.ArrayBlockingQueue?

一个由数据支持的有界阻塞队列,此队列FIFO原则对元素进行排序。队列头部在队列中存在的时间最长,队列尾部存在时间最短。

69.PriorityBlockingQueue?

一个支持优先级排序的无界阻塞队列,但它不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。

70.DelayQueue?

是一个支持延时获取元素的使用优先级队列的实现的无界阻塞队列。队列中的元素必须实现Delayed接口和 Comparable接口,在创建元素时可以指定多久才能从队列中获取当前元素。

71.Java并发容器,你知道几个?

ConcurrentHashMap、CopyOnWriteArrayList 、CopyOnWriteArraySet 、ConcurrentLinkedQueue、

ConcurrentLinkedDeque、ConcurrentSkipListMap、ConcurrentSkipListSet、ArrayBlockingQueue、

LinkedBlockingQueue、LinkedBlockingDeque、PriorityBlockingQueue、SynchronousQueue、

LinkedTransferQueue、DelayQueue

72.ConcurrentHashMap

并发安全版HashMap,java7中采用分段锁技术来提高并发效率,默认分16段。Java8放弃了分段锁,采用CAS,同时当哈希冲突时,当链表的长度到8时,会转化成红黑树。(如需了解细节,见jdk中代码)

73.ConcurrentLinkedQueue

基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用cas算法来实现。(如需了解细节,见jdk中代码)

74.什么是阻塞队列?

阻塞队列是一个支持两个附加操作的队列,这两个附加操作支持阻塞的插入和移除方法。

1、支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。

2、支持阻塞的移除方法:当队列空时,获取元素的线程会等待队列变为非空。

75.阻塞队列常用的应用场景?

常用于生产者和消费者场景,生产者是往队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列正好是生产者存放、消费者来获取的容器。

76.Java里的阻塞的队列

ArrayBlockingQueue:    数组结构组成的 |有界阻塞队列
LinkedBlockingQueue:   链表结构组成的|有界阻塞队列
PriorityBlockingQueue:  支持优先级排序|无界阻塞队列
DelayQueue:            优先级队列实现|无界阻塞队列
SynchronousQueue:      不存储元素| 阻塞队列
LinkedTransferQueue:   链表结构组成|无界阻塞队列
LinkedBlockingDeque:   链表结构组成|双向阻塞队列

77.Fork/Join

java7提供的一个用于并行执行任务的框架,把一个大任务分割成若干个小任务,最终汇总每个小任务结果的后得到大任务结果的框架。

78.工作窃取算法

是指某个线程从其他队列里窃取任务来执行。当大任务被分割成小任务时,有的线程可能提前完成任务,此时闲着不如去帮其他没完成工作线程。此时可以去其他队列窃取任务,为了减少竞争,通常使用双端队列,被窃取的线程从头部拿,窃取的线程从尾部拿任务执行。

79.工作窃取算法的有缺点

优点:充分利用线程进行并行计算,减少了线程间的竞争。

缺点:有些情况下还是存在竞争,比如双端队列中只有一个任务。这样就消耗了更多资源。

80.Java中原子操作更新基本类型,Atomic包提供了哪几个类?

AtomicBoolean:原子更新布尔类型

AtomicInteger:原子更新整形

AtomicLong:原子更新长整形

81.Java中原子操作更新数组,Atomic包提供了哪几个类?

AtomicIntegerArray: 原子更新整形数据里的元素

AtomicLongArray: 原子更新长整形数组里的元素

AtomicReferenceArray: 原子更新饮用类型数组里的元素

AtomicIntegerArray: 主要提供原子方式更新数组里的整形

82.Java中原子操作更新引用类型,Atomic包提供了哪几个类?

如果原子需要更新多个变量,就需要用引用类型了。

AtomicReference : 原子更新引用类型

AtomicReferenceFieldUpdater: 原子更新引用类型里的字段。

AtomicMarkableReference: 原子更新带有标记位的引用类型。标记位用boolean类型表示,构造方法时AtomicMarkableReference(V initialRef,boolean initialMark)

83.Java中原子操作更新字段类,Atomic包提供了哪几个类?

AtomiceIntegerFieldUpdater: 原子更新整形字段的更新器

AtomiceLongFieldUpdater: 原子更新长整形字段的更新器

AtomiceStampedFieldUpdater: 原子更新带有版本号的引用类型,将整数值

84.JDK并发包中提供了哪几个比较常见的处理并发的工具类?

提供并发控制手段: CountDownLatchCyclicBarrierSemaphore

线程间数据交换: Exchanger

85.CountDownLatch

允许一个或多个线程等待其他线程完成操作。

CountDownLatch的构造函数接受一个int类型的参数作为计数器,你想等待n个点完成,就传入n。

两个重要的方法:

countDown() : 调用时,n会减1。

await() : 调用会阻塞当前线程,直到n变成0。

await(long time,TimeUnit unit) : 等待特定时间后,就不会继续阻塞当前线程。

tips:计数器必须大于等于0,当为0时,await就不会阻塞当前线程。

不提供重新初始化或修改内部计数器的值的功能。

86.CyclicBarrier

可循环使用的屏障。

让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier默认构造放时CyclicBarrier(int parities) ,其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达屏障,然后当前线程被阻塞。

87.CountDownLatch与CyclicBarrier区别

CountDownLatch

计数器:计数器只能使用一次。

等待: 一个线程或多个等待另外n个线程完成之后才能执行。

CyclicBarrier

计数器:计数器可以重置(通过reset()方法)。

等待: n个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

88.Semaphore

用来控制同时访问资源的线程数量,通过协调各个线程,来保证合理的公共资源的访问。

应用场景:流量控制,特别是公共资源有限的应用场景,比如数据链接,限流等。

89.Exchanger

Exchanger是一个用于线程间协作的工具类,它提供一个同步点,在这个同步点上,两个线程可以交换彼此的数据。比如第一个线程执行exchange()方法,它会一直等待第二个线程也执行exchange,当两个线程都到同步点,就可以交换数据了。

一般来说为了避免一直等待的情况,可以使用exchange(V x,long timeout,TimeUnit unit),设置最大等待时间。

Exchanger可以用于遗传算法。

90.为什么使用线程池

几乎所有需要异步或者并发执行任务的程序都可以使用线程池。合理使用会给我们带来以下好处。

  • 降低系统消耗:重复利用已经创建的线程降低线程创建和销毁造成的资源消耗。
  • 提高响应速度: 当任务到达时,任务不需要等到线程创建就可以立即执行。
  • 提供线程可以管理性: 可以通过设置合理分配、调优、监控。

91.线程池工作流程

1、判断核心线程池里的线程是否都有在执行任务,否->创建一个新工作线程来执行任务。是->走下个流程。

2、判断工作队列是否已满,否->新任务存储在这个工作队列里,是->走下个流程。

3、判断线程池里的线程是否都在工作状态,否->创建一个新的工作线程来执行任务,

是->走下个流程。

4、按照设置的策略来处理无法执行的任务。

92.创建线程池参数有哪些,作用?

public ThreadPoolExecutor(   int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

1.corePoolSize:核心线程池大小,当提交一个任务时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建,等待需要执行的任务数大于线程核心大小就不会继续创建。

2.maximumPoolSize:线程池最大数,允许创建的最大线程数,如果队列满了,并且已经创建的线程数小于最大线程数,则会创建新的线程执行任务。如果是无界队列,这个参数基本没用。

3.keepAliveTime: 线程保持活动时间,线程池工作线程空闲后,保持存活的时间,所以如果任务很多,并且每个任务执行时间较短,可以调大时间,提高线程利用率。

4.unit: 线程保持活动时间单位,天(DAYS)、小时(HOURS)、分钟(MINUTES、毫秒MILLISECONDS)、微秒(MICROSECONDS)、纳秒(NANOSECONDS)

5.workQueue: 任务队列,保存等待执行的任务的阻塞队列。

一般来说可以选择如下阻塞队列:

ArrayBlockingQueue:基于数组的有界阻塞队列。

LinkedBlockingQueue:基于链表的阻塞队列。

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

PriorityBlockingQueue:一个具有优先级的阻塞队列。

6.threadFactory:设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

  1. handler: 饱和策略也叫拒绝策略。当队列和线程池都满了,即达到饱和状态。所以需要采取策略来处理新的任务。默认策略是AbortPolicy。
    AbortPolicy:直接抛出异常。
    CallerRunsPolicy: 调用者所在的线程来运行任务。
    DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    DiscardPolicy:不处理,直接丢掉。
    当然可以根据自己的应用场景,实现RejectedExecutionHandler接口自定义策略。

93.向线程池提交任务

可以使用execute()和submit() 两种方式提交任务。

execute():无返回值,所以无法判断任务是否被执行成功。

submit():用于提交需要有返回值的任务。线程池返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()来获取返回值,get()方法会阻塞当前线程知道任务完成。get(long timeout,TimeUnit unit)可以设置超市时间。

94.关闭线程池

可以通过shutdown()或shutdownNow()来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt来中断线程,所以无法响应终端的任务可以能永远无法停止。

shutdownNow首先将线程池状态设置成STOP,然后尝试停止所有的正在执行或者暂停的线程,并返回等待执行任务的列表。

shutdown只是将线程池的状态设置成shutdown状态,然后中断所有没有正在执行任务的线程。

只要调用两者之一,isShutdown就会返回true,当所有任务都已关闭,isTerminaed就会返回true。

一般来说调用shutdown方法来关闭线程池,如果任务不一定要执行完,可以直接调用shutdownNow方法。

95.线程池如何合理设置

配置线程池可以从以下几个方面考虑。

  • 任务是cpu密集型、IO密集型或者混合型
  • 任务优先级,高中低。
  • 任务时间执行长短。
  • 任务依赖性:是否依赖其他系统资源。
    cpu密集型可以配置可能小的线程,比如 n + 1个线程。
    io密集型可以配置较多的线程,如 2n个线程。
    混合型可以拆成io密集型任务和cpu密集型任务,
    如果两个任务执行时间相差大,否->分解后执行吞吐量将高于串行执行吞吐量。
    否->没必要分解。
    可以通过Runtime.getRuntime().availableProcessors()来获取cpu个数。
    建议使用有界队列,增加系统的预警能力和稳定性。

96.Executor

从JDK5开始,把工作单元和执行机制分开。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。

97.Executor框架的主要成员

ThreadPoolExecutor :可以通过工厂类Executors来创建。

可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool、CachedThreadPool。

ScheduledThreadPoolExecutor :可以通过工厂类Executors来创建。

可以创建2中类型的ScheduledThreadPoolExecutor:ScheduledThreadPoolExecutor、SingleThreadScheduledExecutor

Future接口:Future和实现Future接口的FutureTask类来表示异步计算的结果。

Runnable和Callable:它们的接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。Runnable不能返回结果,Callable可以返回结果。

98.FixedThreadPool

可重用固定线程数的线程池。

查看源码:

public static ExecutorService newFixedThreadPool(int nThreads) {
   return new ThreadPoolExecutor(nThreads, nThreads,
                                 0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());}

corePoolSize 和maxPoolSize都被设置成我们设置的nThreads。

当线程池中的线程数大于corePoolSize ,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止,如果设为0,表示多余的空闲线程会立即终止。

工作流程:

1.当前线程少于corePoolSize,创建新线程执行任务。

2.当前运行线程等于corePoolSize,将任务加入LinkedBlockingQueue。

3.线程执行完1中的任务,会循环反复从LinkedBlockingQueue获取任务来执行。

LinkedBlockingQueue作为线程池工作队列(默认容量Integer.MAX_VALUE)。因此可能会造成如下赢下。

1.当线程数等于corePoolSize时,新任务将在队列中等待,因为线程池中的线程不会超过corePoolSize。

2.maxnumPoolSize等于说是一个无效参数。

3.keepAliveTime等于说也是一个无效参数。

4.运行中的FixedThreadPool(未执行shundown或shundownNow))则不会调用拒绝策略。

5.由于任务可以不停的加到队列,当任务越来越多时很容易造成OOM。

99.SingleThreadExecutor

是使用单个worker线程的Executor。

查看源码:

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

corePoolSize和maxnumPoolSize被设置为1。其他参数和FixedThreadPool相同。

执行流程以及造成的影响同FixedThreadPool.

100.CachedThreadPool

根据需要创建新线程的线程池。

查看源码:

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());

corePoolSize设置为0,maxmumPoolSize为Integer.MAX_VALUE。keepAliveTime为60秒。

工作流程:

1.首先执行SynchronousQueue.offer (Runnable task)。如果当前maximumPool 中有空闲线程正在执行S ynchronousQueue.poll(keepAliveTIme,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute方 法执行完成;否则执行下面的步骤2。

  1. 当初始maximumPool为空或者maximumPool中当前没有空闲线程时,将没有线程执行 SynchronousQueue.poll (keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1将失 败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。

3.在步骤2中新创建的线程将任务执行完后,会执行SynchronousQueue.poll (keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。

一般来说它适合处理时间短、大量的任务。

她是我青春岁月里勇敢而又失败的一章

相关文章
|
24天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
64 2
|
12天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
36 14
|
23天前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
29天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1月前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
17天前
|
Java 编译器 程序员
Java面试高频题:用最优解法算出2乘以8!
本文探讨了面试中一个看似简单的数学问题——如何高效计算2×8。从直接使用乘法、位运算优化、编译器优化、加法实现到大整数场景下的处理,全面解析了不同方法的原理和适用场景,帮助读者深入理解计算效率优化的重要性。
25 6
|
24天前
|
SQL 存储 Java
面向 Java 程序员的 SQLite 替代品
SQLite 是轻量级数据库,适用于小微型应用,但其对外部数据源支持较弱、无存储过程等问题影响了开发效率。esProc SPL 是一个纯 Java 开发的免费开源工具,支持标准 JDBC 接口,提供丰富的数据源访问、强大的流程控制和高效的数据处理能力,尤其适合 Java 和安卓开发。SPL 代码简洁易懂,支持热切换,可大幅提高开发效率。
|
1月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
52 4
|
1月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
88 4
|
7月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。