自己经验有限,篇幅也有限,这里只是记录一些比较容易混淆或有难度和一些易忘的技术知识点,里面有一些也是面试阿里经常会被问到的问题,但是不保证答案全部正确,有错误的地方望大家指正
JVM相关
- JVM内存是如何分配的?
堆:占用内存最大的区块,主要存放new出来的对象,线程共享,主要设置大小参数名是-Xms
和-Xmx
栈(以前概括都叫栈,具体说其实是非堆内存):一般是线程私有- 寄存器:即是程序计数器,存放当前正要执行的下一条指令地址
- 本地方法栈(不同jvm的实现可能不同,比如平常所用的sun的实现中方法栈和虚拟机栈是一个):线程私有,存储比如
Object
的hashCode()
;虚拟机栈中用于存储局部变量表、动态链接、操作数、方法出口等信息 - 方法区:所有线程共享,用于存放加载类信息,比如常量、静态常量,需要注意的是1.8以后将静态常量放在了堆里
- GC
垃圾回收的算法基础是标记和复制算法,标记一般是树形结构,采用根搜索算法,标记可以回收的对象,一般是对象搜索不到根节点即可以回收,有2次机会,在第一次被标记回收后可以重新被挂靠根节点(也即是被重新引用,涉及的的方法是finalize()
),如果没有下一次判定对象死亡;基础的复制算法我举个例子说明,将一块内存分成2份,运行时只使用其中一块,GC时将活的对象复制到另一块内存,然后清除前一块所有内存空间,类似于给U盘格式化,这样比一个一个释放内存要快得多,相信大家做格式化的时候体会过,现在jvm gc使用的的复制算法是结果改良的,不是平均的分成2份,默认比例好像是1/8,即平常见到的新生代、老年代、持久代等。具体的算法大家看资料文档吧,这种东西不是说说就能清楚的。 - 内存泄漏和内存溢出
内存泄漏,当一个对象不会被使用但占着内存即会导致内存泄漏,比如
Object o1 = new Object();
Object o2 = new Object();
o1 = o2; // 这时2个对象的引用地址是一样的,但是o1申请的内存就没有被使用
内存泄漏积累多了,内存不断被无用对象占用,新的对象申请不到足够的空间就会产生内存溢出。
架构相关
- springMVC的处理流程
- 一个http请求
- 经过一些过滤器或拦截器到达
DispatcherServlet
将请求转发给对应的@Controller
和@RequestMapping
- 参数封装,请求头判定等等
- 调用业务方法获得
Model
等 - 返回
ModelAndView
查找ViewResolver
返回对应的View
,可能是需要渲染的jsp,可能是json,可能是文件流等等。
- 说说
redis
里的bitmap
bitmap
一般用于计数或top计算,比如统计网站当前在线人数,假设用户id是递增的整数,当用户上线时将用户id存进bitmap,比如id是4,则bitmap就是00001000
,id为8的用户上线,bitmap的值变成10001000
,对bitmap做count计算得出是2,而1KB=1024B=8196b且位运算是计算机最快的,这样做的好处是速度快,还能知道是谁上下线的。同理如果要按月统计某个操作,只需要用每天做key值,然后做并集得到新的bitset计数就可。 - Ioc和AOP分别使用了哪些设计模式?
工厂模式和代理模式,细一点还有单例、模版、原型,这里说一下代理模式,常用的一般是动态代理模式,jdk中提供了InvocationHandler
接口可以方便实现动态代理:
public interface IService {
void service();
}
public class MyService implements IService {
@Override
public void service() {
System.out.println("service...");
}
}
public class MyProxy implements InvocationHandler {
private IService service;
public MyProxy(IService service) {
this.service = service;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before service");
Object invoke = method.invoke(service, args);
System.out.println("after service ");
return invoke;
}
}
public class Main {
public static void main(String[] args) {
MyService myService = new MyService();
MyProxy myProxy = new MyProxy(myService);
// 动态生成代理对象
IService instance = (IService) Proxy.newProxyInstance(MyService.class.getClassLoader(), MyService.class.getInterfaces(), myProxy);
instance.service();// 利用代理对象调用service方法
}
}
- MySQL数据库的锁
MySQL常见的有2种引擎,MyISAM采用的是表级锁,给整表加锁,速度快,不会死锁,但是但是锁竞争激烈,效率低,lock table_name read
orwrite
;InnoDB采用的是行级锁,只锁行,效率高,但是会死锁,另外MySQL的行级锁采用锁索引实现,所以只有通过索引检索数据才能使用行级锁,否则会使用表级锁 - 幂等性
幂等性是数学里的一个概念,我并不是很精通,简单来说就是N次变换和1次变换的结果应该保持一致,计算机里他是一种Http协议中提到的性质,注意幂等性本身并不是协议,没有办法通过规范一致化操作,多用于分布式系统,用于保证分布式系统中数据的一致性操作,类似于分布式事务,但是分布式事务中间件一般较重且效率有很大亏损,对于要求高性能的分布式场景中,幂等设计可能是唯一的选择。实现场景简单举例来说,假设微信支付后端是分布式的(肯定是的),我发起了一个支付,如果服务器端已经处理完成但是我的手机没网了,我会误以为支付失败,重新支付,幂等设计在此类场景中一般会这样设计,在发起支付操作前会先向服务端申请一个ticket,这个ticket会关联此次支付的操作,这个ticket只能增长一次,这样在我重新发起支付的时候,服务器就可以正确返回支付成功且保证我只支付了一次。
java基础
- ConcurrentHashMap
ConcurrentHashMap
是线程安全的集合类,功能类似于Hashtable
,但是Hashtable
虽然也是线程安全的,但是Hashtable
只有一把同步锁,并发性能不高,ConcurrentHashMap
则是利用了锁分段技术,简单来说就是,多个类似的HashTable
,单独维护自己的锁,这样多线程操作的时候减少了竞争锁的等待,在多线程应用里是最常用的线程安全集合类。查看源码可以知道,ConcurrentHashMap
内部主要成员是Segment
和Node
,Segment
充当锁,继承ReentrantLock
,Node
相当于一个Map.Entry
,其他大部分成员变量都是volatile
的,因为happen before
的存在,volatile
字段的写入操作先于读操作,这也是用volatile替换锁的经典应用场景。 - ThreadLocal
ThreadLocal,利用线程局部变量来实现线程安全的方式,使用时需要小心应对,因为线程局部变量一旦使用完没有被释放就会导致内存泄漏。 - 有没有可能两个不相等的对象有相同的 hashcode?
hashcode并不是唯一的,只是重复概率非常小而已,但是相等的对象hashcode一定是一样的。 - 编写多线程程序的时候你需要注意哪些?
- 尽量使用
volatile
替换同步锁 - 给线程取个name
- 使用并发集合而不是让集合同步
- 合理创建线程数,一般而言是CPU的核心数*2+1
- 给需要同步的代码同步,而不是图简单给整个方法或类加同步
- 尽量使用
- DateFormat的所有实现都不是线程安全的,如果一定要在多线程中使用可以利用
ThreadLocal
- 对称加密和非对称加密
对称加密:需要同一把密钥来解密,速度快,一般用于需要加密大量数据时使用,常见用于对称加密的算法有DES、3DES、RC系、AES等。
非对称加密:需要2把密钥才能解密,分作公钥和私钥,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密;常见的https协议里的证书机制就是采用的这种方式,常用于非对称加密算法的有RSA、ECC、Elgamal。 - NIO和普通IO的区别?
最主要的区别在于非阻塞与阻塞,NIO是先写入缓冲区在再读出操作,是非阻塞的,而普通IO操作主要是针对流的,一个线程读写流时是不能做其他操作的,就好比如下载文件有些软件可以断点续传,有些不可以。
问题
- 统计log文件里所有出现的单词以及出现的次数并且按照次数排序找出最频繁的单词?
步骤其实很简单:- 读取文件
- 排序
这里直接提供代码,分别是jdk 1.7和jdk 1.8的2个版本
1.7:
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
public class Test {
/**
* 根据map的value进行排序
* @param map
* @param <K>
* @param <V>
* @return
*/
public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
List<Map.Entry<K, V>> list = new LinkedList<Map.Entry<K, V>>(map.entrySet());
// 先将map转换成List便于使用sort排序
Collections.sort(list, new Comparator<Map.Entry<K, V>>() {
public int compare(Map.Entry<K, V> o1, Map.Entry<K, V> o2) {
return (o2.getValue()).compareTo(o1.getValue());
}
});
Map<K, V> result = new LinkedHashMap<K, V>();
for (Map.Entry<K, V> entry : list) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
public static void main(String[] args) {
FileInputStream fileInputStream = null;
String fileName = "ngen.log";
TreeMap<String, Integer> map = new TreeMap<>();
// SortedMap<String, Integer> map = new
try {
fileInputStream = new FileInputStream(fileName);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream));
String line = null;
// 按行读取文件分解单词
while ((line = bufferedReader.readLine()) != null) {
String[] ss = line.split(" ");
for (int i = 0; i < ss.length; i++) {
String s = ss[i];
if (s != null && s.matches("\\w+")) {
// 如果map中有此单词就将次数+1
// 否则此单词第一次出现
if (map.containsKey(s)) {
map.put(s, map.get(s) + 1);
} else {
map.put(s, 1);
}
}
}
}
Map<String, Integer> sortedMap = sortByValue(map);
for (Map.Entry<String, Integer> entry : sortedMap.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + ":" + value);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
1.8:
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Main {
/**
* 根据map的value进行排序
* @param map
* @param <K>
* @param <V>
* @return
*/
public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
return map.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue(Collections.reverseOrder())) // 逆序
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));
}
public static void main(String[] args) {
FileInputStream fileInputStream = null;
String fileName = "D:/app.log";
TreeMap<String, Integer> map = new TreeMap<>();
try {
fileInputStream = new FileInputStream(fileName);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream));
bufferedReader.lines()
.flatMap(line -> Stream.of(line.split(" ")))
.filter(word -> word.matches("\\w+"))
.forEach(s -> { // Stream语法不太熟悉,不知道有木有更方便的方法?
if (map.containsKey(s)) {
map.put(s, map.get(s) + 1);
} else {
map.put(s, 1);
}
})
;
Map<String, Integer> sortedMap = sortByValue(map);
sortedMap.forEach((k, v) -> System.out.println(k + "," + v));
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
从日志文件中读取的是字节还是字符?
乍一看这问题不要太简单,但是我挺佩服问这问题的面试官的,这问题向后衍生无论是广度还是深度都无可挑剔。如何回答这问题要从两方面出发,首先一点,所有操作系统存放在磁盘的任何数据肯定都是字节的,那么读出来肯定也是字节的;第二点,通常日志文件操作写代码的时候读出来的肯定是字符,不然你如何操作呢?只是java提供的方便的I/O操作方法而已,其实里面是将字节转成了字符而已。向后延伸就会问比如java的IO操作注意事项、编码等等问题,还有操作系统底层如何处理等等的问题,这道题很简单,但是切记不要盲目作答。-
秒杀系统设计
自己并没有实际秒杀系统设计经验,这里从朋友以及网络总结几点:- 高并发,总的来说肯定是Nginx做负载均衡,后台做服务集群
- 秒杀计时前的静态页面使用cdn
- 秒杀计时不需要做高并发处理,因为new一个Date返回给前台,任何语言支持个几亿并发都是没问题的
- 先缓存再查库,保证低延迟
- 秒杀系统单独设计,不要与已有业务混淆,不然一旦阻塞会全盘崩溃
- 库存要保持事务唯一,数据库最好另建表,不要与日常业务冲突
- 预估请求处理最大量,当请求过多时拦截并直接返回等待
- 预防恶意刷单,比如同一个IP只能有一个请求
- 动态加载js来激活秒杀按钮,避免秒杀没有开始时被恶意操作
- 并发请求队列
本文已在版权印备案,如需转载请访问版权印。40142943