Java 多线程面试题

Java 多线程面试题

线程和进程区别?

从本质上来说,线程是进程的实际执行单元,一个程序至少有一个进程,一个进程至少有一个线程,它们的区别主要体现在以下几个方面:

进程间是独立的,不能共享内存空间和上下文,而线程可以;
进程是程序的一次执行,线程是进程中执行的一段程序片段;
线程占用的资源比进程少。

程序的运行必须依靠进程,进程的实际执行单元就是线程。
多线程可以提高程序的执行性能。

如何保证一个线程执行完再执行第二个线程?

使用 join() 方法,等待上一个线程的执行完之后,再执行当前线程。
使并行变成串行执行。

创建线程

线程的创建,分为以下三种方式:

  • 1 继承 Thread 类,重写 run 方法
  • 2 实现 Runnable 接口,实现 run 方法
  • 3 实现 Callable 接口,实现 call 方法

1 继承 Thread 类

class ThreadDemo {
    public static void main(String[] args) throws Exception {
        MyThread thread = new MyThread();
        thread.start();
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread........");
    }
}

输出

Thread........

2 实现 Runnable 接口

class ThreadDemo{
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        new Thread(runnable).start();
    }
}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable......");
    }
}

输出

Runnable......

3 实现 Callable 接口

public class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Callable ..... ";
    }


    public static void main(String[] args) {


        MyThread myThread = new MyThread();
        //用来定义线程返回结果
        FutureTask<String> futureTask = new FutureTask<>(myThread);

        Thread thread = new Thread(futureTask);
        //启动线程
        thread.start();

        try {
            //获取结果
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

输出

Callable ..... 

Callable 的调用是可以有返回值的、Runanle接口是没有返回值的。

线程中的 start() 和 run() 有那些区别

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

线程常用方法

1 线程等待

   		 Object object = new Object();

        new Thread(()->{
            synchronized (object){
                System.out.println(  LocalDateTime.now().toString());
                try {
                    //等待2秒
                    object.wait(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println( LocalDateTime.now().toString());
            }
        }).start();

输出

2020-04-21T21:08:19.430
2020-04-21T21:08:21.432

先输出2020-04-21T21:08:19.430 然后2s之后 输出2020-04-21T21:08:21.432

当使用 wait() 方法时,必须先持有当前对象的锁,否则会抛出异常 java.lang.IllegalMonitorStateException。

2 线程唤醒

使用 notify()/notifyAll() 方法唤醒线程。

  • notify() 方法随机唤醒对象的等待池中的一个线程;
  • notifyAll() 唤醒对象的等待池中的所有线程。

注意: wait() 、 notify()/notifyAll() 都是Object类的方法,sleep()是Thread类的方法

Object lock = new Object();
lock.wait();
lock.notify();

3 线程休眠

 try {
            System.out.println("执行前");
            Thread.sleep(1000);
            System.out.println("执行后");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

输出

执行前
执行后

4 yield 交出 CPU 执行权

yield 方法是让同优先级的线程有执行的机会,但不能保证自己会从正在运行的状态迅速转换到可运行的状态。

5 线程中断

使用 System.exit(0) 可以让整个程序退出;禁止在程序中使用
中断单个线程 interrupt()对线程进行“中断”。

可以使用 isInterrupted() 判断线程是否被打断。

   Thread t = new Thread() {
            @Override
            public void run() {
                for (int i = 1; i < 1000; i++) {
                    // 让同优先级的线程有执行的机会
                    if (this.isInterrupted()) {
                        break;
                    }
                    System.out.println(i);

                }
            }
        };

        t.start();

        Thread.sleep(10);

        t.interrupt();
    }

我机器输出到753的时候线程停止了。

Java 多线程面试题_第1张图片

6 线程优先级

使用 setPriority 方法设置(1-10)优先级,默认的优先级是 5,数字越大表示优先级越高。
优先级高但不一定就表示执行优先,仅仅是发生的概率大而已。

Thread thread = new Thread(() ->{}); //jdk8 创建线程
 t.setPriority(10);
        t.interrupt();

线程的常用方法如下

  • currentThread():返回当前正在执行的线程引用
  • getName():返回此线程的名称
  • setPriority()/getPriority():设置和返回此线程的优先级
  • isAlive():检测此线程是否处于活动状态,活动状态指的是程序处于正在运行或准备运行的状态
  • sleep():使线程休眠
  • join():等待线程执行完成
  • yield():让同优先级的线程有执行的机会,但不能保证自己会从正在运行的状态迅速转换到可运行的状态
  • interrupted():是线程处于中断的状态,但不能真正中断线程

死锁

死锁是指两个或者两个以上的线程互相持有对方的锁,互相等待对方释放锁,然后一直等下去这种情况叫做死锁。

实现代码


        Object locak1 = new Object();
        Object locak2 = new Object();


        new Thread() {
            @Override
            public void run() {
                synchronized (locak1) {

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //获取locak2的锁  但是被第二个线程占用了
                    synchronized (locak2) {

                    }


                }
            }
        }.start();


        new Thread() {
            @Override
            public void run() {
                synchronized (locak2) {

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //获取locak1的锁  但是被第一个线程占用了
                    synchronized (locak1) {
                    }
                }
            }
        }.start();


查看死锁
用JDk命令 先查看运行的类 和pid
Java 多线程面试题_第2张图片
然后利用 jstack pid 查看死锁

Java 多线程面试题_第3张图片

如何预防死锁?

  • 尽量使用 tryLock(long timeout, TimeUnit unit) 的方法 (ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁;
  • 尽量使用 Java. util. concurrent 并发类代替自己手写锁;
  • 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁;
  • 尽量减少同步的代码块。

wait() 和 sleep() 有什么区别?

存在类的不同:sleep() 来自 Thread,wait() 来自 Object。
释放锁:sleep() 不释放锁;wait() 释放锁。
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll() 直接唤醒。

守护线程是什么?

守护线程是一种比较低级别的线程,一般用于为其他类别线程提供服务,因此当其他线程都退出时,它也就没有存在的必要了。例如,JVM(Java 虚拟机)中的垃圾回收线程。

线程有哪些状态?

在 JDK 8 中,线程的状态有以下六种。

  • NEW:尚未启动
  • RUNNABLE:正在执行中
  • BLOCKED:阻塞(被同步锁或者 IO 锁阻塞)
  • WAITING:永久等待状态
  • TIMED_WAITING:等待指定的时间重新被唤醒的状态
  • TERMINATED:执行完成

线程的调度策略?

  • 线程体中调用了 yield() 方法,让出了对 CPU 的占用权;
  • 线程体中调用了 sleep() 方法,使线程进入睡眠状态;
  • 线程由于 I/O 操作而受阻塞;
  • 另一个更高优先级的线程出现;
  • 在支持时间片的系统中,该线程的时间片用完。

ThreadPoolExecutor 线程池面试题

1 ThreadPoolExecutor 有哪些常用的方法?

  • submit()/execute():执行线程池
  • shutdown()/shutdownNow():终止线程池
  • isShutdown():判断线程是否终止
  • getActiveCount():正在运行的线程数
  • getCorePoolSize():获取核心线程数
  • getMaximumPoolSize():获取最大线程数
  • getQueue():获取线程池中的任务队列
  • allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程

2 在 ThreadPool 中 submit() 和 execute() 有什么区别?

submit() 和 execute() 都是用来执行线程池的,只不过使用 execute() 执行线程池不能有返回方法,而使用 submit() 可以使用 Future 接收线程池执行的返回值。

3 ThreadPoolExecutor 都需要哪些参数?

ThreadPoolExecutor 最多包含以下七个参数:

  • corePoolSize:线程池中的核心线程数
  • maximumPoolSize:线程池中最大线程数
  • keepAliveTime:闲置超时时间
  • unit:keepAliveTime 超时时间的单位(时/分/秒等)
  • workQueue:线程池中的任务队列
  • threadFactory:为线程池提供创建新线程的线程工厂
  • rejectedExecutionHandler:线程池任务队列超过最大值之后的拒绝策略

4 在线程池中 shutdownNow() 和 shutdown() 有什么区别?

shutdownNow() 和 shutdown() 都是用来终止线程池的
区别

  • 使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;
  • shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

5 说一说线程池的工作原理?

当线程池中有任务需要执行时,线程池会判断如果线程数量没有超过核心数量就会新建线程池进行任务执行,如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;如果任务队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;如果超过了最大线程数,就会执行拒绝执行策略。

Executors 创建线程池面试题

Executors 可以创建以下六种线程池。

  • FixedThreadPool(n):创建一个数量固定的线程池,超出的任务会在队列中等待空闲的线程,可用于控制程序的最大并发数。
  • CachedThreadPool():短时间内处理大量工作的线程池,会根据任务数量产生对应的线程,并试图缓存线程以便重复使用,如果限制 60 秒没被使用,则会被移除缓存。
  • SingleThreadExecutor():创建一个单线程线程池。
  • ScheduledThreadPool(n):创建一个数量固定的线程池,支持执行定时性或周期性任务。
  • SingleThreadScheduledExecutor():此线程池就是单线程的 newScheduledThreadPool。
  • WorkStealingPool(n):Java 8 新增创建线程池的方法,创建时如果不设置任何参数,则以当前机器处理器个数作为线程个数,此线程池会并行处理任务,不能保证执行顺序。

1 Executors 能创建单线程的线程池吗?怎么创建?

Executors 可以创建单线程线程池,创建分为两种方式:

  • Executors.newSingleThreadExecutor():创建一个单线程线程池。
  • Executors.newSingleThreadScheduledExecutor():创建一个可以执行周期性任务的单线程池。

2 Executors 中哪个线程适合执行短时间内大量任务?

newCachedThreadPool() 适合处理大量短时间工作任务。它会试图缓存线程并重用,如果没有缓存任务就会新创建任务,如果线程的限制时间超过六十秒,则会被移除线程池,因此它比较适合短时间内处理大量任务。

3 可以执行周期性任务的线程池都有哪些?

可执行周期性任务的线程池有两个,分别是:newScheduledThreadPool() 和 newSingleThreadScheduledExecutor(),其中 newSingleThreadScheduledExecutor() 是 newScheduledThreadPool() 的单线程版本。

4 可以执行周期性任务的线程池都有哪些?

可执行周期性任务的线程池有两个,分别是:newScheduledThreadPool() 和 newSingleThreadScheduledExecutor(),其中 newSingleThreadScheduledExecutor() 是 newScheduledThreadPool() 的单线程版本。

5 JDK 8 新增了什么线程池?有什么特点?

JDK 8 新增的线程池是 newWorkStealingPool(n),如果不指定并发数(也就是不指定 n),newWorkStealingPool() 会根据当前 CPU 处理器数量生成相应个数的线程池。它的特点是并行处理任务的,不能保证任务的执行顺序。

6 newFixedThreadPool 和 ThreadPoolExecutor 有什么关系?

newFixedThreadPool 是 ThreadPoolExecutor 包装,newFixedThreadPool 底层也是通过 ThreadPoolExecutor 实现的。

7 单线程的线程池存在的意义是什么?

单线程线程池提供了队列功能,如果有多个任务会排队执行,可以保证任务执行的顺序性。单线程线程池也可以重复利用已有线程,减低系统创建和销毁线程的性能开销。

8.线程池为什么建议使用 ThreadPoolExecutor 创建,而非 Executors?

使用 ThreadPoolExecutor 能让开发者更加明确线程池的运行规则,避免资源耗尽的风险。

Executors 返回线程池的缺点如下:

  • FixedThreadPool 和 SingleThreadPool 允许请求队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,可能会导致内存溢出;
  • CachedThreadPool 和 ScheduledThreadPool 允许创建线程数量为 Integer.MAX_VALUE,创建大量线程,可能会导致内存溢出。

Java ThreadLocal 面试题

1.ThreadLocal 为什么是线程安全的?

ThreadLocal 为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内,因此 ThreadLocal 是线程安全的,每个线程都有属于自己的变量。

2.ThreadLocal 如何共享数据?

通过 ThreadLocal 的子类 InheritableThreadLocal 可以天然的支持多线程间的信息共享。

InheritableThreadLocal 存储数据使用的是是 InheritableThreadLocal
ThreadLocal 存储数据使用的是是 ThreadLocal

3 ThreadLocal 为什么会发生内存溢出?

ThreadLocal 造成内存溢出的原因:如果 ThreadLocal 没有被直接引用(外部强引用),在 GC(垃圾回收)时,由于 ThreadLocalMap 中的 key 是弱引用,所以一定就会被回收,这样一来 ThreadLocalMap 中就会出现 key 为 null 的 Entry,并且没有办法访问这些数据,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 并且永远无法回收,从而造成内存泄漏。

4 解决 ThreadLocal 内存溢出的关键代码是什么?

关键代码为 threadLocal.remove() ,使用完 ThreadLocal 之后,调用remove() 方法,清除掉 ThreadLocalMap 中的无用数据就可以避免内存溢出了。

5 ThreadLocal 和 Synchonized 有什么区别?

答:ThreadLocal 和 Synchonized 都用于解决多线程并发,防止任务在共享资源上产生冲突,但是 ThreadLocal 与 Synchronized 有本质的区别,

  • Synchronized 用于实现同步机制,是利用锁的机制使变量或代码块在某一时刻只能被一个线程访问,是一种 “以时间换空间” 的方式;
  • ThreadLocal 为每一个线程提供了独立的变量副本,这样每个线程的(变量)操作都是相互隔离的,这是一种 “以空间换时间” 的方式。

Java ReentrantLock 锁面试题

1.ReentrantLock 常用的方法有哪些?

  • lock():用于获取锁
  • unlock():用于释放锁
  • tryLock():尝试获取锁
  • getHoldCount():查询当前线程执行 lock() 方法的次数
  • getQueueLength():返回正在排队等待获取此锁的线程数
  • isFair():该锁是否为公平锁

2 ReentrantLock 有哪些优势?

ReentrantLock 具备非阻塞方式获取锁的特性,使用 tryLock() 方法。ReentrantLock 可以中断获得的锁,使用 lockInterruptibly() 方法当获取锁之后,如果所在的线程被中断,则会抛出异常并释放当前获得的锁。ReentrantLock 可以在指定时间范围内获取锁,使用 tryLock(long timeout,TimeUnit unit) 方法。

3.ReentrantLock 怎么创建公平锁?

答:new ReentrantLock() 默认创建的为非公平锁,如果要创建公平锁可以使用 new ReentrantLock(true)。

4.公平锁和非公平锁有哪些区别?

答:公平锁指的是线程获取锁的顺序是按照加锁顺序来的,而非公平锁指的是抢锁机制,先 lock() 的线程不一定先获得锁。

5.ReentrantLock 中 lock() 和 lockInterruptibly() 有什么区别?

答:lock() 和 lockInterruptibly() 的区别在于获取线程的途中如果所在的线程中断,lock() 会忽略异常继续等待获取线程,而 lockInterruptibly() 则会抛出 InterruptedException 异常。

6.synchronized 和 ReentrantLock 有什么区别?

答:synchronized 和 ReentrantLock 都是保证线程安全的,它们的区别如下:

  • ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
  • ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
  • ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等;
  • ReentrantLock 性能略高于 synchronized。

7.ReentrantLock 的 tryLock(3, TimeUnit.SECONDS) 表示等待 3 秒后再去获取锁,这种说法对吗?为什么?

答:不对,tryLock(3, TimeUnit.SECONDS) 表示获取锁的最大等待时间为 3 秒,期间会一直尝试获取,而不是等待 3 秒之后再去获取锁。

8.synchronized 是如何实现锁升级的?

答:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,JVM(Java 虚拟机)让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否尤其线程 id 一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,不会阻塞,执行一定次数之后就会升级为重量级锁,进入阻塞,整个过程就是锁升级的过程。

Java 并发包中的高级同步工具

… 待定 目前我不会

Java 中的各种锁和 CAS

乐观锁和悲观锁

悲观锁和乐观锁并不是某个具体的“锁”而是一种并发编程的基本概念。乐观锁和悲观锁最早出现在数据库的设计当中,后来逐渐被 Java 的并发包所引入。

悲观锁

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观地认为,不加锁的并发操作一定会出问题。

乐观锁

乐观锁正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新。

公平锁和非公平锁

根据线程获取锁的抢占机制,锁又可以分为公平锁和非公平锁。

  • 公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。

  • 非公平锁

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。

ReentrantLock 提供了公平锁和非公平锁的实现。

公平锁:new ReentrantLock(true)
非公平锁:new ReentrantLock(false)

如果构造函数不传任何参数的时候,默认提供的是非公平锁。

独占锁和共享锁

根据锁能否被多个线程持有,可以把锁分为独占锁和共享锁。

  • 独占锁

独占锁是指任何时候都只有一个线程能执行资源操作。

  • 共享锁

共享锁指定是可以同时被多个线程读取,但只能被一个线程修改。比如 Java 中的 ReentrantReadWriteLock 就是共享锁的实现方式,它允许一个线程进行写操作,允许多个线程读操作。

可重入锁 和 自旋锁

  • 可重入锁

可重入锁指的是该线程获取了该锁之后,可以无限次的进入该锁锁住的代码。

  • 自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。

CAS 与 ABA

CAS(Compare and Swap)比较并交换,是一种乐观锁的实现,是用非阻塞算法来代替锁定,其中 java.util.concurrent 包下的 AtomicInteger 就是借助 CAS 来实现的。
但 CAS 也不是没有任何副作用,比如著名的 ABA 问题就是 CAS 引起的。

1.synchronized 是哪种锁的实现?为什么?

synchronized 是悲观锁的实现,因为 synchronized 修饰的代码,每次执行时会进行加锁操作,同时只允许一个线程进行操作,所以它是悲观锁的实现。

2.new ReentrantLock() 创建的是公平锁还是非公平锁?

非公平锁

3.synchronized 使用的是公平锁还是非公平锁?

synchronized 使用的是非公平锁,并且是不可设置的。这是因为非公平锁的吞吐量大于公平锁,并且是主流操作系统线程调度的基本选择,所以这也是 synchronized 使用非公平锁原由。

4.为什么非公平锁吞吐量大于公平锁?

比如 A 占用锁的时候,B 请求获取锁,发现被 A 占用之后,堵塞等待被唤醒,这个时候 C 同时来获取 A 占用的锁,如果是公平锁 C 后来者发现不可用之后一定排在 B 之后等待被唤醒,而非公平锁则可以让 C 先用,在 B 被唤醒之前 C 已经使用完成,从而节省了 C 等待和唤醒之间的性能消耗,这就是非公平锁比公平锁吞吐量大的原因。

5.volatile 的作用是什么?

volatile 是 Java 虚拟机提供的最轻量级的同步机制。
当变量被定义成 volatile 之后,具备两种特性:

保证此变量对所有线程的可见性,当一条线程修改了这个变量的值,修改的新值对于其他线程是可见的(可以立即得知的);
禁止指令重排序优化,普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。

6.volatile 对比 synchronized 有什么区别?

synchronized 既能保证可见性,又能保证原子性,而 volatile 只能保证可见性,无法保证原子性。比如,i++ 如果使用 synchronized 修饰是线程安全的,而 volatile 会有线程安全的问题。

7.CAS 是如何实现的?

CAS(Compare and Swap)比较并交换,CAS 是通过调用 JNI(Java Native Interface)的代码实现的,比如,在 Windows 系统 CAS 就是借助 C 语言来调用 CPU 底层指令实现的。

8.CAS 会产生什么问题?应该怎么解决?

CAS 是标准的乐观锁的实现,会产生 ABA 的问题(详见正文)。
ABA 通常的解决办法是添加版本号,每次修改操作时版本号加一,这样数据对比的时候就不会出现 ABA 的问题了。

你可能感兴趣的:(面试题)