JAVA八股整理

1. 集合

1.1 ArrayList是否线程安全?如何线程安全地操作ArrayList?

(1)线程并不安全,容易出现问题的地方:1.增加元素 2.扩充数组长度

(2)使线程安全的操作方法

方法示例原理
VectorList list = new ArrayList(); 替换为List arrayList = new Vector<>();使用了synchronized关键字
Collections .synchronizedList(list)List<String> list = Collections.synchronizedList(new ArrayList<>()); 操作外部list,实际上修改的是原来list的数据。 注意:因为数据没用volatile,所以用迭代器的地方需要加锁,间接用到迭代器的地方也要加锁,比如:toString、equals、hashCode、containsAll等。方法都加了synchronized修饰。加锁的对象是当前SynchronizedCollection实例。
JUC中的 CopyOnWriteArrayListCopyOnWriteArrayList<String> list =       new CopyOnWriteArrayList<String>(); 适用于读多写少的并发场景。 读数据时不用加锁,因为它里边保存了数据快照。Write的时候总是要Copy(将原来array复制到新的array,修改后,将引用指向新数组)。任何可变的操作(add、set、remove等)都通过ReentrantLock 控制并发。

(2)相似的LinkedList的线程安全方法

方法示例原理
Collections.synchronizedList(List)public static List linkedList = Collections.synchronizedList(new LinkedList());所有方法都加了synchronized修饰。加锁的对象是当前SynchronizedCollection实例。
JUC中的ConcurrentLinkedQueueConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue();

1.2 HashMap、TreeMap、LinkedHashMap的区别?

(1)相同点:都是Map,都是key-value存储,都不允许key值重复,都线程不安全

(2)区别:

HashMapTreeMapLinkedHashMap
按插入顺序存放不支持不支持。支持。 遍历时,按插入的顺序出结果。
按key排序不支持。 按照hashCode进行输出。支持。 默认按key升序排序。可用Comparator自定义排序。 用Iterator 遍历TreeMap时,结果是排过序的。不支持。
数据结构数组 + 链表 + 红黑树 (put和get操作,基本可以达到常数时间的性能)红黑树。 (get或put操作的时间复杂度是O(log(n)))HashMap + 双向链表(用于维护元素顺序),是Hash Map
nullkey和value均允许为null。 只允许一条记录的key值为null(多条会覆盖); 允许多条记录的Value为 null。不允许key的值为null同HashMap

1.3 HashMap为什么线程不安全?如何线程安全地操作? 

(1)为什么线程不安全

JDK7JDK8
多线程的结果数据覆盖(场景:多线程put+put) 读出为null(场景:多线程扩容,put+get) 死循环 (场景:多线程扩容(头插法导致),,transfer函数转移时逆序)

数据覆盖(场景:多线程put) 读出为null(场景:多线程扩容,put+get) 不会发生:死循环(因为是尾插法)

(2)如何线程安全地操作HashMap?

方法示例原理性能
HashTableMap<String, Object> map =      new Hashtable<>();synchronized修饰get/put方法。 方法级阻塞,只能同时一个线程操作get或put很差。
Collections.synchronizedMapMap<String, Object> map = Collections.synchronizedMap( new HashMap<String, Object>());所有方法都使用synchronized修饰。很差。 和HashTable差不多。
JUC中的ConcurrentHashMapMap<String, Object> map =          new ConcurrentHashMap<>();每次只给一个桶(数组项)加锁。很好。

1.4 currentHashMap的原理?

JDK1.7JDK1.8
实现方式HashMap(数组 + 链表)HashMap(数组 + 链表 + 红黑树)
保证线程安全的方式使用ReentrantLock 对 Segment同步读操作:volatile; 写操作:synchronized + CAS
粒度对需要进行数据操作的Segment加锁对每个桶(数组项)加锁

CAS(Compare and Swap):在判断数组中当前位置为null的时候,使用CAS把这个新的Node写入数组中对应的位置。

synchronized:当数组中的指定位置不为空时,通过加锁来添加这个节点到链表或红黑树中。

当链表大于8,数组长度小于64:HashMap扩容(初始:2^4,最大长度:2^30,扩张因子:0.75,每次扩2倍)

JDK7:ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。 

2.JVM

2.1 类加载过程

(1)加载:将硬盘上的Java二进制文件(class文件)转为内存(不一定在堆中,HotSpot是在方法区)中的Class对象(作为方法区这个类的各种数据的访问入口)

(2)链接:验证、准备【给类变量(静态变量)分配内存(方法区)并设置为零值(0、false、null等)。(例外:static final类型的String或基本类型,直接赋值为最终值)】、解析(可选)【符号饮用改为直接引用】

(3)初始化:执行类变量(静态变量)的赋值动作和静态语句块(按定义的顺序从上往下执行)。优先级:静态、父类、子类(初始化是操作类变量(也就是静态变量),不是对象的变量)

(4)使用:

  • 若是第一次创建 Dog 对象(对象所属的类没有加载到内存中)则先执行上面的操作。
  • 在堆上为 Dog 对象(包括实例变量)分配空间,所有属性都设成默认值(数字为 0,字符为 null,布尔为 false,引用被设成 null)
  • 初始化实例:给实例变量赋值、执行初始化语句块
  • 执行构造函数检查是否有父类,如果有父类会先调用父类的构造函数
  • 执行本类的构造函数。

有且仅有 5 种情况必须立即对类进行“初始化”:

  • 在遇到 new、putstatic、getstatic、invokestatic 字节码指令时,如果类尚未初始化,则需要先触发其初始化。
  • 对类进行反射调用时,如果类还没有初始化,则需要先触发其初始化。
  • 初始化一个类时,如果其父类还没有初始化,则需要先初始化父类。
  • 虚拟机启动时,用于需要指定一个包含 main() 方法的主类,虚拟机会先初始化这个主类。
  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发其初始化。

3. 多线程 

3.1 哪些地方可以用到多线程?

  • 定时任务。    比如:定时处理数据进行统计等
  • 异步处理。    比如:发邮件, 记录日志, 发短信等。比如注册成功后发激活邮件
  • 批量处理,缩短响应时间。

 3.2 线程池用法?

(1)主要参数:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

a. corePoolSize,线程池的核心线程数,即便是线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。
b. maximumPoolSize,最大线程数。超过此数量,会触发拒绝策略。
c. keepAliveTime线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。
d. unit指定keepAliveTime的单位比如:秒:TimeUnit.SECONDS。
e. workQueue一个阻塞队列,提交的任务将会被放到这个队列里。
f. threadFactory线程工厂,用来创建线程,主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。
j. handler拒绝策略当线程池里线程被耗尽,且队列也满了的时候会调用。默认拒绝策略为AbortPolicy。即:不执行此任务,而且抛出一个运行时异常

(2) 线程池大小:

  • CPU密集型:核心线程数 = CPU核数 + 1
  • IO密集型:核心线程数 = CPU核数 * 2 + 1
  • 最优的池大小=N * U / (1 – f)(N:CPU的数量,U = 期望的CPU的使用率,介于0-1之间,f:阻塞系数(阻塞时间占总时间的比例。总时间:阻塞时间 + 执行时间))

 3.3 什么时候触发最大线程条件?

核心线程池满了,工作队列也满了,会触发最大线程条件,线程池会根据最大线程数的设置决定是否创建新的线程来处理任务。如果线程池满了,则触发饱和(拒绝)策略。

  4. Mysql

4.1 事隔离级别?脏读、不可重复读、幻读是什么意思?

MySQL有四种隔离级别:未提交读,提交读,可重复读,序列化。

  • 脏读:一个事务读取另外一个事务还没有提交的数据。(提交读解决)
  • 不可重复读:指在一个事务内,多次读同一条件的数据,数据条数相同但数据的值发生了改变。(可重复读解决)
  • 幻读:指在一个事务内两次读取同一条件的数据,两次读取的数据条数不同。(序列化)
  • 幻读与不可重复读的区别就是:不可重复读是由于其他事务 update 导致的;幻读是由于其他事务 insert 或 delete 导致的。

4.2 MySql索引失效的原因/解决方案  

(1)索引失效的场景:

  • LIKE 以%或者_开头:%和_这两个是模糊匹配,如果放在开头则不走索引。
  • OR 语句前后没有同时使用索引:当OR 左右查询字段只有一个是索引,该索引失效,只有当OR 左右查询字段均为索引时,才会生效。
  • 联合索引没遵循最左前缀原则:Mysql的联合索引采用的B+树的结构,联合索引从左向右,优先级逐渐减小,假设联合索引abc,查询时按照b,c, bc查询,则不满足最左前缀原则,联合索引失效。
  • 索引列数据类型出现隐式转化:a. 索引失效:SELECT * FROM tb1 WHERE name = 123     b.索引有效:SELECT * FROM tb1 WHERE name = ‘123’
  • 对索引列进行计算或使用函数:索引是针对原值建的二叉树,将列值计算后,原来的二叉树就用不上了。
  • ORDER BY使用错误:比如用函数表达式、对多个单独的索引字段做order等。
  • 全表扫描比索引快,此时mysql会使用全表扫描:例如数据量极少的表,或者索引字段数据重复率过高
  • WHERE 子句中使用参数可能会失效:SELECT id FROM t WHERE num = @num
  • 索引列使用 IS NULL 或 IS NOT NUL:索引无法通过空值计算出存储它的位置
  • 索引列使用NOT,<>,!=:只会走全表扫描

(2)如何避免索引失效?

避免索引失效的关键在于理解查询条件、索引的工作原理以及如何优化查询。通过合理设计索引和查询,确保索引能够高效地发挥作用,从而提升数据库的查询性能。

4.3 联合索引?最左匹配原则?

联合索引为B+树结构,联合索引(a,b),a的值若确定,则b也是有序的

联合索引有效的情况:

  1. SELECT * FROM table_name WHERE a=XX
  2. SELETE * FROM table_name WHERE a=XX AND b=YY
  3. SELECT * FROM table_name WHERE b=YY AND a=XX:这条语句不符合最左匹配原则。但由于查询优化器的存在,MySQL优化器会自动调整where后的a,b的顺序与索引顺序一致。

联合索引无效的情况:

  1. SELECT * FROM table_name WHERE b=YY(不满足最左匹配原则)
  2. SELECT * FROM table_name WHERE a>XX AND b=YY(对于联合索引,会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,此处只会用到a索引,不会用到b索引)

4.4 数据库分表

(1)垂直分表:

db6dfdd26fa44d559b51e4b4b67e08fb.png

(2)水平分表:

优点:当一张表到达一定的数据量后(如500万条数据),索引的成本也随之增加,使用主键索引查找数据时也显得十分吃力,数据检索效率低。

概念:水平分表是将一个数据量大的表按照一定的规则拆分成多个结构相同的表,将数据分散到拆分出来的表中。拆分后,当我们查找某条数据时,只需要按照拆分表时的规则推断出需要查询的数据具体存在哪一张表中,到对应的表查找数据即可。

常见的分表方案:结合数据属性(按时间划分、地理位置等)、按主键id划分、Hash分表等

4.5 分布式ID-如何维护全局唯一ID?

随着业务的增长,一些表可能要占用很大的物理存储空间,为了解决该问题,后期使用数据库分片技术。将一个数据库进行拆分,通过数据库中间件连接。如果数据库中该表选用ID自增策略,则可能产生重复的ID,此时应该使用分布式ID生成策略来生成ID。

6fa7f3ec6d8840c4bafea727e7d17964.png

维护全局ID的方法:Redis、UUID(JDK自带随机UUID、基于名字的UUID)、雪花算法、百度-UidGenerator、美团Leaf

一般情况下推荐雪花算法,如果对时钟回拨要求比较高,则推荐百度、美团

39bdf96e308a4ad48383829151243249.pngRedis缺点增加:数据持久化做的不好,UUID缺点增加:随机UUID不利于Mysql的B+树索引排序

5. Redis

5.1 Redis数据类型及使用场景

(1)String:Redis缓存,计数器,共享session,限制访问次数和频率

(2)Hash:存储对象信息

(3)List:支持高并发(将数据缓存到Redis,之后异步处理),组成程其他数据结构

(4)Set:取交集等(用户共同爱好)

(4)zSet:排序(排行榜等)

(5)bitmap:存储大型数据(判断用户登录,近期7天用户的签到数等)

5.2 Redis为什么快?

(1)数据放在内存中

(2)IO多路复用:保证多连接时的高吞吐量,select,poll,epoll。epoll是最新的也是目前最好的多路复用技术,Redis使用的epoll(Netty也是用的这个),它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

(3)底层使用C语言

5.3 穿透、无底洞、雪崩、击穿 解决方案?

(1)穿透

穿透:缓存穿透是指查询一个根本不存在的数据,缓存层没有命中,然后去查数据库(持久层),数据库(持久层)也没有命中。通常如果从存储层查不到数据则不写入缓存层。大量存储层空命中,可能就是出现了缓存穿透问题。

比如:用户不断发起请求,通过文章的id来获取文章,如果这个id没有对应的数据,则每次都会请求到数据库。如果这个用户是攻击者,在请求过多时会导致数据库压力过大,严重会击垮数据库。

原因:恶意攻击、爬虫导致的大量空命中,自身业务或者数据出现问题

解决方法:主要解决通常如果从存储层查不到数据则不写入缓存层的问题--缓存空对象、布隆过滤器

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值