刚从蚂蚁金服面试出来的被问到的10道面试题

1.List,Map,Set三个接口存取元素时,各有什么特点?

List以特定索引来存取元素,可以有重复元素,Set不能存放重复元素(用对象的equlas方法来区分元素是否重复)Map保存键值对(Key -value pair)映射,映射关系可以是一对一或多对一。Set和Map容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为o(1),而基于排序树版本的实现在插入和删除元素时会按照元素或元素的键key构成排序树从而达到排序和去重的效果

2.TreeMap和TreeSet在排序时如何比较元素?Collection工具类中的sort()方法如何比较元素?

TreeSet要求存放的对象所属的类必须是实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会调该方法比较元素的大小.TreeMap要求存放的键值对映射的键必须事先Comparable接口从而根据键对元素进行排序 Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较,第二种不强制性的要求容器中的元素必须可比较但是要求第二个参数 参数是Comparator接口的子类型(需要重写compare方法实现元素的比较)相当一个临时定义的排序规则 其实就是通过接口注入比较元素大小的算法 也是对回调模式的应用

例子1:

public class Student implements Comparable<Student> {

private String name; // 姓名

private int age; // 年龄

public Student(String name, int age) {

this.name = name;

this.age = age;

}

@Override

public String toString() {

return "Student [name=" + name + ", age=" + age + "]";

}

@Override

public int compareTo(Student o) {

return this.age - o.age; // 比较年龄(年龄的升序)

}

}

import java.util.Set;

import java.util.TreeSet;

class Test01 {

public static void main(String[] args) {

Set<Student> set = new TreeSet<>(); // Java 7的钻石语法(构造器后面的尖括号中不需要写类型)

set.add(new Student("Hao LUO", 33));

set.add(new Student("XJ WANG", 32));

set.add(new Student("Bruce LEE", 60));

set.add(new Student("Bob YANG", 22));

for(Student stu : set) {

System.out.println(stu);

}

// 输出结果:

// Student [name=Bob YANG, age=22]

// Student [name=XJ WANG, age=32]

// Student [name=Hao LUO, age=33]

// Student [name=Bruce LEE, age=60]

}

}

例子2:

public class Student {

private String name; // 姓名

private int age; // 年龄

public Student(String name, int age) {

this.name = name;

this.age = age;

}

/**

* 获取学生姓名

*/

public String getName() {

return name;

}

/**

* 获取学生年龄

*/

public int getAge() {

return age;

}

@Override

public String toString() {

return "Student [name=" + name + ", age=" + age + "]";

}

}

import java.util.ArrayList;

import java.util.Collections;

import java.util.Comparator;

import java.util.List;

class Test02 {

public static void main(String[] args) {

List<Student> list = new ArrayList<>(); // Java 7的钻石语法(构造器后面的尖括号中不需要写类型)

list.add(new Student("Hao LUO", 33));

list.add(new Student("XJ WANG", 32));

list.add(new Student("Bruce LEE", 60));

list.add(new Student("Bob YANG", 22));

// 通过sort方法的第二个参数传入一个Comparator接口对象

// 相当于是传入一个比较对象大小的算法到sort方法中

// 由于Java中没有函数指针、仿函数、委托这样的概念

// 因此要将一个算法传入一个方法中唯一的选择就是通过接口回调

Collections.sort(list, new Comparator<Student> () {

@Override

public int compare(Student o1, Student o2) {

return o1.getName().compareTo(o2.getName()); // 比较学生姓名

}

});

for(Student stu : list) {

System.out.println(stu);

}

// 输出结果:

// Student [name=Bob YANG, age=22]

// Student [name=Bruce LEE, age=60]

// Student [name=Hao LUO, age=33]

// Student [name=XJ WANG, age=32]

}

}

3.Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行 它们有什么区别?

sleep()方法是线程类Thread的静态方法 调用此方***让当前线程暂停执行指定的时间,将执行机会cpu让给其他线程 但是对象的锁依然保持,因此休眠时间结束后会自动修复 线程回到就绪状态 wait()是Object类的方法 调用对象的wait()方法导致当前线程放弃对象的锁 线程暂停执行 进入对象的等待池 wait pool 只有调用对象的notify()方法或者 notifyAll()方法 时才能唤醒等待池中的线程进入等锁池 lock pool 如果线程重新获得对象的锁就可以进入就绪状态

4.线程的sleep()方法和yield()方法有什么区别?

1.sleep()方法给其他线程运行机会时不考虑线程的优先级 因此会给低优先级的县城以运行的机会 yield()方法只会给相同优先级或更高优先级的线程以运行的机会

2.线程执行sleep()方法后转入阻塞 blocked 状态 而执行yield()方法后转入就绪 ready 状态

3、sleep()方法声明抛出 InterruptedException 而yield()方法没有声明任何异常

4、sleep()方法比yield()方法 具有更好的可移植性

5.当一个线程进入一个对象的synchronized方法A之后 其他线程是否可进入此对象的synchronized方法b?

不能,其他线程只能访问该对象的非同步方法 同步方法则不能进入 因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁

6.请说出与线程同步以及线程调度相关的方法

wati():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁

sleep()使一个正在运行的线程处于睡眠状态使一个静态方法调用此方法要处理InterruptedException异常

notify():唤醒一个处于等待状态的线程 当然在调用此方法的时候 并不能确切的唤醒某一个等待状态的线程 而是由于JVM确定唤醒哪个线程,而且与优先级无关

notityAll(): 唤醒所有处于等待状态的线程 该方法并不是将对象的锁给所有线程 而是让他们竞争 只有获得锁的线程才能进入就绪状态

下面的例子演示了100个线程同时向一个银行账户中存入1元钱,在没有使用同步机制和使用同步机制情况下的执行情况。

银行账户类:

/**

* 银行账户

* @author 骆昊

*

*/

public class Account {

private double balance; // 账户余额

/**

* 存款

* @param money 存入金额

*/

public void deposit(double money) {

double newBalance = balance + money;

try {

Thread.sleep(10); // 模拟此业务需要一段处理时间

}

catch(InterruptedException ex) {

ex.printStackTrace();

}

balance = newBalance;

}

/**

* 获得账户余额

*/

public double getBalance() {

return balance;

}

}

存钱线程类:

/**

* 存钱线程

* @author 骆昊

*

*/

public class AddMoneyThread implements Runnable {

private Account account; // 存入账户

private double money; // 存入金额

public AddMoneyThread(Account account, double money) {

this.account = account;

this.money = money;

}

@Override

public void run() {

account.deposit(money);

}

}

测试类:

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class Test01 {

public static void main(String[] args) {

Account account = new Account();

ExecutorService service = Executors.newFixedThreadPool(100);

for(int i = 1; i <= 100; i++) {

service.execute(new AddMoneyThread(account, 1));

}

service.shutdown();

while(!service.isTerminated()) {}

System.out.println("账户余额: " + account.getBalance());

}

}

在 没有同步的情况下,执行结果通常是显示账户余额在10元以下,出现这种状况的原因是,当一个线程A试图存入1元的时候,另外一个线程B也能够进入存款的方 法中,线程B读取到的账户余额仍然是线程A存入1元钱之前的账户余额,因此也是在原来的余额0上面做了加1元的操作,同理线程C也会做类似的事情,所以最 后100个线程执行结束时,本来期望账户余额为100元,但实际得到的通常在10元以下(很可能是1元哦)。解决这个问题的办法就是同步,当一个线程对银 行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行操作,代码有如下几种调整方案:

在银行账户的存款(deposit)方法上同步(synchronized)关键字

/**

* 银行账户

* @author 骆昊

*

*/

public class Account {

private double balance; // 账户余额

/**

* 存款

* @param money 存入金额

*/

public synchronized void deposit(double money) {

double newBalance = balance + money;

try {

Thread.sleep(10); // 模拟此业务需要一段处理时间

}

catch(InterruptedException ex) {

ex.printStackTrace();

}

balance = newBalance;

}

/**

* 获得账户余额

*/

public double getBalance() {

return balance;

}

}

在线程调用存款方法时对银行账户进行同步

/**

* 存钱线程

* @author 骆昊

*

*/

public class AddMoneyThread implements Runnable {

private Account account; // 存入账户

private double money; // 存入金额

public AddMoneyThread(Account account, double money) {

this.account = account;

this.money = money;

}

@Override

public void run() {

synchronized (account) {

account.deposit(money);

}

}

}

通过Java 5显示的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

/**

* 银行账户

*

* @author 骆昊

*

*/

public class Account {

private Lock accountLock = new ReentrantLock();

private double balance; // 账户余额

/**

* 存款

*

* @param money

* 存入金额

*/

public void deposit(double money) {

accountLock.lock();

try {

double newBalance = balance + money;

try {

Thread.sleep(10); // 模拟此业务需要一段处理时间

}

catch (InterruptedException ex) {

ex.printStackTrace();

}

balance = newBalance;

}

finally {

accountLock.unlock();

}

}

/**

* 获得账户余额

*/

public double getBalance() {

return balance;

}

}

按照上述三种方式对代码进行修改后,重写执行测试代码Test01,将看到最终的账户余额为100元。当然也可以使用Semaphore或CountdownLatch来实现同步。

7.synchroized关键字的用法?

synchroized关键字可以将对象或者方法标记为同步 以实现对对象和方法的互斥访问可以用 synchronized(对象){。。。}定义同步代码块 或者在声明方法时将synchronized作为方法的修饰符

8.举例说明同步和异步?

如果系统存在临界资源 资源数量少于竞争资源的线程数量的资源 例如正在写的数据以后可能被另一个线程读到 或者正在读的数据可能被另一个线程写过了 那么这些数据就必须进行同步存取 数据库操作中的排他锁就是最好的例子 当应用程序在对象上调用了一个需要花费很长时间来执行的方法并且不希望让程序等待方法的返回 就应该使用异步编程 在很多情况下采用异步途径往往更有效率 事实上 所谓的同步就是指阻塞式操作 而异步就是非阻塞式操作

9.启动一个线程是调用run()还是start()方法?

启动一个线程是调用start()方法 使线程所代表的虚拟处理机处于可运行状态 意味着它可以用JVM调度并执行 这并不意味着线程就会立即运行 run()方法是线程启动后要进行回调(callback)的方法

10.什么是线程池? (thread pool)?

在面向对象编程中创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其他跟多资源 在Java中便是如此 虚拟就将试图跟踪每一个对象 以便能够在对象销毁后进行垃圾回收 所以提高服务程序效果的一个手段 就是尽可能减少创建和销毁对象的次数 特别是一些很耗资源的对象创建和销毁,这就是“池化资源”技术产生的原因。线程顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行穿甲 使用完毕不需要销毁线程的而是放回池中 从而减少创建和销毁线程对象的开销

Java 5中的Executor 接口定义一个执行线程的工具 它的子类型即线程池接口是 ExcecutorService 要配置一个线程池是比较复杂的

尤其是对线程池的原理不是很清楚的情况下 因此在工具类Executors面提供了一些静态工厂反复嘎 生成一些常用的线程池 如下所示:

newSingleThreadExecutor:创建一个单线程的线程池 这个线程池只有一个线程在工作也即是相当于单线程串执行所有任务 如果这个唯一的线程因为异常技术 那么会有一个新的线程来替代它 此线程池保证所有的任务的执行属性按照任务的提交顺序执行

newFixedThreadPool:创建固定大小的线程池每次提交一个任务就穿甲一个线程 知道线程达到线程池的最大大小 线程的大小一旦达到最大值就会保持不变 如果某个县城因为执行异常而结束那么线程池会补充一个新线程

newCachedThreadPool:创建一个可缓存的线程池 如果线程池的大小超过处理任务所需要的线程那么就会回收部分空闲 (60秒不执行任务)的线程 当任务数增加时 ,此线程池又可以智能的添加新线程来处理任务此线程池不会对线程池大小做限制 线程池大小完全依赖与操作系统 或者说JVM 能够创建的最大线程大小。

newScheduledThreadPool 创建一个大小无限的线程池 此线程池支持定时以及周期性执行任务的需求

newSingleThreadExecutor:创建一个单线程的线程池 此线程池支持定时以及周期性执行任务的需求

下方为大家一套面试题

起因

  • 焦虑。每次自己想跳槽的时候,内心总是担忧着那些面试题怎么解答。
  • 很多问题在实际工作中并不会遇到,没有实际的解决问题经验,看过也记不住。
  • 假如我明年需要换工作,那现在把下次的面试准备工作,拆分、融入到平时的每天中,会不会取得更满意的offer?

目标

  • 收集网上各种经典的 java 面试题
  • 做出答案,分享出去,和猿友讨论,不断进步
  • 扩大知识图谱,扎实基础,梳理知识脉络
  • 避免下次面试时的准备工作

一方面帮助跳槽季来临之时。不时之需,一方面帮助大家巩固下基础,帮助活到学到老的机会 另一方面也希望帮助想要换工作的朋友。

全部评论

相关推荐

努力学习的小绵羊:我反倒觉得这种挺好的,给不到我想要的就别浪费大家时间了
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务