面试题系列(一):多线程

写在前面,本文记录 Java 中多线程相关的面试题,方便日后面试的时候复习

1、线程是什么?
线程是操作系统能够进行运算调度的最小单位,它是进程的子集,是进程中实际运作的单位。
2、进程是什么?
进程是指系统中正在运行的一个应用程序
3、进程和线程的区别是什么?
进程是操作系统资源分配的基本单元,线程是任务调度和执行的基本单位;
线程是进程的子集,一个进程可以有一个或多个线程;
每个进程都有自己的内存空间,进程之间切换,开销比较大;一个进程中的多个线程共享内存空间,每个线程都有自己的栈内存和程序计数器,线程之间的切换开销小;
4、Java中多线程的实现方式有哪些?
① 继承 Thread 类:可以直接 start(),方法较多,弊端就是不能在继承其他类了,Java 不支持多继承

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("my thread running");
    }
}

public class Test {
    public static void main(String[] args) {
    	/**
         * 方式一:继承 Thread
         */
        Thread myThread = new MyThread();
        myThread.start();
    }
}

② 实现 Runnable 接口:Java 支持多实现,所以这个方式更灵活,弊端是不能直接 start()

public class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println("my thread running");
    }
}

public class Test {
    public static void main(String[] args) {
        /**
         * 方式二:实现 Runnable
         */
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.start();
    }
}

③ 实现 Callable 接口:有返回值,可以抛出异常,比较好控制;只有jdk1.5以上才支持,需要返回值的时候使用

public class MyyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 101; i++) {
            sum += i;
        }
        return sum;
    }
}

public class Test {
    public static void main(String[] args) {
        /**
         * 方式三:实现 Callable
         */
        MyyThread myyThread = new MyyThread();
        // Callable方式,需要FutureTask实现类的支持,用于接收运算结果
        FutureTask<Integer> futureTask = new FutureTask<>(myyThread);
        new Thread(futureTask).start();
        Integer sum;
        try {
            sum = futureTask.get();
            System.out.println(sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

5、线程的生命周期有哪几个阶段(线程有哪些状态?)
① 新建状态(NEW):新创建一个线程对象,但还没有调用start()方法
② 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
③ 阻塞(BLOCKED):线程阻塞于锁(等待进入同步方法/代码块)
④ 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
⑤ 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回
⑥ 终止状态(Terminated):表示该线程已经执行完毕
6、Thread 类中的start()和 run()有什么区别?
start()方法被用来启动新创建的线程,而且start()内部 调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启 动,start()方法才会启动新线程。
7、Runnable 和 Callable 有什么不同?
Callable 的 call() 方法可以返回值和抛出异常,而 Runnable 的 run() 方法没有这些功能。Callable 可以返回装载有计算结果的Future对象。
8、volatile 变量是什么?
volatile是一个特殊的修饰符,用来修饰成员变量,保证多线程对成员变量的操作对其它线程是透明的,底层是CAS算法
9、守护线程是什么,有什么作用?
守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。作用就是当你想要程序退出,或者JVM退出,线程能自动关闭,那就得用守护线程
10、线程的优先级是什么?
Java提供了一个线程调度器来监控程序启动后进去就绪状态的所有线程。线程调度器通过线程的优先级来决定调度哪些线程执行。一般来说,Java的线程调度器采用时间片轮转算法使多个线程轮转获得CPU的时间片。然而根据实际情况,每个线程的重要程序也不相同,有时候我们想让一些线程优先执行,那么我们可以将他的优先级调高一下,这样它们获得的时间片会多一些。

Java中线程优先级用1~10来表示,分为三个级别:

  • 低优先级:1~4,其中类变量Thread.MIN_PRORITY最低,数值为1
  • 默认优先级:如果一个线程没有指定优先级,默认优先级为5,由类变量Thread.NORM_PRORITY表示
  • 高优先级:6~10,类变量Thread.MAX_PRORITY最高,数值为10

11、什么是竞态条件?,举例说明
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。比如说,我定义了一个变量sum,多个线程对这个变量进行+1操作,这时候就会产生竞态条件,结果就会出问题。解决方案的话可以给成员变量添加 volatile 关键字,或者给+1操作添加同步代码块
12、两个线程间如何共享数据?
如果多个线程执行的是同一个线程体(run()),可以使用同一个runnable对象,这个runnable里面的数据共享;或者定义成类的变量,然后提供操作变量的方法,这样多个线程就可以操作同一个变量
13、线程中常见的方法有哪些?
start():启动一个线程时调用,但线程不会立即运行,要等获取CPU的使用权后才执行。
run():子类重写来实现线程的功能,程序会将它看做一个普通方法来执行。
yield():让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!yield()方法不会释放锁
wait():让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”),同时,wait()也会让当前线程释放它所持有的锁
notify():唤醒在此对象监视器上等待的单个线程;不保证一定唤醒第一个等待的线程,如果是多个,会随机一个
notifyAll():唤醒在此对象监视器上等待的所有线程;
wait (long timeout) :让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)
sleep():让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行;sleep()是不会释放锁的
join():让“主线程”等待“子线程”结束之后才能继续运行(在线程体run()中创建的线程被称为子线程,当前线程被称为主线程)

// 主线程
public class Father extends Thread {
    public void run() {
        Son s = new Son();
        s.start();
        // 让主线程等待
        s.join();
        ...
    }
}
// 子线程
public class Son extends Thread {
    public void run() {
        ...
    }
}

interrupt():interrupt()的作用是中断本线程。
本线程中断自己是被允许的;其它线程调用本线程的interrupt()方法时,会通过checkAccess()检查权限。这有可能抛出SecurityException异常。
如果本线程是处于阻塞状态:调用线程的wait(), wait(long)或wait(long, int)会让它进入等待(阻塞)状态,或者调用线程的join(), join(long), join(long, int), sleep(long), sleep(long, int)也会让它进入阻塞状态。若线程在阻塞状态时,调用了它的interrupt()方法,那么它的“中断状态”会被清除并且会收到一个InterruptedException异常。例如,线程通过wait()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。
如果线程被阻塞在一个Selector选择器中,那么通过interrupt()中断它时;线程的中断标记会被设置为true,并且它会立即从选择操作中返回。
如果不属于前面所说的情况,那么通过interrupt()中断线程时,它的中断标记会被设置为“true”。中断一个“已终止的线程”不会产生任何操作。
注意:interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true
isDaemon():判断是用户线程还是守护线程,返回true就是守护线程
setDaemon():设定当前线程为守护线程
setPriority():设置线程优先级

14、为什么notify(), wait()等函数定义在Object中,而不是Thread中
Object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作;notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因
15、为什么要使用线程池?
线程池提供了一个线程队列,队列中保存着所有等待状态的线程,避免了创建与销毁的额外开销,提高了响应速度。
16、JDK中提供了哪些线程池实现类
java中提供了一个工具类:Executors,调用静态方法创建线程池
① newFixedThreadPool():创建固定大小的线程池

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 任务执行方式一
for (int i = 0; i < 10; i++) {
    fixedThreadPool.submit(() -> System.out.println(Thread.currentThread().getName() + "task running"));
}
// 任务执行方式二
Future<Integer> future = fixedThreadPool.submit(() -> {
    int sum = 0;
    for (int i = 0; i <= 100; i++) {
        sum += i;
    }
    return sum;
});
System.out.println(future.get());
/**
 * 关闭线程池
 */
fixedThreadPool.shutdown();

② newCachedThreadPool():缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    cachedThreadPool.submit(() -> System.out.println(Thread.currentThread().getName() + "running"));
}
// 关闭池
cachedThreadPool.shutdown();

③ newSingleThreadExecutor():创建单个线程池,线程池中只有一个线程

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
singleThreadExecutor.submit(() -> System.out.println(Thread.currentThread().getName() + "running"));
// 关闭池
singleThreadExecutor.shutdown();

④ newScheduledThreadPool():创建固定大小的线程,可以延迟或定时的执行任务

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
scheduledExecutorService.schedule(() -> System.out.println(Thread.currentThread().getName() + "running"),5,TimeUnit.SECONDS);
// 关闭池
scheduledExecutorService.shutdown();

17、为什么不建议使用 Executors 静态工厂构建线程池
1:FixedThreadPool 和 SingleThreadPool:
允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(OutOfMemory)
2:CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM(OutOfMemory)。

推荐使用 ThreadPoolExecutor 的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量

ExecutorService executor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
executor.submit(() -> System.out.println(Thread.currentThread().getName() + "running"));

18、线程池中常用的参数有哪些?

    /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

corePoolSize:核心线程数量,会一直存在,除非allowCoreThreadTimeOut设置为true
maximumPoolSize:线程池允许的最大线程池数量
keepAliveTime:线程数量超过corePoolSize,空闲线程的最大超时时间
unit:超时时间的单位
workQueue:工作队列,保存未执行的Runnable 任务
threadFactory:创建线程的工厂类
handler:当线程已满,工作队列也满了的时候,会被调用。被用来实现各种拒绝策略
19、如何合理配置核心线程数?
首先考虑线程池所进行的工作性质:IO密集型(cpu频繁读写) / CPU密集型(cpu大部分时间在计算)(I/O密集型适合读写,比如数据库的读写操作,CPU密集型适合运算)
CPU密集型:核心线程数 = CPU核数 + 1
IO密集型:核心线程数 = CPU核数 / (1 - 阻塞系数)其中阻塞系数在在0到1范围内
20、说说线程池的拒绝策略?
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略。
通常有以下四种策略:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

线程池的默认拒绝策略为AbortPolicy
21、说说线程池的线程工厂

// Executors.DefaultThreadFactory :线程池默认的线程工厂
/**
 * The default thread factory
 */
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

自定义线程工厂

public class MyFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setName("自定义线程池");
        return t;
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值