面试题汇总二 Java 多线程篇

前言

题目汇总来源 史上最全各类面试题汇总,没有之一,不接受反驳

 

  • 面试题汇总一 Java 语言基础篇
  • 面试题汇总二 Java 多线程篇
  • 面试题汇总三 Java 集合篇
  • 面试题汇总四 JVM 篇
  • 面试题汇总五 Spring 篇
  • 面试题汇总六 数据库篇
  • 面试题汇总七 计算机网络篇

目录

前言

多线程

开启线程的三种方式?

什么是Callable和Future?

说说进程,线程,协程之间的区别

进程持有哪些资源

线程共享进程哪些资源

进程切换与线程切换

JVM和操作系统的线程状态及对应关系

java的线程和操作系统的线程有什么关系

什么是Daemon线程?它有什么意义?

在java中守护线程和本地线程区别?

为什么要有线程,而不是仅仅用进程?

什么是线程组,为什么在Java中不推荐使用?

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

Java中用到的线程调度算法是什么?

同步方法和同步块,哪个是更好的选择?

run()和start()方法区别

Java中Semaphore是什么?

如何控制某个方法允许并发访问线程的个数?

在Java中wait和seelp方法的不同

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

什么是不可变对象,它对写并发应用有什么帮助?

谈谈 wait / notify 关键字的理解

为什么wait, notify 和 notifyAll这些方法不在thread类里面?

什么导致线程阻塞?

线程之间是如何通信的?

讲一下java中的同步的方法

lock原理

什么是可重入锁(ReentrantLock)?

ReentrantLock的内部实现

谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解

static synchronized 方法的多线程访问和作用

同一个类里面两个synchronized方法,两个线程同时访问的问题

对象锁和类锁是否会互相影响?

谈谈volatile关键字的作用

volatile关键词在单例模式的双重校验中的作用

synchronized与Lock的区别

ReentrantLock 、synchronized和volatile比较

谈谈ThreadLocal关键字的作用

ThreadLocal、synchronized 和 volatile 关键字的区别

你如何确保main()方法所在的线程是Java程序最后结束的线程?

谈谈NIO的理解

在Java中CycliBarriar和CountDownLatch有什么区别?

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

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

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

死锁的四个必要条件?

什么是线程池,如何使用?

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

四种ExecutorService线程池

线程池四种拒绝策略

BlockingQueue介绍

用Java实现阻塞队列

Java中interrupted 和 isInterrupted方法的区别?

多线程有什么要注意的问题?

如何保证多线程读写文件的安全?

实现生产者消费者模式

Java中的ReadWriteLock是什么?

用Java写一个会导致死锁的程序,你将怎么解决?

SimpleDateFormat是线程安全的吗?

Java中的同步集合与并发集合有什么区别?

Java中ConcurrentHashMap的并发度是什么?

什么是Java Timer类?如何创建一个有特定时间间隔的任务?


 

 

多线程

 

开启线程的三种方式?

3种启动线程的方式

  1. 继承 Thread,覆盖 run();
  2. Thread 构造器传入 Runnable;
  3. Thread 构造器传入用 FutureTask 包装好的 Callable(FutureTask继承了 Runnable 接口),可以从FutureTask中获得返回值。

关于 FutureTask 的知识:

可取消的异步任务——FutureTask用法及解析

Java并发编程:Callable、Future和FutureTask

 

什么是Callable和Future?

相关链接见上一题。

Callable

一个有一个带有返回参数方法的接口。

public interface Callable {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Future

Future 接口表示异步计算的结果

public interface Future {

    // 取消线程,参数表示是否中断线程
    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    // 获取结果,如果这个计算任务还没有执行结束,该调用线程会进入阻塞状态
    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

 

说说进程,线程,协程之间的区别

进程、线程、协程之概念理解

  • 进程是系统进行资源分配和调度的基本单位;
  • 线程是程序执行流的最小单元;
  • 线程切换由系统控制,而协程切换由协程主动让渡。

 

进程持有哪些资源

进程是什么?他拥有哪些资源?

1、虚拟地址空间

2、一个全局唯一的进程ID (PID)

3、一个可执行映像(image),也就是该进程的程序文件在内存中的表示

4、一个或多个线程

5、一个位于内核空间中的名为EPROCESS(executive process block,进程执行块)的数据结构,用以记录该进程的关键信息,包括进程的创建时间、映像文件名称等。

6、一个位于内核空间中的对象句柄表,用以记录和索引该进程所创建/打开的内核对象。操作系统根据该表格将用户模式下的句柄翻译为指向内核对象的指针。

7、一个位于描述内存目录表起始位置的基地址,简称页目录基地址(DirBase),当CPU切换到该进程/任务时,会将该地址加载到CR3寄存器,这样当前进程的虚拟地址才会被翻译为正确的物理地址。

8、一个位于用户空间中的进程环境块(Process Environment Block, PEB)。

9、一个访问权限令牌(access token),用于表示该进程的用户、安全组,以及优先级别。

 

线程共享进程哪些资源

进程和线程的区别,及线程间的公共资源和私有资源

线程共享的资源包括:

  1. 进程代码段;
  2. 进程的公有数据;
  3. 进程的所拥有资源;

线程独立的资源包括:

  1. 线程ID;
  2. 寄存器组的值;
  3. 线程的堆栈;
  4. 错误返回码;
  5. 线程优先级;

 

进程切换与线程切换

进程切换与线程切换的区别?

线程上下文切换和进程上下文切换的区别

进程切换步骤:

  • 切换虚拟地址,即切换页目录以使用新的地址空间;
  • 切换内核栈和硬件上下文;
  • 隐藏开销:页表缓冲TLB清空,导致虚拟地址转换为物理地址速度下降。

线程切换只要切换内核栈和硬件上下文。

 

JVM和操作系统的线程状态及对应关系

Java线程的6种状态及切换(透彻讲解)

Java线程和操作系统线程的关系

Java线程状态:NEW, RUNNABLE(READY, RUNNING), BLOCKED, WAITING, TIMED_WATING, TERMINATED

操作系统线程(进程)状态:

状态的对应关系:

虚拟机中的线程状态,不反应任何操作系统线程状态。

 

java的线程和操作系统的线程有什么关系

Java线程和操作系统线程的关系

对于Sun JDK来说,它的Window版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程中。

 

什么是Daemon线程?它有什么意义?

在java中守护线程和本地线程区别?

守护线程(Daemon Thread)

在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。 

所谓守护 线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

 

为什么要有线程,而不是仅仅用进程?

有了进程为什么还要线程?

我的理解:

进程的出现满足了用户同时运行多道程序的需求;

若没有多线程,一个进程中的每个操作也只能顺序进行,此时运行耗时操作比如 io,会导致整个进程挂起;

线程的出现可以满足在一个进程中同时执行多个操作的需求。

 

什么是线程组,为什么在Java中不推荐使用?

什么是线程组? java线程组和线程

为什么不推荐使用? 2018-04-12-java-为什么不推荐使用线程组

  • 线程组ThreadGroup对象中比较有用的方法是stop、resume、suspend等方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方废弃掉了,所以线程组本身的应用价值就大打折扣了。
  • 线程组ThreadGroup不是线程安全的,这在使用过程中获取的信息并不全是及时有效的,这就降低了它的统计使用价值。

 

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

Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

悲观锁

假设冲突必然会发生,采取一切办法保证数据一致性。Java 中采用 synchronized 关键字保证同步,但多线程环境下频繁切换上下文会导致性能问题。

乐观锁

假设冲突不会发生,只在最后阶段进行一致性验证。两个步骤:冲突检测和数据更新。Java 中的一种实现方式是 CAS。

CAS

package sun.misc;

import ...

public final class Unsafe {
    ...

    public final native boolean compareAndSwapObject(
        Object o, long offset, Object expect, Object update);

    public final native boolean compareAndSwapInt(
        Object o, long offset, int expect, int update);

    public final native boolean compareAndSwapLong(
        Object o, long offset, long expect, long update);

    ...
}

可以看到,cas 可以操作 object、int 和 long,其中 o 和 offset 用于确定对象 o 的待修改成员变量 field,expect 为期望值,update 为更新值。当 field == expect 的时候,field = update 并返回 true;否则不更新,返回 false。

offset 的取值用到了 unsafe 和反射,应该在通常情况下不需要我们直接操作。

stateOffset = unsafe.objectFieldOffset
    (AbstractQueuedSynchronizer.class.getDeclaredField("state"));

CAS的应用:java.util.concurrent

volatile int i = 1;
i++;

i++ 并非原子操作,而是包含读取、+1 和写入三个步骤,因此在多线程环境下可能得不到期望的结果。可以使用 JUC 包下的 AtomicInteger。

AtomicInteger integer = new AtomicInteger(1);
integer.getAndIncrement();

内部实现:

// -------- AtomicInteger --------
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

// -------- Unsafe --------
public final int getAndAddInt(Object o, long offset, int delta) {
    int num;
    do {
        num = this.getIntVolatile(o, offset);
    } while(!this.compareAndSwapInt(o, offset, num, num + delta));

    return num;
}

CAS的应用:AtomicStampedReference

ABA 问题:

  1. 一个变量 int state = 0;
  2. 线程 A 读取到 state = 0;
  3. 线程 B 修改 state = 1 再改回 state = 0;

此时线程 A 再读取 state,还是0,但实际上已经 state 已经被修改过。

对于对改动敏感的变量操作,可以使用 AtomicStampedReference,它会同时保存时间戳以确认是否发生改动。

// -------- AtomicStampedReference --------
public boolean compareAndSet(V expectedReference,
                             V newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair current = pair;
    return expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
                    newStamp == current.stamp) ||
                    casPair(current, Pair.of(newReference, newStamp)));
}

private boolean casPair(Pair cmp, Pair val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

private static class Pair {
    final T reference;
    final int stamp;
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    static  Pair of(T reference, int stamp) {
        return new Pair(reference, stamp);
    }
}

 

Java中用到的线程调度算法是什么?

水中月,浅析Java的线程调度策略

JVM使用抢占的、基于优先权的调度策略。

不过优先级策略不靠谱,因为 JDK 1.2 之后,Java 使用了本地线程模型,线程调度由操作系统执行,优先级会受操作系统的影响。

 

同步方法和同步块,哪个是更好的选择?

同步代码块好。

  • 加锁范围小于同步方法,因此性能更高;
  • 可以任意指定加锁对象,更灵活。

 

run()和start()方法区别

调用 run() 只是在当前线程运行方法,而 start() 才是启动新线程运行。

// -------- Thread --------
public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

private native void start0();

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

 

Java中Semaphore是什么?

如何控制某个方法允许并发访问线程的个数?

使用Semaphore控制某个方法允许并发访问的线程的个数

使用方法↑

获取信号量:

// -------- main(String[] args) --------
semaphore.acquire();

// -------- Semaphore --------
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// -------- AbstractQueuedSynchronizer --------
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 如果尝试结果<0则阻塞
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

// -------- NonfairSync --------
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

// -------- Sync --------
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        // 信号量不足或者成功请求到信号量则返回
        if (remaining < 0 ||
                compareAndSetState(available, remaining))
            return remaining;
    }
}

// 到这里 tryAcquireShared(arg) 完成
// 接下来阻塞线程 doAcquireSharedInterruptibly(arg)

// -------- AbstractQueuedSynchronizer --------
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                // 自旋,直到请求到信号量退出阻塞状态
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

释放信号量:

// -------- main(String[] args) --------
semaphore.release();

// -------- Semaphore --------
public void release() {
    sync.releaseShared(1);
}

// -------- AbstractQueuedSynchronizer --------
public final boolean releaseShared(int arg) {
    // 尝试释放信号量
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

// -------- Sync --------
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        // 这个相比于请求信号量就简单一些,cas成功就返回
        if (compareAndSetState(current, next))
            return true;
    }
}

 

在Java中wait和seelp方法的不同

Java中wait和sleep方法的区别

  • wait 作用于对象,sleep 作用于 Thread
  • wait 释放对象锁,sleep 不释放对象锁
  • wait 运行于同步方法或同步代码块,sleep 可运行于任何地方
  • wait 不需要捕获异常,sleep 需要捕获异常

 

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

暂停当前线程,将当前线程从 Running 状态转到 Ready 状态,让出控制权给其他线程。

 

什么是不可变对象,它对写并发应用有什么帮助?

Java并发编程(六)不可改变对象

对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,任何对它的改变都应该产生一个新的对象。

不可变就不用担心并发问题。

 

谈谈 wait / notify 关键字的理解

wait和notify的理解与使用

  • wait / notify 存在于所有对象;
  • 使用时需要 synchronized,否则 IllegalMonitorStateException;
  • 调用 wait 方法会让当前线程阻塞,让出对象锁;若 wait 有设置时间参数,到时间后自动唤醒;
  • notify 一次唤醒一个等待的线程;notifyAll 一次性唤醒所有该对象上的等待线程。

 

为什么wait, notify 和 notifyAll这些方法不在thread类里面?

wait,notify,notifyAll,sleep这些方法都跟线程的状态变化有关,为什么jdk把前三个方法放在Object类里面,而把sleep放在Thread类里面?

  • Java 内置锁机制中,锁是对象级而不是线程级,任何对象都能创建锁;
  • 一个线程可以有多个锁,若跟线程绑定可能会不够用。

 

什么导致线程阻塞?

线程阻塞方法:

  • object.wait()
  • Thread.sleep()
  • Thread.yield()
  • thread.join()
  • @Deprecated thread.suspend()

还有 synchronized 方法或代码块以及 IO 等。

 

线程之间是如何通信的?

讲一下java中的同步的方法

java中线程同步的几种方法

  • 关键字:synchronized,volatile
  • wait() / notify()
  • ThreadLocal
  • JUC 下的一堆工具

 

lock原理

Java并发编程:Lock

与 synchronized 不同,Lock 是基于JDK的一系列类。

public interface Lock {
    // 上锁,获取不到锁则阻塞
    void lock();
    // 上锁,且若线程阻塞可中断
    void lockInterruptibly() throws InterruptedException;
    // 上锁,获取不到锁直接返回false
    boolean tryLock();
    // 上锁,在规定时间内获取不到锁返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 解锁
    void unlock();
    // 获得新的 Condition,用于精准控制线程休眠与唤醒
    Condition newCondition();
}

不像 synchronized,Lock 需要手动解锁,因此使用时最好用 try ... catch ... finally 包围,将解锁写入 finally。

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

Condition

线程高级篇-Lock锁和Condition条件

Condition 中的 await() 方法相当于 Object 的 wait() 方法,Condition 中的 signal() 方法相当于 Object 的 notify() 方法,Condition 中的 signalAll() 相当于 Object 的 notifyAll() 方法。

Condition 可以进行更精细的线程休眠和唤醒控制。一个 Lock 可以针对不同条件,创建多个 Condition。

 

什么是可重入锁(ReentrantLock)?

ReentrantLock的内部实现

ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。

内部实现

简易版看这篇:

彻底理解ReentrantLock

详细版看这篇:

ReentrantLock实现原理深入探究

 

谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解

java synchronized关键字的用法

Java的内置锁

每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。若一个线程进入该方法或方法块,此时锁被其他线程占用,则发生阻塞。

对象锁

若 synchronized 关键词修饰普通方法或方法中的代码块,则获取的是一个对象的对象锁。对于方法,获取的是被调用对象的对象锁,而方法块可以获取指定对象的对象锁。

public class MyClass {
    public synchronized void test1() {
        // 获取被调用对象的对象锁
    }

    public void test2() {
        synchronized (this) {
            // 获取指定对象的对象锁
        }
    }
}

类锁

若 synchronized 关键词修饰 static 方法或方法中的代码块,则获取的是一个类锁。类锁与对象锁执行时不冲突。

public class MyClass {
    public synchronized static void test3() {
        // 获取该类的类锁
    }
}

synchronized可重入性

同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。

 

static synchronized 方法的多线程访问和作用

见上题。对类施加的锁,针对所有线程执行该类添加 synchronized 关键词的静态方法或方法块。

 

同一个类里面两个synchronized方法,两个线程同时访问的问题

如果两个方法都是非 static 或都是 static,其中一个线程会发生阻塞,等待另一个线程方法执行完毕。

如果一个 static 另一个非 static,则能并行执行。

 

对象锁和类锁是否会互相影响?

不会

 

谈谈volatile关键字的作用

谈谈Java中的volatile

当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存

相关概念:

JMM(java内存模型)

如下图,共享变量放在主内存中,但每个线程操作的都是相应本地内存的副本,因此若某个线程修改了共享变量,可能不能及时更新到主内存,其他线程无法及时读到最新数据。

将一个共享变量声明为volatile后,会有以下效应:

  1. 当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
  2. 这个写会操作会导致其他线程中的缓存无效。

面试题汇总二 Java 多线程篇_第1张图片

复合类操作

如 i++,包含了读取、+1、写入三个步骤,在多线程环境下不保证原子性。可改成相应的原子类进行操作。

禁止重排序

如 int a = 1; int b = 2; 在JVM优化时可能调换顺序,若使用了 volatile 可屏蔽重排序。

 

volatile关键词在单例模式的双重校验中的作用

public class Singleton {
    private Singleton (){}  //私有化构造方法
    private static volatile Singleton singleton=null;

    public static Singleton getInstance(){
        //第一次校验
        if(singleton==null){
            synchronized(Singleton.class){

            //第二次校验
            if(singleton==null){     
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

singleTon = new Singleton()的执行过程如下:

  1. 获取对象地址;
  2. 在对象地址上初始化一个新的Singleton;
  3. 将singleton引用指向Singleton对象;

若不加volatile,JVM的指令重排优化可能导致2和3执行顺序对调,因此其他线程就会拿到非null的singleton引用,但此时Singleton对象还未初始化完毕。

 

synchronized与Lock的区别

详解synchronized与Lock的区别与使用

 

ReentrantLock 、synchronized和volatile比较

那我自己简单总结一下:

  • volatile 是线程同步轻量级实现,性能较好;解决变量的线程可见性问题;修饰变量;不保证原子性。
  • synchronized 相对于 volatile 更重量级;修饰方法和代码块;保证原子性。
  • ReentrantLock 是JDK中的一个类;需要在方法中手动加减锁;相对与 synchronized 可以进行更精细操作。

 

谈谈ThreadLocal关键字的作用

Java并发编程:深入剖析ThreadLocal

ThreadLocal,线程本地变量,为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

我的理解:

每个 Thread 都带有一个 ThreadLocalMap,可以理解为是一个 Map

ThreadLocal 在内部就是操作所在线程的 ThreadLocalMap;

当创建一个 ThreadLocal 时,就在当前线程的 ThreadLocalMap 中存入 key 为自身,value 为副本值的一个键值对;

同理,从 ThreadLocal 取出变量时,也是先获取当前线程的 ThreadLocalMap,再以自身为键值取出相应变量。

 

ThreadLocal、synchronized 和 volatile 关键字的区别

多线程之volatile、ThreadLocal、synchronized关键字区别

synchronized 和 volatile 的区别就不再说了,都是为了实现线程间同步。而 ThreadLocal 是为了实现每个线程内部的局部变量,和前两者作用不一样。

 

你如何确保main()方法所在的线程是Java程序最后结束的线程?

main 方法里 join 子线程。

 

谈谈NIO的理解

Java NIO?看这一篇就够了!

 

在Java中CycliBarriar和CountDownLatch有什么区别?

线程之间的通信

Java中CycliBarriar和CountdownLatch区别(附测试实例)

CycliBarriar

让一组线程等待至某个状态之后再全部同时执行。可以重复使用。

CyclicBarrier cb = new CyclicBarrier(3);

若线程1和线程2先后执行 cb.await(),将会被阻塞;直到线程3调用 cb.await(),三个线程将同时开始执行。

CountDownLatch

一个或多个线程,等待另外N个线程完成某个事情之后才能执行。不可重用。

CountDownLatch cdl = new CountDownLatch(3);

若线程1执行了 cdl.await(),将被阻塞;直到线程2和线程3分别执行 cdl.countDown(),将计数置为0,线程1才会继续执行。

 

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

CopyOnWriteArrayList的使用

读写分离,读直接读,写或改时先复制一个副本,在副本上做修改,再将引用指向该副本。

读不加锁,写要加锁。

频繁复制会有性能问题,因此适用于读多写少的情况。

 

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

网上只找到了这个:

忙循环就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可 能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。

 

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

java holdsLock()方法检测一个线程是否拥有锁

 

死锁的四个必要条件?

死锁的四个必要条件和解决办法

    1、互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
    2、占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
    3、不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
    4、循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

 

什么是线程池,如何使用?

Java并发编程与技术内幕:线程池深入理解

这篇使用部分可以看一下,源码部分和我电脑里的(jdk 1.8)不太一样。

首先,讲讲什么是线程池?照笔者的简单理解,其实就是一组线程实时处理休眠状态,等待唤醒执行。那么为什么要有线程池这个东西呢?可以从以下几个方面来考虑:其一、减少在创建和销毁线程上所花的时间以及系统资源的开销 。其二、将当前任务与主线程隔离,能实现和主线程的异步执行,特别是很多可以分开重复执行的任务。但是,一味的开线程也不一定能带来性能上的,线池休眠也是要占用一定的内存空间,所以合理的选择线程池的大小也是有一定的依据。

尝试分析源码

对于 ThreadPoolExecutor 的使用,比较关键的方法就是 execute() 和 submit(),而 submit() 内部也是调用 execute(),所以着重看一下 execute() 方法。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    
    // clt 是一个原子整数,代表什么不重要,
    // 知道 workerCountOf(c) 表示当前可以直接用的线程数,
    // isRunning(c) 表示线程池当前是否是可用状态就行
    int c = ctl.get();

    // 当前线程数 < corePoolSize 则创建新线程执行任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

    // 当前线程数 >= corePoolSize,加入等待队列
    if (isRunning(c) && workQueue.offer(command)) {
        // 由于是多线程环境,需要反复确认线程池是不是被停了
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // 当前线程数为0,则一定创建一个线程执行任务
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 没有成功加入等待队列,尝试在 maximumPoolSize 的范围内创建线程,
    // 如果创建失败就说明线程数已经超过最大值。
    // 如果等待队列使用 LinkedBlockingQueue 这类没有容量限制的队列时,应该走不到这步
    else if (!addWorker(command, false))
        reject(command);
}

接下来看看 addWorker() 方法。

private boolean addWorker(Runnable firstTask, boolean core) {
    // 参数 core 表示当前容量是参考 corePoolSize 还是 maximumPoolSize
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // 线程池状态变了,不能创建新线程。具体判定条件没看懂。
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // workerCount++
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // workerCount 导致的 cas 失败,重试内部循环
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        // Worder 实现了 Runnable 接口
        // 所以获得的线程 t 传入的参数就是 Worker 自己
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // ...
                int rs = runStateOf(ctl.get());
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // 到这一步终于开始执行线程了
                // 这里调用的 run() 方法是 Worker 的
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
            // 包括 workers.remove(w) 和 workerCount-- 等操作
    }
    return workerStarted;
}

接下来简单看看 Worker 的运行,了解一下线程池中的线程如何在执行完一个任务的情况下执行另一个。

// -------- Worker --------
public void run() {
    runWorker(this);
}

// -------- ThreadPoolExecutor --------
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask; // 这个 firstTask 是 Worker 构造函数的参数
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;

    try {
        // 这个 while 里的条件是关键
        // 如果 task 非空直接执行,否则 getTask()
        // getTask() 会从等待队列里取待执行任务,队列空则阻塞,阻塞超时则返回空
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // 线程池被停止后要不要中断线程的一系列判断和操作
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task); // 方法里面是空的
                Throwable thrown = null;
                try {
                    // 新任务执行
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown); // 方法里面是空的
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // 因为超时等原因,需要结束该线程
        // 下面的方法已看不懂了,比如为什么 workCount-- 不执行等
        processWorkerExit(w, completedAbruptly);
    }
}

 

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

submit() 可以获取返回值,execute() 不行。

 

四种ExecutorService线程池

newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

  核心线程数 最大线程数 持续时间 等待队列 拒绝策略
CachedThreadPool 0 Integer.MAX_VALUE 60s SynchronousQueue AbortPolicy
FixedThreadPool n n 0s LinkedBlockQueue AbortPolicy
ScheduledThreadPool n Integer.MAX_VALUE 0s DelayedWorkQueue AbortPolicy
SingleThreadExecutor 1 1 0s LinkedBlockQueue AbortPolicy

 

线程池四种拒绝策略

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

 

BlockingQueue介绍

用Java实现阻塞队列

Java并发编程:阻塞队列

队列方法
  取操作 放操作 观察操作
非阻塞异常 remove() add(E e)  
非阻塞无异常 poll() offer(E e) peek()
延时 poll(long timeout, TimeUnit unit) offer(E e,long timeout, TimeUnit unit)  
阻塞 take() put(E e)  

常见的阻塞队列:

  • ArrayBlockingQueue:基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。
  • LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。
  • PriorityBlockingQueue:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。
  • DelayQueue:基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

 

Java中interrupted 和 isInterrupted方法的区别?

Thread的isInterrupted()和interrupted()的区别

Thread类中interrupt()、interrupted()和isInterrupted()方法详解

// 针对当前线程,清除中断位
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

// 针对被调用的线程,不清除中断位
public boolean isInterrupted() {
    return isInterrupted(false);
}

private native boolean isInterrupted(boolean ClearInterrupted);

// 针对被调用线程
public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();
    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

 

多线程有什么要注意的问题?

java 多线程开发注意事项

 

如何保证多线程读写文件的安全?

JAVA NIO 文件锁FileLock

文件锁,通过 channel 获得。

// 阻塞
public final FileLock lock()
public abstract FileLock lock (long position, long size, boolean shared)

// 非阻塞
public final FileLock tryLock()
public abstract FileLock tryLock(long position, long size, boolean shared)

// 无参为独占锁,shared = true 为共享锁。

 

实现生产者消费者模式

生产者消费者模式-Java实现

 

Java中的ReadWriteLock是什么?

【Java并发】ReadWriteLock读写锁的使用

ReadWriteLock 接口

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

实现:ReentrantReadWriteLock

  • 写锁为独占锁,读锁为共享锁
  • 读锁被获得后不能获得写锁;写锁被获得后只有同一线程允许获得读锁

 

用Java写一个会导致死锁的程序,你将怎么解决?

让线程A和线程B互相等待对方占用的资源。

final Object obj1 = new Object();
final Object obj2 = new Object();
final BiConsumer consumer = (o1, o2) -> {
    synchronized (o1) {
        System.out.println(Thread.currentThread().getId() + " get " + o1);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (o2) {
            System.out.println(Thread.currentThread().getId() + " get " + o2);
        }
    }
};
final Runnable r1 = () -> consumer.accept(obj1, obj2);
final Runnable r2 = () -> consumer.accept(obj2, obj1);
new Thread(r1).start();
new Thread(r2).start();

 

SimpleDateFormat是线程安全的吗?

浅谈SimpleDateFormat的线程安全问题

多个线程同时调用 parse 会在方法内部互相干扰,最好每个线程单独使用 SimpleDateFormat。

 

Java中的同步集合与并发集合有什么区别?

【转】Java多线程-同步集合和并发集合

同步集合类

  • Hashtable
  • Vector
  • 同步集合包装类,Collections.synchronizedMap()和Collections.synchronizedList() 

并发集合类

  • ConcurrentHashMap
  • CopyOnWriteArrayList
  • CopyOnWriteHashSet

 

Java中ConcurrentHashMap的并发度是什么?

这 5 道 Java 面试题,你还真不一定懂。

ConcurrentHashMap的JDK1.8实现

 

什么是Java Timer类?如何创建一个有特定时间间隔的任务?

Java:利用java Timer类实现定时执行任务的功能

 

 

 

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