对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。
当一个基础数据类型与封装类进行==、+、-、*、/运算时,会将封装类进行拆箱,对基础数据类型进行运算。
equals在包装类中已经重写过,比较的是值的大小,但是也判断了是否为一个类: return (o instanceof Long) && (((Long) o).value == value);
==比较地址值,Integer有缓存,所以不用new也可能相等
1)hashCode()介绍:
hashCode()的作用是获取哈希码,哈希码是用来确定索引位置。hashCode()定义在 JDK 的 Object 类中,任何类都包含它。此方法是本地方法。
2)为什么要有 hashCode?
快速确定一个值的索引,以hashmap为例,可以用hashcode值快速判断两个值是否是同一个
3)为什么重写 equals 时必须重写 hashCode 方法?
如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据),是分别new出来的。
4)为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?
哈希碰撞
对于三者使用的总结:
阿里编约规定:for循环内使用 StingBuilder append方法拼接,不使用+号,因为+号每次循环都会new一个 StingBuilder,浪费资源
底层是一个char[] 的数组。由于value是private的,并且没有提供setValue等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改。此外,value变量是final的, 也就是说在String类内部,一旦这个值初始化了一直引用同一个对象
包装类就是把数据封装成对象,为了面向对象编程
封装
把类封装成对象,内部属性对外部隐藏。外部无法访问内部属性,但是可以通过提供的方法访问。
继承
继承是指子类对父类的属性进行复用,使得子类具有父类相同的行为。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
关于继承如下 3 点请记住:
多态
多态是同一个行为具有多个不同表现形式或形态的能力。具体表现形式为,父类引用指向子类对象。
多态的特点:
可以,通过setAccessible(ture),getDeclaredField(),getDeclaredMethod()来获取
--知乎回答
当一个类收到了加载请求时,它是不会先自己去尝试加载的,而是委派给父类加载器去完成,比如我现在要new一个Person,这个Person是我们自定义的类,如果我们要加载它,就会先委派App ClassLoader,只有当父类加载器都反馈自己无法完成这个请求(也就是父类加载器都没有找到加载所需的Class)时,子类加载器才会自行尝试加载
这样做的好处是避免核心API被篡改、避免类的重复加载
JDBC
DriverManager是被根加载器加载的,但是他在加载时会尝试加载所有Driver的实现类,但是这些实现类基本都是第三方提供的,根据双亲委派原则,第三方的类不能被根加载器加载。
共享:方法区(元空间)、堆
私有:栈、本地方法栈、程序计数器
总结:栈管运行,堆管存储
方法区:对每个加载的类型(类class、接口、枚举、注解)、域信息(public)、方法信息
见原文中虚拟机堆的概念以及Eden年轻代介绍
从类被加载到虚拟机内存中开始,到释放内存总共有7个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三个部分统称为连接。详细介绍可见原文
字符串在java程序中被大量使用,为了避免每次都创建相同的字符串对象及内存分配,JVM内部对字符串对象的创建做了一定的优化
字符串常量池比较特殊,它的主要使用方法有两种:
常量池介绍参考:https://www.cnblogs.com/syp172654682/p/8082625.html
判断一个对象的死亡至少需要两次标记
标记清除算法就是分为“标记”和“清除”两个阶段。标记出所有需要回收的对象,标记结束后统一回收。标记清除算法就是分为“标记”和“清除”两个阶段。标记出所有需要回收的对象,标记结束后统一回收
为了解决效率问题,复制算法就出现了。它将可用内存按容量划分成两等分,每次只使用其中的一块。和survivor一样也是用from和to两个指针这样的玩法。fromPlace存满了,就把存活的对象copy到另一块toPlace上,然后交换指针的内容。这样就解决了碎片的问题。
这个算法的代价就是把内存缩水了,这样堆内存的使用效率就会变得十分低下了
复制算法在对象存活率高的时候会有一定的效率问题,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存
据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,提高效率。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体。只是在JDK1.8中,链表长度大于8的时候,链表会转成红黑树!
默认大小为16,扩容因子为0.75(大于这个值碰撞的概率会大大增加,是用松柏分布算出来的),在容量大于总大小的0.75倍的时候会扩容,新的大小为原本的两倍
首先要先了解HashMap数组下标的计算方法。
那肯定会想到%取余操作,但是为了实现高效,要使用位运算,所以需要使用位运算来构建取余操作
现在问题就变为怎么使用位运算来构建取余操作
所以现在HashMap数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。
总结一下,就是在计算存放下标时,既要减少碰撞且内存能放下,使用hash并取余,又要计算高效,使用位运算,最终构建了一种计算方式,就是(n - 1) & hash,这种计算方式要求长度n必须为2次幂
String类中的hashCode计算方法还是比较简单的,就是以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模。哈希计算公式可以计为s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
此题可以组成如下连环炮来问
参考链接:https://juejin.cn/post/6844903921190699022#heading-22
在进行put操作的时候,会计算hash碰撞。如果线程A、B同时put,并且hash函数计算出的下标是相同的。如果此时A执行完判断,没有插入,CPU时间片用完。B得到时间片后正常插入。A由于已经进行完了碰撞判断,会直接进行put操作,导致后续A的put覆盖B的值,导致线程不安全。
++size,同理
分为两步
重新Hash的原因:是因为Index的计算方法是使用位运算根据数组长度计算的(HashCode(Key) & (Length - 1)),元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。这个也是长度为二次幂的原因,方便位运算
ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 、synchronized 和 volatile 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。
原子性:可以使用synchronized,同一时间只有一个线程执行该代码
可见性:当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
有序性:可以通过volatile关键字来保证一定的“有序性”,JMM通过happens-before 保证有序性
原子性、可见性、有序性都可以通过锁synchronized保证
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息
有几种情况线程会从使用CPU的的状态中退出:
线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
首先,资源是有限的,所以多个进程会竞争有限的资源。在竞争过程中,每个进程都在等待其他进程释放资源,这样没有进程抢先释放自己的资源,所有进程都会无限的等待下去,这种情况被称为死锁。
](https://www.pdai.tech/md/java/thread/java-thread-x-thread-basic.html)
实现接口会更好一些,因为:
new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,然后自动执行 run() 方法的内容。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
没有实际的需要把构造器定义成同步的,因为它将会在构造的时候锁住该对象,直到所有的构造器完成它们的工作,这个构造的过程对其它线程来说,通常是不可访问的。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
偏向锁:适用于只有一个线程访问的同步场景
偏向锁是针对于一个线程而言的, 线程获得锁之后就不会再有解锁等操作了, 这样可以省略很多开销. 假如有两个线程来竞争该锁话, 那么偏向锁就失效了, 进而升级成轻量级锁了.
为什么要这样做呢? 因为经验表明, 其实大部分情况下, 都会是同一个线程进入同一块同步代码块的
轻量级锁:追求响应时间, 同步快执行速度非常快
当出现有两个线程来竞争锁的话, 那么偏向锁就失效了, 此时锁就会膨胀, 升级为轻量级锁.两个线程竞争锁时,未竞争到锁的线程会自旋循环获取。
重量级锁:追求响应时间, 同步快执行速度非常快
加锁和释放锁的原理:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,获取和释放对象监视器 monitor 。指令会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得。
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。
保证可见性原理:如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。Synchronized的happens-before规则,即监视器锁规则:同一个监视器,先加锁,后解锁。
“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
锁对象不能为空,因为锁的信息都保存在对象头里
作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
避免死锁
不能用synchronized修饰构造器
优化 synchronized 的使用范围,让临界区的代码在符合要求的情况下尽可能的小。
使用其他类型的 lock(锁),synchronized 使用的锁经过 jdk 版本的升级,性能已经大幅提升了,但相对于更加轻量级的锁(如读写锁)还是偏重一点,所以可以选择更合适的锁。
可以根据需要实现一个 Lock 接口,这样锁的获取和释放就能完全被我们控制了
效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
无法知道是否成功获得锁,相对而言,Lock可以拿到状态,如果成功获取锁,....,如果获取失败,.....
Lock类这里不做过多解释,主要看里面的4个方法:
lock(): 加锁
unlock(): 解锁
tryLock(): 尝试获取锁,返回一个boolean值
tryLock(long,TimeUtil): 尝试获取锁,可以设置超时
ReentrantLock实现了Lock接口,ReentrantLock总共有三个内部类。
NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类
即通过继承,AQS对其服务支持
CPU缓存出现为了解决CPU速度和内存速度不一致的问题,所以就会出现CPU缓存中的变量和内存中不一致的问题
线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
所以,volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。
经典的单例模式
public class Singleton {
public static volatile Singleton singleton;
/**
* 构造函数私有,禁止外部实例化
*/
private Singleton() {};
public static Singleton getInstance() {
if (singleton == null) {
synchronized (singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
实例化一个对象其实可以分为三个步骤:
但是由于操作系统可以对指令进行重排序,多线程环境下就可能将一个未初始化的对象引用暴露出来,一些类
原理
插入lock前缀CPU指令,相当于内存屏障。这个指令的作用就是告诉CPU和编译器,这条指令不能被重排序
内存不可见的原因:线程本身并不直接与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作。这也是导致线程间数据不可见的本质原因
原理:lcok前缀指令
实际上底层也是由lock指令实现的
《深入理解Java虚拟机》
happens-before 规则中有一条是 volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
ThreadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类,就是让每个线程绑定自己的值, 避免因多线程操作共享变量而导致的数据不一致的情况。
ThreadLocalMap是ThreadLocal的静态内部类,是一个定制化的HashMap,但是没有实现Map接口
每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。系统GC的时候,key会被回收。这个时候如果线程一直不结束,就有可能发生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
官方文档:为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
好处:
《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回线程池对象的弊端如下:
ThreadPoolExecutor 3 个最重要的参数:
这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。原子类说简单点就是具有原子/原子操作特征的类。
并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出大量应用广泛的同步器
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
个人理解就是:AQS管理线程去使用、分配一个共享资源。而分配机制就是一套线程阻塞等待以及被唤醒时锁分配的机制,获取不到锁的线程在队列,获取到锁的线程使用资源。
具体说就是,AQS使用一个int成员变量来表示同步状态,通过内置的先进先出队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
[
](https://www.pdai.tech/md/java/thread/java-thread-x-lock-AbstractQueuedSynchronizer.html)
AQS定义两种资源共享方式
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
from---where--group by---having---select---order by
最左匹配原则,abc,直接用bc的时候
or关键字会让索引失效,可以用union代替
B+树的非叶子节点仅保存索引字段和指针,假设主键为bigint类型,InnoDB 的bigint占用8个字节,指针占用6个字节,8+6=14,所以我们可以得出,一个page能存放的指针个数为16k/(8+6)约等于1170每个指针对应第二层的行记录数
每个指针对应第二层的行记录数
再来说说一个page能存储多少条行记录,常规的互联网项目单条行记录大小约为1k,那么一个page能存储的行记录数为16k/1k=16
所以一个2层高的b+树能存储的行记录数大约为117016=18720
3层为11701170*16约等于2190w
[
](https://blog.csdn.net/qq_33709582/article/details/108260720)
前置通知(Before):在目标方法被调用之前调用通知功能 方法开始前开始事务
后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么 关闭数据库连接、关闭文件
返回通知(After-returning):在目标方法成功执行之后调用通知 方法完成后提交事务
异常通知(After-throwing):在目标方法抛出异常后调用通知 方法异常回滚事务
环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和之后执行自定义的行为 Transactional注解
Spring 框架中同时使用了两种动态代理 JDK Proxy 和 CGLib,当 Bean 实现了接口时,Spring 就会使用 JDK Proxy,在没有实现接口时就会使用 CGLib,我们也可以在配置中指定强制使用 CGLib
分层作答,con,service,dao
SpringBoot之常用注解 - Nihaorz - 博客园
Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,默认每 100ms 进行一次过期扫描:
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。