# 线程、线程池和锁面试题

# 1. 线程

# 线程和进程有什么区别和联系?

答:

  • 联系:

    • 进程是线程的『上级』和『容器』,一个进程中可以有一个或多个线程(至少一个)

    • 线程概念是进程概念的轻量化,很多线程概念和进程大概念都是一脉相承。

  • 区别:

    • 进程概念要比线程概念出现更早;

    • 进程与进程间是隔离的,不能共享内存空间和上下文。但是,(因为同属一个进程)一个进程下的线程缺可以;

    • 线程占用资源比进程更少,切换线程比切换进程代价更小;

    • 进程是程序的一次执行,线程是程序中的部分代码、部分逻辑的执行。

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

答:

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

示例代码:

Thread joinThread = new Thread(() -> {
    try {
        System.out.println("执行前");
        Thread.sleep(1000);
        System.out.println("执行后");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
joinThread.start();
joinThread.join();
System.out.println("主程序");

# 线程有哪些常用的方法?

答:线程的常用方法如下:

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

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

答:

  1. 从来源看:sleep() 来自 Thread ,wait() 来自 Object 。

  2. 从线程状态看:sleep() 导致当前线程进入 TIMED_WAITING 状态,wait() 导致当前线程进入 WAITING 状态。

  3. 从恢复执行看:sleep() 在指定时间之后,线程会恢复执行,wait() 则需要等待别的线程使用 notify()/notifyAll() 来唤醒它。(这和上述 2 有关)。

  4. 从锁看:如果当前线程持有锁,sleep() 不会释放锁,wait() 会释放锁。

# 守护线程是什么?

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

# 线程有哪些状态?

答:在 JDK 8 中,线程的状态有以下 6 种。

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

这里需要说明的是:经典操作系统线程核心三态是 Runnable、Running 和 Blocked,而 Java 线程的状态中与之对应的是:Runnable、Blocked、Waiting、Timed_Waiting。

  • Java 线程状态没有区分 Runnable 和 Running,把它们都归于 Runnable 。

  • Java 把 Blocked 状态细分为:Blocked、Waiting 和 Timed_Waiting 三种。

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

答:

  • start() 方法用于启动线程; run() 方法用于执行线程的运行时代码。

  • run() 可以重复调用,而 start() 只能调用一次。

# 产生死锁需要具备哪些条件?

答:产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个线程使用;

  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;

  • 不剥夺条件:线程已获得的资源,在末使用完之前,不能强行剥夺;

  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系;

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

简而言之一句话:每个线程拥有部分资源,不愿意放弃已有资源,并且在等待别人释放资源。

# 如何预防死锁?

答:预防死锁的方法如下:

  • 尽量使用 tryLock(long timeout, TimeUnit unit) 的方法 (ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁;

  • 尽量使用 Java.util.concurrent 并发类代替自己手写锁;

  • 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁;

  • 优化代码逻辑,如果无法获取全部资源,就释放以获得资源,并重新开始获取资源。

# 如何让两个程序依次输出 11/22/33 等数字,请写出实现代码?

答:使用思路是在每个线程输出信息之后,让当前线程等待一会再执行下一次操作,具体实现代码如下:

new Thread(() -> {
    for (int i = 1; i < 4; i++) {
        System.out.println("线程一:" + i);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

new Thread(() -> {
    for (int i = 1; i < 4; i++) {
        System.out.println("线程二:" + i);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

程序执行结果如下:

线程一:1
线程二:1
线程二:2
线程一:2
线程二:3
线程一:3

# 说一下线程的调度策略?

答:线程调度器选择优先级最高的线程运行,但是如果发生以下情况,就会终止线程的运行:

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

# 2. 线程池:ThreadPoolExecutor

# ThreadPoolExecutor 有哪些常用的方法?

答:常用方法如下所示:

   getCorePoolSize():获取核心线程数
getMaximumPoolSize():获取最大线程数
    getActiveCount():正在运行的线程数
          getQueue():获取线程池中的任务队列
        isShutdown():判断线程是否终止
            submit():执行线程池
           execute():执行线程池
          shutdown(): 终止线程池
       shutdownNow():终止线程池

# 以下程序执行的结果是什么?

ThreadPoolExecutor threadPoolExecutor = 
    new ThreadPoolExecutor(2, 10, 
        10L, TimeUnit.SECONDS, 
        new LinkedBlockingQueue());
    
threadPoolExecutor.execute(new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println("I:" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
});

threadPoolExecutor.shutdownNow();
System.out.println("Java");

答:程序执行的结果是:

I:0
Java
java.lang.InterruptedException: sleep interrupted(报错信息)
I:1

题目解析:因为程序中使用了 shutdownNow() 会导致程序执行一次之后报错,抛出 sleep interrupted 异常,又因为本身有 try/catch,所以程序会继续执行打印 I:1

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

答:submit() 和 execute() 都是用来执行线程池的,不同的是:

  • 使用 execute() 执行线程池不能有返回方法;
  • 使用 submit() 可以使用 Future 接收线程池执行的返回值。

# 说一下 ThreadPoolExecutor 都需要哪些参数?

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

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

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

答:shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是:

  • 使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;

  • shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

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

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

# 以下线程名称被打印了几次?

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
        10L, TimeUnit.SECONDS, 
        new LinkedBlockingQueue<Runnable>(2),
        new ThreadPoolExecutor.DiscardPolicy());
threadPool.allowCoreThreadTimeOut(true);
for (int i = 0; i < 10; i++) {
    threadPool.execute(new Runnable() {
        @Override
        public void run() {
            // 打印线程名称
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

答:线程名被打印了 3 次。

线程池第 1 次执行任务时,会新创建任务并执行;第 2 次执行任务时,因为没有空闲线程所以会把任务放入队列;第 3 次同样把任务放入队列,因为队列最多可以放两条数据,所以第 4 次之后的执行都会被舍弃(没有定义拒绝策略),于是就打印了 3 次线程名称。

# 3. 线程池:Executors

# 以下程序会输出什么结果?

public static void main(String[] args) {
    ExecutorService workStealingPool = Executors.newWorkStealingPool();
    for (int i = 0; i < 5; i++) {
        int finalNumber = i;
        workStealingPool.execute(() -> {
            System.out.print(finalNumber);
        });
    }
}

A:不输出任何结果
B:输出 0 到 9 有序数字
C:输出 0 到 9 无需数字
D:以上全对

答:A

newWorkStealingPool 内部实现是 ForkJoinPool,它会随着主程序的退出而退出,因为主程序没有任何休眠和等待操作,程序会一闪而过,不会执行任何信息,所以也就不会输出任何结果。

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

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

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

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

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

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

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

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

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

# newFixedThreadPool 和 ThreadPoolExecutor 有什么关系?

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

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

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

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

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

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

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

# 4. ThreadLocal

# ThreadLocal 为什么是线程安全的?

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

# 以下程序打印的结果是 true 还是 false?

ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("老王");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("老王");
new Thread(() -> {
    System.out.println(threadLocal.get().equals(threadLocal2.get()));
}).start();

答:false。

因为 threadLocal 使用的是 InheritableThreadLocal(共享本地线程),所以 threadLocal.get() 结果为 老王 ,而 threadLocal2 使用的是 ThreadLocal,因此在新线程中 threadLocal2.get() 的结果为 null ,因而它们比较的最终结果为 false。

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

答:

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

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

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

# ThreadLocal 和 Synchonized 有什么区别?

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

  • Synchronized 用于实现同步机制,是利用锁的机制使变量或代码块在某一时刻只能被一个线程访问,是一种 以时间换空间 的方式;

  • ThreadLocal 为每一个线程提供了独立的变量副本,这样每个线程的(变量)操作都是相互隔离的,这是一种 以空间换时间 的方式。

# 5. synchronized 和 ReentrantLock

# ReentrantLock 常用的方法有哪些?

答:ReentrantLock 常见方法如下:

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

# ReentrantLock 有哪些优势?

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

# ReentrantLock 怎么创建公平锁?

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

# 公平锁和非公平锁有哪些区别?

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

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

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

题目解析:执行以下代码,在线程中分别使用 lock() 和 lockInterruptibly() 查看运行结果,代码如下:

Lock interruptLock = new ReentrantLock();
interruptLock.lock();
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            interruptLock.lock();
            //interruptLock.lockInterruptibly();  
            // java.lang.InterruptedException
        } catch (Exception e) {
           e.printStackTrace();
        }
    }
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
TimeUnit.SECONDS.sleep(3);
System.out.println("Over");
System.exit(0);

执行以下代码会发现使用 lock() 时程序不会报错,运行完成直接退出;而使用 lockInterruptibly() 则会抛出异常 java.lang.InterruptedException,这就说明:在获取线程的途中如果所在的线程中断,lock() 会忽略异常继续等待获取线程,而 lockInterruptibly() 则会抛出 InterruptedException 异常。

# synchronized 和 ReentrantLock 有什么区别?

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

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

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

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

# synchronized 是如何实现锁升级的?

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

# 6. CAS 和各种锁

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

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

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

答:非公平锁。

  • new ReentrantLock() 等同于 new ReentrantLock(false) 它是非公平锁;

  • new ReentrantLock(true) 是公平锁。

# synchronized 使用的是公平锁还是非公平锁?

答:synchronized 使用的是非公平锁,并且是不可设置的。

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

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

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

# volatile 的作用是什么?

答:volatile 是 Java 虚拟机提供的最轻量级的同步机制。

当变量被定义成 volatile 之后,具备两种特性:

  • 保证此变量对所有线程的可见性,当一条线程修改了这个变量的值,修改的新值对于其他线程是可见的(可以立即得知的)

  • 禁止指令重排序优化,普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。

# volatile 对比 synchronized 有什么区别?

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

# CAS 是如何实现的?

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

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

答:CAS 是标准的乐观锁的实现,会产生 ABA 的问题。

ABA 通常的解决办法是添加版本号,每次修改操作时版本号加一,这样数据对比的时候就不会出现 ABA 的问题了。

# 以下说法错误的是?

A:独占锁是指任何时候都只有一个线程能执行资源操作
B:共享锁指定是可以同时被多个线程读取和修改
C:公平锁是指多个线程按照申请锁的顺序来获取锁
D:非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁

答:B

共享锁指定是可以同时被多个线程读取,但只能被一个线程修改。