Java基础学习多线程实战面试题形式

Posted riches

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java基础学习多线程实战面试题形式相关的知识,希望对你有一定的参考价值。

1、什么是JUC包

  在 Java 5.0 提供了 java.util.concurrent(简称 JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。

2、sleep( ) 和 wait( n)、wait( ) 的区别

  • sleep 方法:是 Thread 类的静态方法,当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待 CPU 的到来。睡眠不释放锁(如果有的话)。
  • wait 方法:是 Object 的方法,必须与 synchronized 关键字一起使用,线程进入阻塞状态,当 notify 或者 notifyall 被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,释放互斥锁。

3、synchronized 关键字

底层实现

  进入时,执行 monitorenter,将计数器 +1,释放锁 monitorexit 时,计数器-1; 当一个线程判断到计数器为 0 时,则当前锁空闲,可以占用;反之,当前线程进入等待状态。

含义:(monitor 机制)

  Synchronized 是在加锁,加对象锁

  对象锁是一种重量锁(monitor),synchronized 的锁机制会根据线程竞争情况在运行时会有偏向锁(单一线程)、轻量锁(多个线程访问 synchronized 区域)、对象锁(重量锁,多个线程存在竞争的情况)、自旋锁等。 该关键字是一个几种锁的封装。

4、volatile 关键字

  定义:volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略。

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性
  • 禁止进行指令重排序。(实现有序性
  • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。 

5、volatile 修饰符有过什么实践

  • 一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。
  • volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。简单的说,就是当你写一个 volatile 变量之前,Java 内存模型会插入一个写屏障(write barrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。意思就是说,在你写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。

6、ThreadLocal(线程局部变量)关键字

  当使用 ThreadLocal 维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。

  ThreadLocal 内部实现机制:

  • 每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap,里边会包含若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程;
  • Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用即是:为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系;
  • Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用。

技术图片

7、线程池

  java.util.concurrent.ThreadPoolExecutor 类就是一个线程池。客户端调用 ThreadPoolExecutor.submit(Runnable task) 提交任务,线程池内部维护的工作者线程的数量就是该线程池的线程池大小,有 3 种形态:

  • 当前线程池大小 :表示线程池中实际工作者线程的数量。
  • 最大线程池大小 (maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限。
  • 核心线程大小 (corePoolSize ):表示一个不大于最大线程池大小的工作者线程数量上限。

形态解析:

  • 如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
  • 如果运行的线程等于或者多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不是添加新线程。
  • 如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出 maxinumPoolSize, 在这种情况下,任务将被拒绝。

8、为什么要使用线程池?

  • 减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程大约需要 1 MB 内存,线程开的越多,消耗的内存也就越大,最后死机)

9、核心线程池内部实现

  对于核心的几个线程池,无论是 newFixedThreadPool() 方法,newSingleThreadExecutor() 还是 newCachedThreadPool() 方法,虽然看起来创建的线程有着完全不同的功能特点,但其实内部实现均使用了 ThreadPoolExecutor 实现,其实都只是 ThreadPoolExecutor 类的封装

  为何 ThreadPoolExecutor 有如此强大的功能呢?我们可以来看一下 ThreadPoolExecutor 最重要的构造函数:

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

函数的参数含义如下:

  • corePoolSize:指定了线程池中的线程数量
  • maximumPoolSize:指定了线程池中的最大线程数量
  • keepAliveTime:当线程池线程数量超过corePoolSize 时,多余的空闲线程的存活时间。即,超过了 corePoolSize 的空闲线程,在多长时间内,会被销毁。
  • unit: keepAliveTime 的单位。
  • workQueue:任务队列,被提交但尚未被执行的任务。
  • threadFactory:线程工厂,用于创建线程,一般用默认的即可。
  • handler:拒绝策略。当任务太多来不及处理,如何拒绝任务。

10、Atomic关键字

  可以使基本数据类型以原子的方式实现自增自减等操作。

  对于原子操作类,最大的特点是在多线程并发操作同一个资源的情况下,使用Lock-Free算法来替代锁,这样开销小、速度快,对于原子操作类是采用原子操作指令实现的,从而可以保证操作的原子性。什么是原子性?比如一个操作i++;实际上这是三个原子操作,先把i的值读取、然后修改(+1)、最后写入给i。所以使用Atomic原子类操作数,比如:i++;那么它会在这步操作都完成情况下才允许其它线程再对它进行操作,而这个实现则是通过Lock-Free+原子操作指令来确定的 。

  用法:

public class Counter {
    private AtomicInteger count = new AtomicInteger();

    public int getCount() {
        return count.get();
    }

    public void increment() {
        count.incrementAndGet();
    }
}

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

  • 实现Runnable接口,然后将它传递给Thread的构造函数,创建一个Thread对象
  • 直接继承Thread类

两种方式的区别

继承方式:

  • (1)Java中类是单继承的,如果继承了Thread了,该类就不能再有其他的直接父类了.
  • (2)从操作上分析,继承方式更简单,获取线程名字也简单.(操作上,更简单).
  • (3)从多线程共享同一个资源上分析,继承方式不能做到.

实现方式:

  • (1)Java中类可以多实现接口,此时该类还可以继承其他类,并且还可以实现其他接口(设计上,更优雅).
  • (2)从操作上分析,实现方式稍微复杂点,获取线程名字也比较复杂,得使用Thread.currentThread()来获取当前线程的引用.
  • (3)从多线程共享同一个资源上分析,实现方式可以做到(是否共享同一个资源).

12、run() 方法和 start() 方法有什么区别?

  • start() 方法会新建一个线程并让这个线程执行 run() 方法。
  • run() 方法只是作为一个普通的方法调用而已,它只会在当前线程中,串行执行 run() 中的代码。

13、怎么理解线程优先级?

  Java 中的线程可以有自己的优先级。优先极高的线程在竞争资源时会更有优势,更可能抢占资源,当然,这只是一个概率问题。如果运行不好,高优先级线程可能也会抢占失败。

  由于线程的优先级调度和底层操作系统有密切的关系,在各个平台上表现不一,并且这种优先级产生的后果也可能不容易预测,无法精准控制,比如一个低优先级的线程可能一直抢占不到资源,从而始终无法运行,而产生饥饿(虽然优先级低,但是也不能饿死它啊)。因此,在要求严格的场合,还是需要自己在应用层解决线程调度的问题。

  在 Java 中,使用 1 到 10 表示线程优先级,一般可以使用内置的三个静态标量表示:

技术图片

  数字越大则优先级越高,但有效范围在 1 到 10 之间,默认的优先级为 5 。 

14、在 Java 中如何停止一个线程?

  • Java 提供了很丰富的 API 但没有为停止线程提供 API 。
  • JDK 1.0 本来有一些像 stop(),suspend() 和 resume() 的控制方法但是由于潜在的死锁威胁因此在后续的 JDK 版本中他们被弃用了,之后 Java API 的设计者就没有提供一个兼容且线程安全的方法来停止任何一个线程。
  • 当 run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用 volatile 布尔变量来退出 run() 方法的循环或者是取消任务来中断线程。

例:

 //被volatile修饰的变量发生修改时,所有使用该变量的线程都会停止使用缓存中的变量,同时停止工作去主存中获取最新的volatile变量值(轻量的锁)
    private static volatile Boolean stop = false;

    public static void main(String args[]) throws InterruptedException {
        //新建立一个线程
        Thread testThread = new Thread() {
            @Override
            public void run() {
                System.out.println();
                int i = 1;
                //不断的对i进行自增操作
                while (!stop) {
                    i++;
                }
                System.out.println("Thread stop i=" + i);
            }
        };
        //启动该线程
        testThread.start();
        //休眠一秒
        Thread.sleep(1000);
        //主线程中将stop置为true
        stop = true;
        System.out.println(Thread.currentThread() + "now, in main thread stop is: " + stop);
        testThread.join();
    }

15、多线程中的忙循环是什么?

  忙循环就是程序员用循环让一个线程等待,不像传统方法 wait(),sleep() 或yield() 它们都放弃了 CPU 控制权,而忙循环不会放弃 CPU,它就是在运行一个空循环。这么做的目的是为了保留 CPU 缓存

  在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存,为了避免重建缓存和减少等待重建的时间就可以使用它了。

16、你是如何调用 wait()方法的?使用 if 块还是循环?为什么?

  wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足(此时用if判断可能会有问题),所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:

// The standard idiom for using the wait method
synchronized (obj) {
while (condition does not hold)
obj.wait(); // (Releases lock, and reacquires on wakeup)
... // Perform action appropriate to condition
}

17、什么是多线程环境下的伪共享(false sharing)?

  伪共享是多线程系统(每个处理器有自己的局部缓存)中一个众所周知的性能问题。伪共享发生在不同处理器的上的线程对变量的修改依赖于相同的缓存行,如下图所示:

技术图片

  伪共享问题很难被发现,因为线程可能访问完全不同的全局变量,内存中却碰巧在很相近的位置上。如其他诸多的并发问题,避免伪共享的最基本方式是仔细审查代码,根据缓存行来调整你的数据结构。

以上是关于Java基础学习多线程实战面试题形式的主要内容,如果未能解决你的问题,请参考以下文章

JAVA基础再回首(二十五)——Lock锁的使用死锁问题多线程生产者和消费者线程池匿名内部类使用多线程定时器面试题

面试题:多线程

106道Java并发和多线程基础面试题大集合(2w字),这波面试稳了~

Java面试题2021,Java多线程实战指南pdf下载

学习JAVA都要掌握哪些东西

Java工程师面试题,二级java刷题软件