1、Serializable接口为什么需要定义serialVersionUID常量?
a、版本验证,在反序列化时如果原实体类出现了增删字段,确保任然能够反序列化到新类中,如果不显示的定义serialVersionUID,则jvm会依据类的信息自动生成一个serialVersionUID,当类信息发生变化时,会自动生成一个新的serialVersionUID,导致反序列化失败
b、安全性,防止恶意用户截取数据后,将数据反序列化到他自己的实体类上
c、可读性,看到serialVersionUID可以明确这个实体类可以进行序列化和反序列化,且serialVersionUID可以用来做版本控制
2、内存泄漏和内存溢出的表现形式和形成原因及解决方案
a、内存泄漏的形成原因是资源使用完未关闭等方式导致分配的内存回收不了,导致对象在垃圾回收时回收不了,表现形式为随着系统的运行可用内存越来越少,最终导致系统卡顿,内存资源耗尽。
b、内存溢出的形成原因是向系统申请内存,而系统没有足够的内存分配,导致内存溢出,表现形式为索引越界,程序立即抛出内存溢出异常。
c、解决方案:内存泄漏,检查代码确保不使用完毕的动态内存及时释放,如关闭资源等;内存溢出,调整启动参数给程序分配更多的内存、给系统增加内存、优化算法和数据结构,合理分配内存;
3、线程间的通信方式主要有哪些?
a、线程通信的前提是线程间进行了同步;
b、线程采用不同的同步方式其通信方式也不同,通过sychronized关键字方式同步,需要依赖Monitor同步监视器,就是锁对象调用Object的wait()、notify()、notifyAll()方法进行线程通信;如果采用Lock接口方式同步,则需要依赖Condition的await()、signal()、signalAll()方法实现线程通信;
c、同步是基于同步队列的,即将同步线程放入同步队列中;等待是基于等待队列的,即将线程从同步队列中放入等待队列中,而唤醒方法是将线程从等待队列中放入同步队列中
4、sychronized锁的底层原理?
sychronized底层是通过Java的对象头实现的,对象头主要包含mark word、metadata Adress、array length三部分,其中markword主要存储对象的hashcode和锁信息、metadata Adress主要存对象的类型、arraylength主要存数组类型的长度;sychronized锁升级时主要针对的就是markword
5、Lock锁的底层原理?
Lock接口锁主要是基于AQS同步框架实现的,即Lock接口的实现类需要继承AQS同时重写其指定的方法,并且使用AQS的同步状态记录锁的信息,AQS是基于模板方法的,Lock的经典实现包括ReentrantLock可重入锁、ReadWriteLock接口的经典实现ReentrantReadWriteLock,他们各自内部也支持公平锁和非公平锁。
6、sychronized锁升级的过程?
a、开始时,没有线程访问,sychronized处于无锁状态
b、当线程1访问时,尝试通过CAS方式加偏向锁,因为此时没有竞争,加锁是成功的,markword中同时记录锁偏向线程的id,当线程1再次访问该同步代码块时,只需要比较markword中是否存有自己的线程id即可,无需进行加锁和解锁操作
c、当线程2访问同步代码块时,由于偏向锁中已经存有线程1的id,这时线程2会发起锁膨胀的流程撤销偏向锁中的id,锁升级为轻量级锁,线程1和线程2同时通过CAS方式加轻量级锁,如果线程1加速成功;线程2会认为线程1很快就会执行完毕,他会自旋然后再次尝试获取轻量级锁。
d、当线程2自旋一定次数之后仍然没有获取轻量级锁,这时线程2会再次发起锁膨胀流程,将锁膨胀为重量级锁,同时线程2进入阻塞状态,而线程1再次获取轻量级锁会失败,并会唤醒其他线程,同时进入可运行状态,竞争重量级锁,成功获取锁的线程执行,失败的需要进入阻塞状态等待获取锁的线程执行完毕,再唤醒处于阻塞状态的线程。锁升级的过程是不可逆的,即锁升级为重量级锁后不会出现锁降级
7、线程池的创建方式最优解?
通过Executors的newSingleThreadExecutor()等方式获取的线程池的等待队列或者最大线程数为无界即为Integer.MAX_VALUE,在并发激烈的情况下可能会导致任务堆积,致使内存耗尽。最好通过创建ThreadPoolExecutor对象方式限制等待队列和最大线程数,防止出现任务堆积的情况。
8、hashmap底层原理的描述?
主要针对1.8版本的Hashmap,主要从数据结构、put方法和扩容进行描述;
1.8版本的Hashmap的底层数据结构为数组+单向链表+红黑树
put方法描述,第一添加数据时需要对数组进行初始化,未设置初始容量和加载因子时,创建一个默认容量16、阈值12的数组,并通过添加数据的hash值定位存放在数组的位置。
再次添加数据的时候,如果要存放的数组位置已经有链表或红黑树存在时,需要比较添加值的hashcode和该位置首节点的hashcode是否相等以及key是否相等,如果相等则替换;如果不等则先判断当前首节点是否为树节点,是则按照树添加数据方式操作(红黑树添加数据之后涉及到树的再平衡),否则按照链表添加数据方式操作,两者逻辑上是一致的,以链表为例,遍历链表如果存在key相同的则替换,否则添加到链表的尾部,同时判断当前链表的长度是否大于等于8,大于则判断数组容量是否小于64,是则扩容为原数组的两倍,否则进行树化变成红黑树,当前链表的长度小于8时跳过,直接判断当前数组的实际长度是否大于阈值,如果大于值扩容(扩容之后链表需要重新调整,j位置的链表可能需要分成两部分,一部分在原位置,另一部分需要放到j+oldcap位置;红黑树也一样可能需要进行树拆分,如果拆分的树节点小于等于6,则需要去树化)
9、jdk1.8版本的ConCurrentHashMap的原理?
底层数据结构为数组+单向链表+红黑树,
同样的第一次添加数据时需要对数组初始化,只是需要通过CAS比较sizeCtrl获取初始化的机会。
在发现当前位置首节点的hash为(fh = f.hash) == MOVED==-1(当前位置已经完成数据迁移),则说明数组当前正在扩容中,当前线程可以进行辅助扩容;
否则,锁住首节点,遍历链表或者树,进行尾插,逻辑类似hashmap(树化和去树化条件也一致),添加完成后通过addCount()方法检查容器实际值和阈值比较,确定是否需要扩容(在sc < 0,且nextTable!=null表示有其他线程正在扩容,当前线程可以通过transfer函数辅助扩容),扩容完成后,需对原数组位置数据进行迁移,从后往前一段一段迁移(stride为每个线程每次分到的负责迁移的数据段),没完成一个桶的迁移工作将其首节点置为ForwardingNode,直到所有需要迁移桶的首节点都为ForwardingNode,即表示迁移工作完成(数据迁移逻辑和hashmap的一致)