美团Java面试题、笔试题(含答案)

一、多线程-线程基础

1.1、线程和进程的区别?

当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

一个进程之内可以分为一到多个线程。

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行

Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。在 windows 中进程是不活动的,只是作为线程的容器

二者对比

  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

1.2、并行和并发有什么区别?

单核CPU

  • 单核CPU下线程实际还是串行执行的
  • 操作系统\]中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。

一般会将这种线程轮流使用CPU的做法称为并发(concurrent)

多核CPU

每个核(core)都可以调度运行线程,这时候线程可以是并行的。

并发(concurrent)是同一时间应对(dealing with)多件事情的能力

并行(parallel)是同一时间动手做(doing)多件事情的能力

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记及答案【扫一扫】 即可免费获取**

1.3、创建线程的四种方式

共有四种方式可以创建线程

  • 继承Thread类
  • 实现runnable接口
  • 实现Callable接口
  • 线程池创建线程

1、继承Thread类

java 复制代码
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": MyThread...run...");
    }

    public static void main(String[] args) {
        // 创建MyThread对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
}

AI写代码java
运行
12345678910111213141516

2、实现runnable接口

java 复制代码
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": MyThread...run...");
    }

    public static void main(String[] args) {
        // 创建MyRunnable对象
        MyRunnable mr = new MyRunnable();

        // 创建Thread对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
}

AI写代码java
运行
12345678910111213141516171819

3、实现Callable接口

java 复制代码
public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName() + ": MyThread...run...");
        return "OK";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建MyCallable对象
        MyCallable mc = new MyCallable();

        // 创建FutureTask
        FutureTask<String> ft = new FutureTask<String>(mc);

        // 创建Thread对象
        Thread t1 = new Thread(ft);
        Thread t2 = new Thread(ft);

        // 调用start方法启动线程
        t1.start();

        // 调用ft的get方法获取执行结果
        String result = ft.get();

        // 输出
        System.out.println(result);
    }
}

AI写代码java
运行
12345678910111213141516171819202122232425262728

4、线程池创建线程

typescript 复制代码
public class MyExecutors implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": MyThread...run...");
    }

    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        threadPool.submit(new MyExecutors());

        // 关闭线程池
        threadPool.shutdown();
    }
}

AI写代码java
运行
123456789101112131415

1.4、Runnable 和 Callable有什么区别

  1. Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  2. Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
  3. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛。

1.5、线程的 run()和 start()有什么区别?

  • start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
  • run():封装了要被线程执行的代码,可以被调用多次。

1.6、线程包括哪些状态,状态之间是如何变化的

线程的状态可以参考JDK中的Thread类中的枚举State

vbnet 复制代码
public enum State {
        /**
         * 尚未启动的线程的线程状态
         */
        NEW,

        /**
         * 可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待来自		 * 操作系统的其他资源,例如处理器。
         */
        RUNNABLE,

        /**
         * 线程阻塞等待监视器锁的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法或在调          * 用Object.wait后重新进入同步块/方法。
         */
        BLOCKED,

        /**
         * 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:
		* Object.wait没有超时
         * 没有超时的Thread.join
         * LockSupport.park
         * 处于等待状态的线程正在等待另一个线程执行特定操作。
         * 例如,一个对对象调用Object.wait()的线程正在等待另一个线程对该对象调用Object.notify()			* 或Object.notifyAll() 。已调用Thread.join()的线程正在等待指定线程终止。
         */
        WAITING,

        /**
         * 具有指定等待时间的等待线程的线程状态。由于以指定的正等待时间调用以下方法之一,线程处于定          * 时等待状态:
		* Thread.sleep
		* Object.wait超时
		* Thread.join超时
		* LockSupport.parkNanos
		* LockSupport.parkUntil
         * </ul>
         */
        TIMED_WAITING,

        /**
         * 已终止线程的线程状态。线程已完成执行
         */
        TERMINATED;
    }

AI写代码java
运行
123456789101112131415161718192021222324252627282930313233343536373839404142

状态之间是如何变化的

新建

  • 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
  • 此时未与操作系统底层线程关联

可运行

  • 调用了 start 方法,就会由新建 进入可运行
  • 此时与底层线程关联,由操作系统调度执行

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记及答案【扫一扫】 即可免费获取**

死亡

  • 线程内代码已经执行完毕,由可运行 进入终结
  • 此时会取消与底层线程关联

阻塞

  • 当获取锁失败后,由可运行 进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
  • 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞 线程,唤醒后的线程进入可运行状态

等待

  • 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行 状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间
  • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待 线程,恢复为可运行状态

有时限等待

  • 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行 状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
  • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待 线程,恢复为可运行状态,并重新去竞争锁
  • 如果等待超时,也会从有时限等待 状态恢复为可运行状态,并重新去竞争锁
  • 还有一种情况是调用 sleep(long) 方法也会从可运行 状态进入有时限等待 状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态

1.7、新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。

为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成

csharp 复制代码
public class JoinTest {
    public static void main(String[] args) {
        // 创建线程对象
        Thread t1 = new Thread(() -> {
            System.out.println("t1");
        });
        Thread t2 = new Thread(() -> {
            try {
                // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2");
        });

        Thread t3 = new Thread(() -> {
            try {
                // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t3");
        });

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

AI写代码java
运行
1234567891011121314151617181920212223242526272829303132

1.8、notify()和 notifyAll()有什么区别?

  • notifyAll:唤醒所有wait的线程
  • notify:只随机唤醒一个 wait 线程
csharp 复制代码
public class WaitNotifyTest {

    static boolean flag = false;
    static Object lock = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                while (!flag) {
                    System.out.println(Thread.currentThread().getName() + "...wating...");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "...flag is true");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                while (!flag) {
                    System.out.println(Thread.currentThread().getName() + "...wating...");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "...flag is true");
            }
        });

        Thread t3 = new Thread(() -> {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + " hold lock");
                lock.notifyAll();
                flag = true;
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

AI写代码java
运行
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051

1.9、在 java 中 wait 和 sleep 方法的不同?

共同点

  • wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态。

不同点

  • 方法归属不同

    • sleep(long) 是 Thread 的静态方法。
    • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有。
  • 醒来时机不同

    • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来。
    • wait(long) 和 wait()还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去。
    • 它们都可以被打断唤醒。
  • 锁特性不同(重点)

    • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制。
    • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
    • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

1.10、如何停止一个正在运行的线程?

有三种方式可以停止线程

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程

代码参考如下:

1、使用退出标志,使线程正常退出

java 复制代码
public class MyInterrupt1 extends Thread {
    // 线程执行的退出标记
    volatile boolean flag = false;

    @Override
    public void run() {
        while (!flag) {
            System.out.println("MyThread...run...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建MyThread对象
        MyInterrupt1 t1 = new MyInterrupt1();
        t1.start();

        // 主线程休眠6秒
        Thread.sleep(6000);

        // 更改标记为true
        t1.flag = true;
    }
}

AI写代码java
运行
12345678910111213141516171819202122232425262728

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记及答案【扫一扫】 即可免费获取**

2、使用stop方法强行终止

java 复制代码
public class MyInterrupt2 extends Thread {
    // 线程执行的退出标记
    volatile boolean flag = false;

    @Override
    public void run() {
        while (!flag) {
            System.out.println("MyThread...run...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建MyThread对象
        MyInterrupt2 t1 = new MyInterrupt2();
        t1.start();

        // 主线程休眠2秒
        Thread.sleep(6000);

        // 调用stop方法
        t1.stop();
    }
}

AI写代码java
运行
12345678910111213141516171819202122232425262728

3、使用interrupt方法中断线程

ini 复制代码
public class MyInterrupt3 {
    public static void main(String[] args) throws InterruptedException {
        // 1.打断阻塞的线程
        /*Thread t1 = new Thread(()->{
            System.out.println("t1 正在运行...");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        t1.start();
        Thread.sleep(500);
        t1.interrupt();
        System.out.println(t1.isInterrupted());*/

        //2.打断正常的线程
        Thread t2 = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                boolean interrupted = current.isInterrupted();
                if (interrupted) {
                    System.out.println("打断状态:" + interrupted);
                    break;
                }
            }
        }, "t2");
        t2.start();
        Thread.sleep(500);
        t2.interrupt();
    }
}

AI写代码java
运行
1234567891011121314151617181920212223242526272829303132

二、多线程-线程安全

2.1、讲一下synchronized关键字的底层原理?

2.1.1、基本使用

Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。

如下抢票的代码,如果不加锁,就会出现超卖或者一张票卖给多个人。

java 复制代码
public class TicketDemo {
    private int ticketNum = 10;

    public synchronized void getTicket() {
        synchronized (this) {
            if (this.ticketNum <= 0) {
                return;
            }
            System.out.println(Thread.currentThread().getName() + "\t抢到一张票, 剩余:" + ticketNum);
            // 非原子性操作
            this.ticketNum--;
        }
    }

    public static void main(String[] args) {
        TicketDemo ticketDemo = new TicketDemo();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                ticketDemo.getTicket();
            }).start();
        }
    }
}

AI写代码java
运行
1234567891011121314151617181920212223

2.1.2、Monitor

Monitor 被翻译为监视器,是由jvm提供,c++语言实现。

在代码中想要体现monitor需要借助javap命令查看clsss的字节码,比如以下代码:

java 复制代码
public class SyncTest {

    static final Object lock = new Object();
    static int counter = 0;

    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }
}

AI写代码java
运行
1234567891011

找到这个类的class文件,在class文件目录下执行javap -v SyncTest.class,反编译效果如下:

  • monitorenter:上锁开始的地方
  • monitorexit:解锁的地方
  • 其中被monitorentermonitorexit包围住的指令就是上锁的代码。
  • 有两个monitorexit的原因,第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁。

在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁。

monitor主要就是跟这个对象产生关联,如下图:

Monitor内部具体的存储结构

  • Owner:存储当前获取锁的线程的,只能有一个线程可以获取。
  • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程。
  • WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程。

具体的流程

  • 代码进入synchorized代码块,先让lock(对象锁)关联的monitor,然后判断Owner是否有线程持有。
  • 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功。
  • 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)。
  • 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待。

参考回答

  • Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】。

  • 它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor

  • monitor内部有三个属性,分别是ownerentrylistwaitset

    • owner是关联的获得锁的线程,并且只能关联一个线程;
    • entrylist关联的是处于阻塞状态的线程;
    • waitset关联的是处于Waiting状态的线程。
相关推荐
Full Stack Developme2 分钟前
java.nio 包详解
java·python·nio
零千叶18 分钟前
【面试】Java JVM 调优面试手册
java·开发语言·jvm
代码充电宝27 分钟前
LeetCode 算法题【简单】290. 单词规律
java·算法·leetcode·职场和发展·哈希表
li37149089031 分钟前
nginx报400bad request 请求头过大异常处理
java·运维·nginx
摇滚侠35 分钟前
Spring Boot 项目, idea 控制台日志设置彩色
java·spring boot·intellij-idea
helloworddm1 小时前
Orleans 流系统握手机制时序图
后端·c#
Aevget1 小时前
「Java EE开发指南」用MyEclipse开发的EJB开发工具(二)
java·ide·java-ee·eclipse·myeclipse
黄昏晓x1 小时前
C++----多态
java·jvm·c++
Brookty2 小时前
【算法】前缀和
java·学习·算法·前缀和·动态规划
开心-开心急了2 小时前
Flask入门教程——李辉 第三章 关键知识梳理
后端·python·flask