JavaSE

# JavaSE

# Java中重写equals方法为什么要重写hashcode方法?

主要有两个原因:

1)提高效率,使用 hashCode 方法提前校验,可以避免每次对比都使用 equals 方法。 2)保证是同一个对象,如何重写了 equals 方法,而没有重写 hashCode 方法,会出现 equals 相等而 hashCode 不相等的情况。


# 自定义注解

参考文章 (opens new window)

有时候框架提供的注解不能满足我们的要求,我们需要自定义注解。 比如 Hibernate Validator 是 SpringBoot 内置的校验框架,需要校验一个参数 showStatus ,我们只希望它是 0 或者 1,不能是其他数字,此时可以使用自定义注解解决这个问题。 首先自定义一个校验注解类 FlagValidator,然后添加@Constraint 注解【约束】,使用它的 validatedBy 属性指定校验逻辑的具体实现类; 然后创建 FlagValidatorClass 作为校验逻辑的具体实现类,实现 ConstraintValidator 接口,这里需要指定两个泛型参数,第一个需要指定为你自定义的校验注解类,第二个指定为你要校验属性的类型,isValid 方法中就是具体的校验逻辑。 这个自定义注解逻辑处理类由于实现了 ConstraintValidator 接口,所以它默认被 spring 管理成 bean,所以可以在这个逻辑处理类里面用@Autowiredu 或者@Resources 注入别的服务,而且不用在类上面用@Compent 注解成 spring 的 bean.


# Java IO

底层细节,注意是底层细节,而不是怎么用 https://www.cnblogs.com/crazymakercircle/p/10225159.html Java IO 读写原理:用户程序进行 IO 操作,涉及到 read 和 write 两大系统调用。值得注意的是:read 系统调用,是把数据从内核缓冲区复制到进程缓冲区;而 write 系统调用时把数据从进程缓冲区复制内核缓冲区。二者都不是都不负责内核缓冲区和磁盘之间的数据交换。

缓冲区的目的是减少频繁的系统 IO 调用。

四种IO模型:
	同步阻塞
	同步非阻塞
	IO 多路复用
	异步 IO
阻塞 IO:指的是需要内核 IO操作彻底完成之后,才返回用户空间。阻塞指的是用户空间的执行状态,传统的IO模型都是同步阻塞IO。
同步 IO:是一种用户空间和内核空间的调用发起方式。同步 IO 是指用户空间是主动发起 IO 请求的一方,内核空间是被动接受方。

NIO 的三大核心:Channel(通道)、Buffer(缓冲区)、Selector IO 是基于字符流和字节流的,而 NIO 的是基于通道和缓冲区的,数据总是从通道读取到缓冲区的,或者从缓冲区写入通道的。 IO 的各种流是阻塞的,当一个线程调用 read() 或者 write() 时,该线程被阻塞,直到有一些数据被读取或者数据完全写完,该线程在此期间不能做别的事情。NIO 是非阻塞的。


# Tomcat

服务器 Server 服务 Service 连接器 Connector 引擎 Engine 主机 Host 应用服务 Context



# 容器

# ArrayList

为什么 Iterator 的 remove 方法可以保证从源集合中安全的删除对象,而在迭代期间不能直接删除元素?

如果迭代期间直接删除元素会抛出异常 ConcurrentModificationException。因为在迭代器内部有个 exceptedModCount 变量,该变量每次初始化迭代器的时候等于 ArrayList 的 modCount,modCount 记录了对结构修改的次数,使用原生的 API 修改,这两个变量会同步,但是手动修改元素的话,一旦运行到了检查函数两个变量就不会相等,抛出异常。所以 ArrayList 不是线程安全的。

# Java ArrayList | LinkedList | Vector的区别?

  • ArrayList 实现了 List 接口,即 ArrayList 实现了可变大小的数组。它允许所有元素,包括 null。是为可变数组实现的,当更多的元素添加到 ArrayList 的时候,它的大小会动态增大。它的元素可以通过 get/set 方法直接访问,因为 ArrayList 本质上是一个数组。 注意 ArrayList 没有同步方法。如果多个线程同时访问一个 List,则必须自己实现访问同步。一种解决方法是在创建 List 时构造一个同步的 List :

    List list = Collections.synchronizedList(new ArrayList(...));

  • LinkedList 底层是双向链表,添加和删除性能比 ArrayList 好,但是 get/set 性能相较较差。注意 LinkedList 没有同步方法。如果多个线程同时访问一个 List,则必须自己实现访问同步。一种解决方法是在创建 List 时构造一个同步的 List: List list = Collections.synchronizedList(new LinkedList(...));

  • Vector Vector 和 ArrayList 几乎是一样的,区别在于 Vector 是线程安全的,因为这个原因,它的性能较 ArrayList 差。


# 红黑树

参考文章 (opens new window)


# HashMap源码

参考文章 (opens new window)

参考文章-美团博客-Java8系列之重新认识HashMap (opens new window)

HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

# 底层原理

1.7 是数组 + 链表 1.8 是数组 + 链表 + 红黑树 引入红黑树的原因:提高性能,即解决发生哈希碰撞后,链表过长而导致索引效率低的问题,同时将时间复杂度从 O(n) 降低到 O(logn)。 HashMap 中的重要参数(变量)

  • capacity (默认 16 即 2^4, 最大容量 2^32 ):HashMap 中数组的长度
  • loadFactor(默认 0.75F):HashMap 在其容量自动增加前可达到多满的一种尺度
  • threshold(扩容阈值 = 容量 * 加载因子)默认值为 16 × 0.75 = 12

与红黑树相关的参数:(树形化:将链表转化成红黑树)

  • 桶的树化阈值:即链表转成红黑树的阈值,在存储数据时,当链表长度 > 该值时,则将链表转换成红黑树,默认值为 8

  • 桶的链表还原阈值:即红黑树转化为链表的阈值,在 resize() 时,当原有的的红黑树内数量 < 6 时,将红黑树转化为链表,默认值为 6

  • 最小化树形化阈值:即当哈希表的容量 > 该值时候才考虑树形化,否则直接扩容。该值不能小于 4 * 桶的树化阈值, 默认为 64

    哈希碰撞:对象 Hash 的前提是实现 equals 和 hashCode,hashCode 的作用就是保证对象返回唯一的 hash,但是当两个对象计算值一样时,就发生了哈希碰撞。哈希碰撞会影响性能,要尽量避免。

# 如何解决 Hash 冲突呢?

​ 1)优化 hash 算法 ​ 2)扩容机制 ​ 3)优化数据结构:比如引进红黑树,时间复杂度由 O(n) 变成了 O(logn)

# Map 的特点

  • map 的键和值都允许为空:当 key 为 null 时,hash 值默认值为 0,所以只能有一个 key 为 null,value 的值可以多个为 null。

  • Map 是线程不安全的:没有同步锁保护,多线程下建议使用 concurrentHashMap 多个线程同时修改 HashMap 会抛出异常 ConcurrentModificationException

  • Map 是不保证有序的:插入顺序和存储顺序不一定相同。存储的顺序是根据 Hash 算法计算而来的数组下标顺序,该算法讲究随机性、均匀性,不具备某种规则。

  • Map 存储位置随时间变化:因为扩容机制中需要重新计算元素的存储位置

# HashMap的结构在7和8有哪些变化?

  1. 在链表插入元素时,JDK7 采用了头插法而 JDK8 采用了尾插法。这样做的原因是 7 中单链表纵向延伸头插法容易出现环形链表死循环问题。
  2. 扩容后计算存储位置的方式不同。7 中使用异或的方式,8 中扩容后的位置 = 原位置 or 原位置 + 旧容量
  3. 底层不一样。

# 为什么扩容因子是 0.75呢?

这个值太低的话会降低空间的使用率,所以不能太小。但是如果扩容因子越大,发生哈希碰撞的概率也越大,发生碰撞之后代价更大,结果是效率变低。0.75 是空间换时间的考虑,是时间和成本的折中方案。

为什么数组的长度总是 2 的 n 次幂? (opens new window) 在确定元素放在数组中的某个位置时,先进行 Hash 运算,再对数组长度取模。HashMap 底层数组的长度总是 2 的 n 次方,这是 HashMap 在速度上的优化。当 length 总是 2 的 n 次方时,h & (length-1)运算等价于对 length 取模,也就是 h%length,但是&比%具有更高的效率。

ConcurrentHashMap 底层结构 用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap。

# hashmap 和 treemap 有什么区别?

  1. HashMap 的结构是一种散列表,采用数组 + 链表 + 红黑树的存储结构,而 treemap 的存储结构只有一条红黑树;
  2. HashMap 是非线程安全的容器,而 treemap 是线程安全的;
  3. HashMap 每次扩容到原来的两倍,而 treemap 没有扩容的概念;
  4. hashmap 比 treemap 速度快,因为 HashMap 前面还做了一层桶,查找元素要快很多;
  5. hashmap 中的元素时无序的,而 treemap 是有序的。

# ConcurrentHashMap源码解析

参考文章 (opens new window)

源码分析 (opens new window)



# 并发编程

# 创建线程的方式

  1. 继承 Thread 类:Java 单继承的局限性。
  2. 实现 Runnable 接口:不能得到 run() 方法的返回值。
  3. 实现 Callable 接口:可以通过 task.get() 方法得到返回值。JDK1.5
  4. 线程池: submit 和 execute 的区别? 1)submit 有返回值,execute 没有返回值 2)submit 处理异常更方便

# 线程的方法比较

线程中为什么调用 start()方法会执行 run()方法,不能直接调用 run() 方法吗?

经典面试题。new 一个 Thread ,线程会进入新建状态,调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配时间片之后就可以开始运行了。start()会执行线程的相应准备工作,然后自动执行 run()方法的内容,这是真正的多线程工作。

而直接执行 run()方法,会把 run()方法当成一个 main线程下的普通方法执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结:调用 start() 方法才可以启动线程并使线程进入就绪状态,而 run() 方法只是 Thread 的一个普通方法,还是在主线程里执行。


# sleep() 和 wait() 区别?

  1. sleep() 方法会让当前线程让出 CPU,但是不会释放锁。 wait() 方法会让当前线程暂时退出,同时释放同步锁。只有调用了 notify() 或者 notifyAll() ,之前调用 wait() 的线程才会有权利重新参与线程调度;
  2. sleep() 方法可以在任何地方调用,而 wait() 方法只能在同步方法或者同步代码块中调用;
  3. sleep() 是线程类 Thread 的方法,wait() 是 Object 的方法。

# ThreadLocal

  • ThreadLocal 不是为了解决多线程访问共享变量的问题,而是为每个线程创建一个单独的副本,提供了保存对象的方法和避免参数传递的复杂性。
  • ThreadLocal 实现原理和内存泄漏问题: https://www.jianshu.com/p/1342a879f523
  • ThreadLocal 有内存泄漏的风险,尽量在业务代码结束前调用 remove 方法进行数据清除。

# synchronized「待完善」

在 JDK 1.6 之前,synchronized 是重量级锁,效率低下。从 JDK 1.6 开始,synchronized 做了很多优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

synchronized 同步锁一共包含四种状态:无锁、偏向锁、轻量级锁、重量级锁,它会随着竞争情况逐渐升级。synchronized 同步锁可以升级但是不可以降级,目的是为了提高获取锁和释放锁的效率。


# volatile「待完善」

作用:

对于可见性,Java 提供了 volatile 关键词来保证可见性和禁止指令重排。volatile 提供了 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值立即被更新到主内存中,当有其他线程需要读取时,它会从主内存中读取新值。

特点:

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

如何保证原子性呢?

使用原子类 Atomic 解决原子性问题。Atomic 底层使用了 Unsafe 类,调用 C++ 代码操作内存。


# CAS

# 什么是 CAS

CAS 是 compareAndSwap 的缩写,即我们说的比较并交换。

CAS 是一种基于乐观锁的操作,包含三个操作数,内存位置值 V,预期原值 A,新值 B。如果内存地址里面的值 V 和 预期值 A 相同,那么就将内存里面的指更新为 B。CAS 是通过无限循环来获取数据的,如果在第一轮循环中,a 线程获取地址里面的值被 b 线程修改了,那么 a 线程需要自旋,到下次循环才有机会执行。

# CAS 的问题

  • 只能保证一个共享变量的原子性:当对一个共享变量执行操作时,我们可以利用循环 CAS 的方式来保证原子操作,但是对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以使用锁。 如果需要对多个共享变量进行操作,可以使用加锁方式(悲观锁)保证原子性,或者可以把多个共享变量合并成一个共享变量进行CAS操作。
  • 循环时间开销大:对于资源竞争严重的情况,CAS 自旋概率大,浪费 CPU 资源。解决办法是限制自旋次数。
  • ABA 问题:解决办法是在变量前面加版本号,每次变量更新的时候变量的版本号都+1。 从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

# AQS

AQS 的全称为 AbstractQueueedSynchronizer 抽象队列同步器 ,这个类在 java.util.concurrent.locks包下。

AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLockSemaphore,其他的诸如ReentrantReadWriteLockSynchronousQueueFutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。

AQS 简单说就是并发包里的一个核心组件,里面有 state 变量,加锁线程变量和等待队列等并发中的核心组件,维护了加锁的状态。

# 原理

如果有一个线程尝试使用 ReentrantLocklock() 方法进行加锁,会发生什么?这个 AQS 内部有一个核心变量叫做 state ,是 int 类型的,代表了加锁的状态。初始状态下,这个值为 0。另外这个 AQS 内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下这个变量是 null。

接着线程跑过来调用 ReentrantLocklock() 方法,这个加锁的过程,直接就是用 CAS 操作将 state 的值从 0 变为 1。

如果之前没加锁,那么 state 的值肯定是 0,此时线程 1 就可以加锁成功。一旦线程 1 加锁成功之后,就可以设置当前加锁线程是自己。

所以,ReentrantLock 只是一个外层的 API,内核中的锁机制实现都是依赖 AQS 组件。

假设现在线程 2 来了,state 的值不是 0,那么 CAS 操作将 state 从 0 变为 1 的过程会失效,因为当前 state 的值为 1,说明有线程已经加锁了。同时查看加锁线程并不线程 2 ,这个时候加锁失败。

线程 2 会将自己放入 AQS 的一个等待队列中,因为自己尝试加锁失败,此时就要将自己放入队列中等待。等待线程 1 释放锁之后,就可以将重新尝试获取锁了。

释放的过程详细看这篇文章: https://zhuanlan.zhihu.com/p/86072774 。


# 线程池

# 源码

/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                          int maximumPoolSize,//线程池的最大线程数
                          long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                          TimeUnit unit,//时间单位
                          BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                          ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                          RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                         ) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

ThreadPoolExecutor 中 7 大参数:

核心线程数:corePoolSize 最大线程数:maxmumPoolSize 多余线程存活时间:keepAliveTime 时间单位:unit 任务队列:workQueue 线程工厂:ThreadFactory 拒绝策略:Handler 队列满了,并且工作线程大于线程池最大数

拒绝策略:

1)ThreadPoolExecutor.AbortPolicy():抛出异常 RejectedExecutionException 2)直接丢弃 3)丢弃队列中最老的任务 4)将任务分给调用者线程去执行

# 线程池原理

线程池

阿里巴巴 Java 开发手册强制规定线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,目的是让同学更加明确线程池的运行规则避免资源耗尽的风险。 资源耗尽的风险: 1)FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2) CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

拒绝策略「待完善」


#

# 重入锁

重入锁是指一个线程获取该锁之后,该线程可以继续获取该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为 0 时,表明该锁未被任何线程所持有,其他线程可以竞争获取锁。

# 自旋锁

自旋锁是指一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能被成功获取,知道获取到锁才会退出循环。

它是为实现保护共享资源而提出的一种锁机制。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环,看是否能获取锁。