对线面试官

前言

本文内容提炼自微信公众号:对线面试官

注解

代码中的特殊标记

可在编译、类加载或运行时被读取,并执行相应操作

例如:
Spring的@Controller、@Param、@Select

Lombok的@Slf4j、@Data

Java的@Override、@Deprecated、@FunctionalInterface

Java原生注解大多用于标记和检查

Java原生还有元注解,用于修饰注解,例如@Retention和@Target

@Retention用于设置注解的生命周期(SOURCE、CLASS、RUNTIME)

@Target用于设置注解的修饰对象(TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE、ANNOTATION_TYPE、PACKAGE、TYPE_PARAMETER、TYPE_USE)

.java -> .class过程有个注解抽象语法树
在这里插入图片描述
若想在编译期处理注解,需要将注解的@Retention设为SOURCE和CLASS,并且需继承AbstractProcessor类并重写process方法(lombok的AnnotationProcessor便是如此)

上述两级别加载到jvm中后,注解将被抹除

自定义注解@Retetion一般设为RUNTIME级别,配合反射机制获取到运行时信息,并处理

泛型

在创建对象或调用方法时才明确下具体的类型

好处:
无需强制转换,代码更加简洁
程序更加健壮,编译器无警告,运行期不会抛ClassCast Exception异常

应用场景:集合

写基础组件时,可利用泛型保证组件的通用性

NIO

始于JDK1.4

出现是为了提高速度

non-blocking io / new io

传统io是以字节为单位处理,nio是以块为单位处理

传统io只能是阻塞的,而nio可做到非阻塞

组成

buffer(缓冲区):存数据
channel(管道):运数据
selector(选择器):检查channel状态

Unix下IO模型有:
阻塞io、非阻塞io、io复用、信号驱动和异步io

io复用

以Linux系统为例,其对文件的操作是通过文件描述符(fd)

io复用模型:通过一个进程监听多个fd,一旦某个fd就绪,就去通知程序可以往下处理

linux下io复用模型用的函数有select/poll和epoll

select

最大连接数为1024或2048

传入fd_set参数,只能是1024或2048(具体看操作系统位数)

fd_set底层为bitmap,0表示数据未到缓冲区,1表示数据已到达

select会不断遍历fd_set,判断标志位变更情况,若有变化通知程序往下处理

epoll

linux2.6内核正式提出,完善了select一些缺点

它定义了epoll_event结构体,不存在最大连接数的限制

epoll不再遍历所有fd,它专门为就绪的fd维护了一片空间,每次从中获取使用

零拷贝

以读请求为例,会调用read相关的系统函数,用户态->内核态

cpu让dma去磁盘把数据拷贝到内核空间

等内核缓冲区有数据后,cpu会把内核缓冲区数据拷贝到用户缓冲区

最终用户程序可以获取到数据

零拷贝将黄字过程省略,相当于磁盘数据直接拷贝到用户程序中,由此提高了效率和性能

常见零拷贝技术:
mmap(内核缓冲区与用户缓冲区共享)、sendfile(系统底层函数支持)

反射

通过反射可以在运行时获取类的信息,使代码更具通用性和灵活性

运行时指jvm加载.class运行的时期

Q:既然泛型会擦除,那为什么反射能获取到泛型的信息呢?
A:泛型虽然会擦除,但擦除是有范围的,定义在类上的泛型信息是不会被擦除的,Java编译器仍在class文件以Signature属性的方式保留了泛型信息

动态代理

代理模式的一种

代理模式有静态代理和动态代理

静态代理需要自己写代理类,实现对应的接口,比较麻烦

在Java中,动态代理有两种实现方式:JDK和CGLIB

JDK

JDK动态代理使用了反射机制

JDK动态代理会帮我们实现接口的方法,通过invokeHandler对需要增强的方法进行增强

CGLIB

CGLIB代理使用了ASM框架,通过修改其字节码生成子类

场景

Mybatis不用写实现类,只写接口就能执行SQL

Spring AOP

多线程

进程是资源分配的基本单位

线程是资源调度的基本单位

场景

Tomcat会从线程连接池取线程去处理请求

数据库连接池Druid、C3P0、DBCP

比如现在要跑一个定时任务,该任务的链路执行时间和过程都非常长,那么就用一个线程池处理该定时任务,提高了系统的吞吐量

线程安全

多个线程去执行某类,这个类始终能表现出正常的行为,那么这个类是线程安全的

解决线程安全问题的思路:
1.保证原子性,考虑atomic包
2.保证操作可见性,考虑volatile关键字
3.涉及对线程的控制,考虑CountDownLatch、Semaphore等并发工具类
4.涉及集合,考虑java.util.concurrent包下的集合类
5.若synchronized无法满足需求,考虑lock包下的类

确实存在线程安全问题 -> 上述方法选取

无脑使用synchronized会影响程序性能

死锁

原因:当前线程拥有其他线程需要的资源,当前线程等待其他线程拥有的资源,大家都不释放自己拥有的资源

死锁避免方案:
1.固定加锁的顺序,例如使用hash值的大小来确定加锁的先后
2.减小锁的粒度
3.使用可释放的定时锁

CAS

compare and swap,比较并交换,原子性操作

CAS有三个操作数:当前值A、内存值V、要修改的新值B

若A与V相等,将V改成B;若A与V不等,重试或放弃

synchronized每次只会让一个线程去操作共享变量,而CAS相当于没有加锁,多个线程可以直接操作共享资源,在实际去修改的时候才去判断能否修改成功

ABA问题

假设线程A读到当前值是10,可能线程B把值修改为100,然后线程C又把值修改为10,等到线程A拿到执行权时,因为当前值和内存值是一致的,线程A认为值自始至终没有改变,符合修改条件

解决

Java提供AtomicStampedReference,对比内存值和版本是否一致

其他

阿里巴巴开发手册推荐使用LongAdder对象,它减少了乐观锁的重试次数,比AtomicLong性能更好

AtomicLong做累加时是多个线程操作同一个目标资源,在高并发下,只有一个线程会执行成功,其他线程都会失败并不断自旋(重试),自旋将成为瓶颈

而LongAdder将目标资源分散到数组Cell中,每个线程对自己的Cell变量的value进行原子操作,大大降低了失败次数

synchronized

它是一个Java的关键字,能够将代码块或方法锁起来

互斥锁,一次只能允许一个线程进入锁住的代码块

若其修饰实例方法,对应的锁是对象实例
若其修饰静态方法,对应的锁是当前类的Class实例
若其修饰代码块,对应的锁是传入synchronized的对象实例

原理

通过反编译可知

当其修饰方法时,编译器将生成ACC_SYNCHRONIZED关键字作为标识

当其修饰代码块时,其底层是monitorenter和monitorexit指令

Mark Word

在内存中,对象一般由三部分组成:对象头、对象实际数据、对齐填充,其中对象头中有个Mark Word,它会记录对象锁相关的信息

monitor

每个对象都会有一个与之对应的monitor对象

monitor对象中存储:
1.当前持有锁的线程
2.等待锁的线程队列

优化

synchronized在jdk1.6之后做了很多优化

1.6-

synchronized在jdk1.6之前是重量级锁

线程进入对象a的同步代码块/方法时,对象a的monitor对象会把此线程的id进行存储,并将monitor对象地址设置在a的mark word中,同时其他阻塞的线程会存储到monitor的等待队列中

1.6前,加锁是依赖操作系统的mutex相关指令,会有用户态和核心态的切换,性能消耗十分明显

1.6+

1.6后,引入在jvm层面实现加锁的偏向锁和轻量级锁,不再依赖操作系统,避免了状态切换的开销

Mark Word中的四种锁状态:
1.无锁
2.偏向锁
3.轻量级锁
4.重量级锁

引入偏向锁和轻量级锁是为了在不同的使用场景使用不同的锁,进而提高效率

偏向锁

jvm认为只有单个线程会来执行同步代码(无竞争环境)

直接在Mark Word记录线程id

当有线程来时,对比线程id是否相等,相等则代表当前线程可获取到锁,执行同步代码

若不相等,则用CAS来尝试修改当前线程的id,如果CAS修改成功,代表当前线程可获取到锁,执行同步代码

若CAS失败,说明处于竞争环境,撤销偏向锁,升级为轻量级锁

轻量级锁

在轻量级锁的状态下,当前线程会在栈帧下创建Lock Record,Lock Record会把Mark Word的信息拷贝进去,且有个Owner指针指向加锁的对象

线程执行到同步代码时,则用CAS试图将Mark Word指向到线程栈帧到Lock Record

如果成功则获取到轻量级锁,如果失败则自旋重试,经过一定次数后,升级为重量级锁

重量级锁

参考1.6-

小结

锁只有升级,没有降级

只有一个线程进入临界区 — 偏向锁
多个线程交替进入临界区 — 轻量级锁
多个线程同时进入临界区 — 重量级锁

AQS&ReentrantLock

公平锁

在竞争环境下,先到临界区的线程一定比后到的线程更快地获取到锁

非公平锁

在竞争环境下,先到临界区的线程未必比后到的线程更快地获取到锁

Q:如何实现上述锁?
A:公平锁可以把竞争的线程放在一个先进先出的队列上,只要持有锁的线程执行完了,唤醒队列的下一个线程去获取锁;非公平锁则让到达线程先尝试能否获取到锁,获取不到则将其放在队尾

公平和非公平的核心:线程执行同步代码块时,是否会去尝试获取锁

synchronized是非公平锁,因为无论它处于哪种锁,线程都会先尝试获取:
1.偏向锁,CAS尝试获取
2.轻量级锁,CAS尝试获取
3.重量级锁,进入monitor对象的队前会先尝试获取

AQS

Abstract Queued Synchronizer

实现自定义锁的一个框架

底层维护了一个先进先出的队列以及state状态变量

队列中有一个个Node结点,结点标识线程、状态值、模式(独占or共享)、前驱后继结点

AQS定义了模版,具体实现由各个子类完成

ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore都是基于AQS实现的

AQS支持两种模式:独占(锁只会被一个线程独占)和共享(多个线程可同时共享锁)

ReentrantLock

加解锁过程
在这里插入图片描述

线程池

ThreadPoolExecutor重要参数:
corePoolSize(核心线程数)
maximumPoolSize(最大线程数)
keepAliveTime(线程空余时间)
workQueue(阻塞队列)
handler(任务拒绝策略)

1.若当前线程数小于corePoolSize,创建线程执行任务
2.若1不满足,若workQueue未满,将任务放入其中
3.若2不满足,若当前线程数小于maximumPoolSize,创建线程执行任务
4.若3不满足,执行任务拒绝策略
在这里插入图片描述
在这里插入图片描述
一般我们将corePoolSize和maximumPoolSize设为相同数量

若当前线程数大于核心线程数,线程达到keepAliveTime,就会对线程进行回收

Q:如果自定义线程数?
首先考量业务,是cpu密集型还是io密集型
根据经验,cpu密集型可以先给到N+1,io密集型可以给到2N试试,具体给多少还要结合压测结果

ThreadLocal

通过它可以拿到线程的变量,让每个线程都可以通过set/get操作局部变量

各线程间的局部变量不会冲突,实现了线程间的数据隔离

ThreadLocal本身并不存储值,它只是帮助线程从ThreadLocalMap中获取相应的value

一个ThreadLocal对应一个私有变量

ThreadLocal对象会被对应的栈引用关联,ThreadLocalMap的key也指向着ThreadLocal
在这里插入图片描述

Java中四种引用

强引用

最常见,只要把一个对象与一个引用变量关联上,这个引用变量就是一个强引用

只要对象没有被解除关联(引用设为null),在gc时就不会被回收

软引用

需要继承SoftReference

若内存充足,只有软引用指向的对象不会被回收,内存不足时才会被回收

弱引用

需要继承WeakReference

只要发生gc,弱引用指向的对象就会被回收

ThreadLocalMap的key是弱引用,指向ThreadLocal

虚引用

需要继承PhantomReference

主要用于跟踪对象垃圾回收的状态,当回收时通过引用队列做些通知类的工作

Q:为什么不将ThreadLocalMap的key设为强引用?

因为外界是通过ThreadRef->ThreadLocal来访问ThreadLocalMap的

若ThreadRef->null,此时外界无法访问ThreadLocalMap

那key的强引用也毫无意义

Q:ThreadLocal内存泄露?

内存泄露是用户申请内存后,使用完毕后未进行释放

导致用户无法再使用该块内存空间,系统又无法回收的情况

当ThreadLocal被回收后,ThreadLocalMap Entry的key没有了指向,但Entry仍然有ThreadRef->Thread->ThreadLocalMap->Entry value->Object这条引用链存在,导致了内存泄漏

长期性内存泄漏需要满足:ThreadLocal被回收&&线程被复用(线程池)&&线程复用后不再调用ThreadLocal的set/get/remove方法(调用时它会检查key的情况,会把指向null的清掉)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值