文章目录
-
-
- java
-
-
- java内部类和子类有什么区别
- 静态方法和静态字段的目的是什么
- 封装及其意义
- 什么是多态
- 接口和抽象类的区别
- 静态内部类和非静态内部类的区别
- List\<String\>是否可以强转为List\<Object\>?
- java8新特性有哪些
- 阻塞IO、非阻塞IO、多路复用IO、信号驱动IO和异步IO有什么区别
- java 中的传统IO和NIO(New IO)的区别
- java NIO核心概念
- 并发和并行有什么区别
- 进程和线程的区别
- 什么是守护线程
- 创建线程的方式
- Runnable和Callable区别
- 线程状态
- 线程通信方式
- 线程池核心参数&创建方式&常见拒绝策略
- 线程池原理
- 死锁产生的四个必要条件
- 如何避免死锁?
- ThreadLocal是什么?使用场景?
- ThreadLocal原理
- ThreadLocal为什么会造成内存泄漏
- synchronized底层原理?
- volatile和synchronized的区别
- 单例模式双重检查加锁中的INSTANCE为什么要加volatile
- synchronized和ReentrantLock的相同和不同
- Java对象存储结构
- 什么是偏向锁、轻量级锁、自旋锁和重量级锁
- AQS原理
- ReentrantLock公平锁和非公平锁的实现原理
- 乐观锁适用场景
- HashTable和HashMap区别
- HashTable 如何实现线程安全?
- ConcurrentHashMap原理
- HashMap在多线程环境下会发生什么?
- TreeMap底层数据结构是什么?
- 什么是this引用溢出?什么情况下会发生?
- 代理模式是什么?
- 静态代理和动态代理的区别
- 静态代理和动态代理的优缺点
- JDK动态代理和CGLIB动态代理的区别
-
- JVM
- Spring&SpringBoot
- MySQL&数据库
- 设计模式&代码质量
- 分布式
- 网络
- Kafka
- Redis&缓存
- 问题排查经验
-
java
java内部类和子类有什么区别
内部类是指在外部类中定义一个内部类,内部类可以访问外部类的字段和方法
子类是指从父类继承一个子类,子类只能访问父类的public和protected成员
静态方法和静态字段的目的是什么
可以被所有实例共享,静态类只能访问静态方法和字段
封装及其意义
在一个基本单元中组合属性和方法,意义:模块化开发,数据隐藏
什么是多态
多态是指父对象中的行为在子对象中有不同的表现
编译时多态:方法重载
运行时多态:方法重写
接口和抽象类的区别
接口中只能声明常量和抽象方法,抽象类中可以声明常量、成员变量、普通方法和抽象方法
静态内部类和非静态内部类的区别
- 非静态内部类持有外部类对象的引用,静态内部类不持有
- 非静态内部类可以访问外部类的静态和非静态成员,静态内部类只能访问外部类的静态成员
- 静态内部类可以脱离外部类对象创建,非静态内部类不行
List<String>是否可以强转为List<Object>?
不可以,这样的转换没有意义,泛型的主要作用就是省去人为的类型强转的过程,转成List<Object>从中去元素的时候还是要强转为String,失去了使用泛型的意义,但是可以转List
java8新特性有哪些
接口中允许有方法的默认实现,这样接口中加方法的时候不需要考虑所有的实现类
stream流式处理
阻塞IO、非阻塞IO、多路复用IO、信号驱动IO和异步IO有什么区别
阻塞IO过程:
- 用户线程发起IO请求并阻塞
- 内核等待数据就绪
- 内核在数据就绪后把数据拷贝到用户线程
- 用户线程解除阻塞
阻塞IO并不一定就是效率低,线程阻塞时并没有占用CPU。
非阻塞IO过程:
while (true) {
resp = socket.nonBlockingRead();
if (resp != err) {
处理数据
break;
}
}
用户线程不停地去发起IO请求,如果数据未就绪立即返回err,用户线程继续循环发起请求,如果数据已就绪且再次收到IO请求,内核把数据拷贝给用户线程去处理。
缺点:CPU占用高。
多路复用IO
用单个用户线程去轮询所有的socket状态,发现IO事件到达,才会真正去调用资源做IO操作,如果没有IO事件到达,则用户线程阻塞。好处是不用创建和维护大量线程,且轮询socket状态由内核去做,效率比非阻塞IO中的用户线程轮询要高。如果某个IO操作很耗时,会导致后续到达的IO事件堆积。
信号驱动IO
用户线程注册一个数据就绪回调,并发起IO请求,不阻塞,内核收到IO请求等待数据就绪,给用户线程发数据就绪信号,触发用户线程执行回调,在回调中调用资源执行IO操作。
异步IO
用户线程发起IO请求,不阻塞,内核收到IO请求,等待数据就绪,把数据拷贝到用户线程并通知用户线程整个IO操作已完成,然后用户线程直接去使用即可。Java7以后提供了异步IO的API。
java 中的传统IO和NIO(New IO)的区别
- 传统IO是阻塞IO,NIO是多路复用IO。
- 传统IO面向流,NIO面向缓冲区。
- 传统IO不可随机访问流中的数据,NIO可随机访问缓冲区中的数据。
- NIO是非阻塞的,体现在Reactor 模式下,客户端的连接不会直接与服务端的 Handler 线程绑定。当客户端不关闭连接,但又不传输数据时,Handler线程将不会因此而阻塞,这样同一个 Handler 线程就可以处理其它客户端的请求,而避免了像传统 IO 一样一直等到连接关闭。
java NIO核心概念
- Channel:和流(Stream)类似,流是单向的,但Channel可以是双向的,常见的Channel有FileChannel、SocketChannel和ServerSocketChannel,分别对应文件IO,TCP client IO和TCP server IO。
- Buffer:本质上是一个数组,所有对Channel的读写操作必须都要先经过Buffer。
- Selector:能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
并发和并行有什么区别
并发是指一个处理器同时处理多个任务,在不同的任务中快速切换,造成一种同时处理的假象,并行是指多个处理器同时处理多个任务
进程和线程的区别
- 进程是指执行中的一段程序,线程是指进程执行过程中的每个任务
- 一个进程可以包含多个线程,一个线程只能属于一个进程
- 线程无地址空间,它包括在进程的地址空间中
- 创建线程的开销小于进程
什么是守护线程
后台提供通用服务的线程,守护线程是所有非守护线程的“保姆”,当JVM中只剩下守护线程时,守护线程无事可做,JVM也就退出了
创建线程的方式
- 继承Thread,重写run方法
new Thread(runnable)
或new Thread(new FutureTask(callable))
Runnable和Callable区别
- Runnable无法返回线程执行接口,Callable可以通过call方法的返回值返回线程执行结果
- Runnable无法抛出受检异常,Callable可以
- Callable没有继承Runnable,不能直接作为
new Thread(target)
中的target,需要通过FutureTask适配
线程状态
New: 对应初始状态
Runnable: 对应就绪和运行状态
Blocked: 对应休眠状态,没抢到互斥锁时进入该状态
Waiting: 对应休眠状态,无期限等待其他线程唤醒,调用Object.wait()
或Thread.join()
或LockSupport.park()
进入该状态
Time Waiting: 对应休眠状态,一段时间后被系统自动唤醒
Terminated: 对应终止状态
线程通信方式
共享内存和消息传递。消息传递比如wait()和notify(),或消息队列等。
线程池核心参数&创建方式&常见拒绝策略
核心线程数、最大线程数、空闲线程存活时间,拒绝策略
创建方式两种
new ThreadPoolExecutor(..)
直接创建- 通过Executors的一系列工厂方法创建
常见拒绝策略
- 直接在调用者线程中执行任务
- 直接丢弃任务
- 拒绝任务,抛出异常
- 丢弃等待队列中最早的任务,加入新任务
线程池原理
- 创建核心线程执行任务
- 如果核心线程数达到最大,进入等待队列
- 如果等待队列满了,创建非核心线程执行任务,非核心线程等待达到最大存活时间后销毁
- 非核心线程数达到最大,走拒绝策略
死锁产生的四个必要条件
- 互斥条件:同一时刻,一个资源只能被一个线程使用。
- 请求和保持条件:线程在申请一个资源的时候不会释放已有的资源。
- 不剥夺条件:线程持有的资源在未使用完时不可强行剥夺。
- 循环等待条件:若干线程之间形成头尾相连的循环等待资源的关系。
如何避免死锁?
-
同一个线程中避免多次锁定
-
加锁顺序相同
-
使用定时锁
-
死锁检测算法,检测的是当前时刻是否处于死锁状态
boolean testDeadLock() { // 出现死锁返回true // n 整型 进程数量 // m 整型 资源的种类数 // available[m] 每种资源的可用数量 // alloc[n][m] 进程i持有资源j的数量 // req[n][m] 进程i当前申请的资源j的数量,假定后续不会再申请 // finish[n] 进程i是否结束 change = true; // 循环控制变量 while (change) { change = false; for (int i = 0; i < n; i++) { if (!finish[i] && req[i] <= available) { available += alloc[i]; finish[i] = true; change = true; } } } for (boolean f : finish) { if (!f) { return true; } } return false; }
-
死锁预防:银行家算法,模拟所有进程从当前时刻到结束整个过程,检测是否会发生死锁
// need[n][m] 进程i需要的资源j的总数 change = true; // 循环控制变量 while (change) { change = false; for (int i = 0; i < n; i++) { if (!finish[i] && need[i] <= available) { available += alloc[i]; finish[i] = true; change = true; } } }
ThreadLocal是什么?使用场景?
每个线程都创建自己的副本对象,线程之间互不干扰。
使用场景:
- 每个线程维护一个私有的数据库连接,不会出现意外关闭其他线程的连接的情况
- web中每个线程维护一个私有的session
- 解决线程安全问题,对需要线程隔离的对象,使用ThreadLocal存储确保线程隔离
ThreadLocal原理
Thread对象内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量的弱引用,value为变量副本。
ThreadLocal为什么会造成内存泄漏
首先ThreadLocalMap使用ThreadLocal的弱引用作为key是为了避免内存泄漏,如果是强引用,那么ThreadLocal无法被回收,除非线程结束。但是就算用了弱引用还是会有内存泄漏的问题。如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
ThreadLocal.remove()
会清除线程ThreadLocalMap里所有key为null的value
synchronized底层原理?
synchronized在字节码层面通过monitorenter和monitorexit进行加锁,当线程执行到monitorenter时,需要获得锁才能继续执行,执行到monitorexit时会释放锁,每个对象维护一个自己被加锁次数的计数器,当计数器不为0时,只有获得这个锁的线程才能再次获得锁。
volatile和synchronized的区别
- volatile只能保证变量修改的可见性,不能保证原子性,synchronized可以保证可见性和原子性
- volatile不会造成线程阻塞,synchronized会
- volatile只能用于变量,synchronized只能用于代码块和方法
单例模式双重检查加锁中的INSTANCE为什么要加volatile
class Singleton {
private static volatile Singleton INSTENCE;
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
第八行的new并不是原子操作,翻译为字节码对应三个指令,也就是三个步骤
- 分配内存
- 执行构造方法
- 对象内存地址赋给INSTANCE引用
第一步是不会变的,一定会放在第一步执行。在没有volatile的情况下,第二步和第三步顺序是可以变化的(指令重排序)。如果没有volatile,第二步和第三步颠倒,对象在没有初始化完成的情况下就把引用提前暴露给了INSTANCE,在单线程环境下这样做没有问题,但是在多线程环境下,没有初始化完成的对象提前暴露给了其他线程。
synchronized和ReentrantLock的相同和不同
相同
- 都用来协调多线程对共享资源的访问
- 都是可重入的
- 都是互斥锁
不同
- ReentrantLock显式获得释放锁,synchronized隐式
- ReentrantLock可中断,可超时,更灵活
- ReentrantLock是API级别的,synchronized是JVM级别的
- ReentrantLock可实现公平锁,而synchronized是非公平的
- ReentrantLock是乐观并发策略(CAS),synchronized是悲观并发策略
Java对象存储结构
-
对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。原因是为了寻址最优,64位机器正好8个字节。
-
MetaData:大小也通常为4字节,它主要指向类的数据,也就是指向元数据区中的位置。通过这个指针可以最终确定该对象的实际类型。
-
MarkWord:保存该对象的一些重要的运行时状态信息。如hashCode缓存、对象所属年代、是否偏向锁、偏向锁线程ID、偏向时间戳、指向重量级锁的指针等。MarkWord的最后两位bit是锁状态标志位,用来标记当前对象的状态。对象的所处的状态,决定了markword存储的内容,如下表所示:
什么是偏向锁、轻量级锁、自旋锁和重量级锁
- 自旋锁:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗。
JDK5默认启用自旋锁且自旋时间阈值写死,JDK6引入适应性自旋锁,也就是自旋时间阈值不再固定,而是根据上一次自旋时间和CPU状态等多种因素确定。
-
重量级锁synchronized:
-
Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中
-
Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中
-
Wait Set:哪些调用wait方法被阻塞的线程被放置在这里
-
OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck
-
Owner:当前已经获取到所资源的线程被称为Owner
JVM每次从ContentionList的尾部取出一个线程用于锁竞争候选(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。
OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。
处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成。
线程在进入ContentionList之前会先尝试自旋获取锁,获取不到才会进入ContentionList,这对于ContentionList中的线程以及OnDeck线程都是不公平的。
-
-
偏向锁:如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,