目录
Java基础:
面向对象的特征:继承、封装和多态
重载和重写的区别
int 和 Integer 有什么区别;Integer的值缓存范围
说说反射的用途及实现
Http 请求的 GET 和 POST 方式的区别
MVC设计思想
什么是Java序列化和反序列化;如何实现Java序列化;或者请描述Serializable接口的作用
Colletcion类库中常用类
进程和线程:
线程和进程的概念
并行和并发的概念
创建线程的方式及实现
进程间通信的方式
说说 CountDownLatch、CyclicBarrier 原理和区别
说说 Semaphore 原理
说说 Exchanger 原理
ThreadLocal 原理分析;ThreadLocal为什么会出现OOM,出现的深层次原理
讲讲线程池的实现原理
线程池的几种实现方式
线程的生命周期;状态是如何转移的
Java中用到的线程调度算法
单例模式的线程安全性
线程类的构造方法、静态块是被哪个线程调用的?
sleep() 和 wait() 有什么区别?
多线程的上下文切换
锁机制:
什么是线程安全?如何保证线程安全?
重入锁的概念;重入锁为什么可以防止死锁?
产生死锁的四个条件
如何检查死锁
volatile 实现原理
synchronized 实现原理(对象监视器)
synchronized 与 lock 的区别
AQS 同步队列
CAS 无锁的概念;乐观锁和悲观锁
常见的原子操作类
什么是 ABA 问题;出现 ABA 问题 JDK 是如何解决的
乐观锁的业务场景及实现方式
Java 8 并发包下常见的并发类
偏向锁、轻量级锁、重量级锁、自旋锁的概念
数据库:
DDL、DML、DCL 分别指什么
explain 命令
脏读、幻读、不可重复读
事务的隔离级别
数据库的几大范式
说说分库与分表设计
分库与分表带来的分布式困境与对应之策
说说 SQL 优化之道
存储引擎的 InnoDB 与 MyISAM 区别、优缺点、使用场景
索引类别(B+树索引、全文索引、哈希索引);索引的区别
什么是自适应哈希索引(AHI)
为什么要用 B+tree 作为 MySql 索引的数据结构
聚集索引与非聚集索引的区别
limit 20000 加载很慢怎么解决
常见的几种分布式 ID 的设计方案
JVM
JVM 运行时内存区域划分
常见的 GC 回收算法及其含义
常见的 JVM 性能监控和故障处理工具类
JVM 性能调优
类加载器、双亲委派模型
类加载的过程
强引用、软引用、弱引用、虚引用
Java 内存模型 JMM
封装,就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏;
它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展;
通过继承创建的新类成为"子类"或者"派生类";
被继承的类成为"基类"、"父类"或"超类";
继承的过程,就是从一般到特殊的过程;
一般情况下,一个子类只能有一个基类,要实现多重继承,可以通过多级继承来实现。
继承概念的实现方式有三类:
是允许你将父对象设置成为一个或者更多的它的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。
多态概念的实现方式有两种:
区别点 | 重载 | 重写(覆写) |
---|---|---|
英文 | Overloading | Overiding |
定义 | 方法名称相同,参数的类型或个数不同 | 方法名称、参数类型、返回值类型全部相同 |
权限 | 对权限没要求 | 被重写的方法不能拥有更严格的权限 |
范围 | 发生在一个类中 | 发生在继承类中 |
两者之间的区别:
Integer的值缓存范围:-128 ~ 127
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能成为Java语言的反射机制。
反射机制的原理:
反射的本质就是当获取到表示 Student.class 的对象后,反向获取 Student 类的信息。
Java反射框架提供以下功能:
GET产生一个TCP数据包:
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
POST产生两个TCP数据包:
对于POST方式的请求,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
首先,MVC不是一种设计模式,而是一种设计思想,接下来看下两个概念的区别:
接下来说说MVC的设计思想:
如何实现Java序列化:实现serializable接口
它的 writeObject(Object obj) 方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中;
它的 readObject() 方法从一个源输入流中读取字节序列,再把他们反序列化为一个对象,并将其返回。
Collection是List、set父接口,不是Map父接口。
A)定义 Thread 类的子类,并重写该类的 run() 方法,该 run() 方法的方法体就代表了线程要完成的任务,因此把 run() 方法称为执行体;
B)创建 Thread 子类的实例,即创建了线程对象;
C)调用线程对象的 start() 方法来启动该线程。
A)定义 runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体;
B)创建 Runnable 接口实现类的实例,并依此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象;
C)调用线程对象的 start() 方法来启动该线程。
A)创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值;
B)创建 Callable 实现类的实例,使用 Future Task 类来包装 Callable 对象,该 Future Task 对象封装了该 Callable 对象的 call() 方法的返回值;
C)使用 Future Task 对象作为 Thread 对象的 target 创建并启动新线程;
D)调用 Future Task 对象的 get() 方法来获得子线程执行结束后的返回值。
三种方式的比较:
1> 通过 Runnable 和 Callable 创建多线程:
优势:
A)线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类;
B)在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势:
C)编程稍微复杂,如果要访问当前线程,必须使用 Thread.currentThread() 方法。
D) Runnable 和 Callable 的区别:
Runnable | Callable |
---|---|
重写的方法是 run() | 重写的方法是call() |
Runnable的任务是不能返回值的 | Callable的任务执行后可返回值 |
run()方法不可以抛出异常 | call() 方法可以抛出异常 |
运行 Callable 任务可以看到一个 Future 对象,表示异步计算的结果。 它提供了检索计算是否完成的方法,以等待计算的完成, 并检索计算的结果。通过 Future 对象可以了解任务执行情况, 可取消任务的执行,还可以获取执行结果。 |
2> 使用继承 Thread 类的方式创建多线程:
优势:
如果要访问当前线程,则无需使用 Thread.currentThread() 方法,使用 this即可;
劣势:
已经继承了 Thread 类,不能再继承其他父类。
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
管道是一种半双工(即数据只能在一个方向上流动)的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常指父子进程关系。
将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们称为高级管道方式。
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号量(Semaphore)是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及统一进程内不用线程之间的同步手段。
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是对快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
套接字(socket):套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
CountDownLatch:
是同步辅助类,它可以指定一个计数值,在并发环境下由线程进行减1操作,当计数值变为0后,被 await() 方法阻塞的线程将会唤醒,实现线程间的同步。
一个或N个线程等待其它线程的关系。
CyclicBarrier:
是同步辅助类,它允许一组线程互相等待,直到所有线程都达到某个特公共屏障点(也可以叫同步点),即相互等待的线程都完成调用 await() 方法,所有被屏障拦截的线程才会继续运行 await() 方法后面的程序。
各个线程内部相互等待的关系。
两者的区别:
CountDownLatch | CyclicBarrier |
减计数方式 | 加计数方式 |
计算为0时释放所有等待的线程 | 计数达到指定值时释放所有等待线程 |
计数为0时,无法重置 | 计数达到指定值时,计数置为0重新开始 |
调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响 | 调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞 |
不可重复利用 | 可重复利用 |
两者的使用场景,下面的文章说的很清楚:
CountDownLatch 和 CyclicBarrier 的使用场景
Semaphore(信号量) 是用来控制同时访问特定资源的线程数量,它通过协调各个线程,保证合理的使用公共资源。
线程可以通过 acquire() 方法来获取信号量的许可。当信号量中没有可用的许可的时候,线程阻塞,直到有可用的许可为止。线程可以通过 release() 方法释放它持有的信号量的许可。
Semaphore 内部基础AQS的共享模式,所以实现都委托给了Sync类。
Semaphore 有两种模式:
调用 acquire() 的顺序就是获取许可证的顺序,遵循FIFO;
为抢占式的,可能一个新的获取线程恰好在一个许可证释放时得到这个许可证,而前面还有等待的线程。
主要用于两个工作线程之间交换数据。
Java Exchanger 原理
ThreadLocal 是线程的局部变量,是每一个线程所单独持有的,其他线程不能对其进行访问。通常是类中的 private static 字段,是对该字段初始值的一个拷贝,它们希望将状态与某一个线程(例如用户ID或者事务ID)相关联。
ThreadLocal 为什么会出现OOM:
ThreadLocal 的实现是这样的:每个 Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object;
ThreadLocal 里面使用了一个存在弱引用的 map ,map 的类型是 ThreadLocal.ThreadLocalMap。Map 中的 key 为一个 ThreadLocal 实例本身,而这个 key 使用弱引用指向 ThreadLocal 。当 ThreadLocal 实例置为 null 后,没有任何强引用指向 ThreadLocal 实例,所以ThreadLocal 将会被 GC 回收。但是我们的 value 却不能回收,而这块 value 永远不会被访问到。所以存在着内存泄漏。因为存在一条从 Current Thread 链接过来的强引用,只有当 Thread 结束以后, Current Thread 就不会存在栈中,强引用断开,Current Thread、Map Value 将全部GC回收。
如何避免内存泄漏:
每次使用完 ThreadLocal,都调用它的 remove() 方法,清除数据。
提交一个任务到线程池中,线程池的处理流程如下:
创建一个可缓存的线程池,如果线程池的长度超过处理的需要,可以灵活回收空闲线程,若无可回收,则新建线程。
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(new Runnable() {
@Override
public void run() {
log.info("task:{}",index);
}
});
}
executorService.shutdown();
创建一个定长线程池,可以控制线程最大并发数,超出的线程会在队列中等待。
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(new Runnable() {
@Override
public void run() {
log.info("task:{}",index);
}
});
}
executorService.shutdown();
创建一个定长线程池,支持定时、周期性的任务执行。
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
// 延迟一秒之后,每隔三秒执行一次
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
log.info("scheduled run");
}
},1,3,TimeUnit.SECONDS);
// 也可以使用timer的schedule方法来实现定时功能
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
log.info("timer run");
}
},new Date(),5*1000);
创建一个单线程化的线程池,只会用唯一一个工作线程执行任务。
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(new Runnable() {
@Override
public void run() {
log.info("task:{}",index);
}
});
}
executorService.shutdown();
线程池的相关信息可以看下这个链接:线程池相关概念
线程的生命周期:
当创建一个 Thread 类的一个实例(对象)时,此线程进入新建状态(未被启动);
例如:Thread t1 = new Thread();
线程已经被启动,正在等待被分配给 CPU 时间片,也就是说此时线程正在就绪队列中排队等候得到 CPU 资源;
例如:ti.start();
线程获得 CPU 资源正在执行任务(run() 方法),此时除非此线程自动放弃 CPU 资源或者有优先级更高的线程进入,线程将一直运行到结束。
由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行,即进入阻塞状态。
正在睡眠:调用 sleep(long t) 方法 可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。
正在等待:调用 wait() 方法(调用 motify() 方法回到就绪状态)。
被另一个线程所阻塞:调用 suspend() 方法(调用 resume() 方法恢复)。
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这是线程不可能再进入就绪状态等待执行。
自然终止:正常运行 run() 方法后终止;
异常终止:调用 stop() 方法让一个线程终止运行。
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
操作系统中可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
某个类的实例在多线程环境下置灰被创建一次出来。
写法:
线程类的构造方法、静态块是被 new 这个线程类所在的线程调用的,而 run() 方法里面的代码才是被线程自身所调用的。
相同点:
两者都可以用来放弃CPU一定的时间;
不同点:
如果线程持有某个对象的监视器:
指CPU控制权由一个已经正在运行的线程切换到另一个就绪并等待获取CPU执行权的线程的过程。
线程安全:是指要控制多个线程对某个资源的有序访问或修改,而这些线程之间没有产生冲突。
线程安全问题都是由 全部变量 和 静态变量 引起的。
造成线程安全问题的主要诱因有两点:
保证线程安全的方法
重入锁:指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取对象上的锁。而其他的线程是不可以的。
死锁:如果一个进程集合里面的每个进程都在等待这个集合中的其他一个进程(包括自身)才能继续往下执行,若无外力他们将无法推进。这个情况就是死锁。处于死锁状态的进程成为死锁进程。
进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源;
进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但是又对自己获得的的资源保持不放;
是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完成后自己释放;
是指进程发生死锁后,必然存在一个进程 -- 资源之间的环形链。
通过 jConsole(JDK 自带的图形化界面工具) 检查死锁
volatile 定义:
Java 编程语言允许线程访问共享变量,为了确保共享变量能被准备和一致的更新,线程应该确保通过排它锁单独获得这个变量
依赖 JVM 实现。
每个对象都有一个监视器锁(monitor)。当 montior 被占用时,就会处于锁定状态,线程执行 monitorenter 指令时尝试获取 monitor 的所有权,过程如下:
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置FIFO队列来完成资源获取线程的排队工作。
总是认为不会产生并发问题,每次取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或者 CAS 操作实现;
总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。 synchronized 的思想就属于悲观锁。
Java 中的原子操作类
ABA 问题:
如果另一个线程修改 V 值,假设值原来是 A,先修改成 B,再修改回成 A,当前线程的 CAS 操作无法分辨当前 V 值是否发生过变化。
如何解决 ABA 问题:
用 AtomicStampedReference 解决 ABA 问题。
乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
Java 并发包常用类小结
为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径;
为了在无多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗;
通过对象内部的监视器(montior)实现,其中 montior 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高;
就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否很快释放锁。
锁优化相关知识点
explain 命令显示了 Mysql 如何使用索引来处理 SELECT 语句以及连接表。可以帮助选择更好的索引和写出更优化的查询语句。
使用方法:在 SELECT 语句前加上 explain 就可以了。
explain 列说明:
id | select 识别符。这是 select 的查询序列号 |
select_type
|
select 类型,可以为一下任何一种:
|
table | 输出的行所引用的表 |
type | 联接类型。下面给出各个联接类型,按照从最佳类型到最坏类型进行排序:
|
possible_keys
|
指出 MySQL 能使用哪个索引在该表中找到行 |
key
|
显示 MySQL 实际觉得使用的键(索引)。如果没有选择索引,键是 NULL |
key_len
|
显示 MySQL 决定使用的键长度。如果键是 NULL,则长度为 NULL |
ref | 显示使用哪个列或常数与 key 一起从表中选择行 |
rows
|
显示 MySQL 认为它执行查询时必须检查的行数。多行之间的数据相乘可以估算要处理的行数 |
filtered | 显示了通过条件过滤出的行数的百分比估计值 |
Extra | 该列包含 MySQL 解决查询的详细信息
|
数据库事务 ACID
脏读:是指一个事务处理过程中读取了另一个未提交的事务中的数据;
幻读:是事务非独立执行时发生的一种现象;
不可重复读:是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
以上四种级别中,隔离级别最高的是 Serializable ,级别最低的是 Read uncommited 。级别越高,执行效率就越低。
在 Mysql 数据库默认的隔离级别为 Repeatable read 级别。
分库与分表带来的分布式困境与应对之策
两者的区别:
InnoDB 与 MyISAM 的优缺点:
使用场景:
InnoDB 存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index,AHI)。AHI 是通过缓冲池的 B+ 树页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引。InnoDB 存储引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引。
鉴于 BTREE 具有良好的定位特性,其常被用于对检索时间要求苛刻的场合,例如:
快速理解聚集索引和非聚集索引
limit n 等价于 limit 0,n
让 limit 走索引去查询,例如:order by 索引字段,或者 limit 前面跟 where 条件走索引字段等。
首先了解下什么是 JVM 内存:
Java 源代码文件(.java后缀)会被 Java 编译器编译为字节码文件(.class后缀),然后由 JVM 中的类加载器加载各个类的字节码文件,加载完毕之后,交由 JVM 执行引擎执行。在整个程序执行过程中,JVM 会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一段被称作为 Runtime Data Area(运行时数据区),也就是我们常说的 JVM 内存。因此,在 Java 中我们常常说到的内存管理就是针对这块空间进行管理(如何分配和回收空间)。
根据《Java 虚拟机规范》的规定,运行时数据区通常包括这几个部分:
什么是 GC:
GC 是将 Java 的无用的堆对象进行清理,释放内存,以免发生内存泄漏。
常见的回收算法:
调优的目的:为了令应用程序使用最小的硬件消耗来承载更大的吞吐。
JVM 调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。
JVM 性能调优
JMM 解决了 可见性和有序性的问题,而锁解决了原子性的问题。
特性: