Android 高频面试必问之Java基础

最后

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 本地方法栈中 JNI 引用的对象。
6.2 垃圾收集算法

标记清除法

如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。 标记复杂算法有两个主要的缺点:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

复制的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 它的优点是每次只需要对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。而缺点也是显而易见的,内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

标记整理法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。 根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法 分代收集算法,就是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

7,成员变量和局部变量的区别

  • 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public、private、static 等修饰符所修饰,而局部变量不能被这些修饰符所修饰;但是它们都可以被 final 所修饰。

  • 从变量在内存中的存储方式来看:如果成员变量被 static 所修饰,那么这个成员变量属于类,如果没有被 static 修饰,则该成员变量属于对象实例。对象存在于堆内存,局部变量存在于栈内存(具体是Java虚拟机栈)。

  • 从变量在内存中的生存时间来看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用结束而自动消失。

  • 成员变量如果没有赋初始值,则会自动以类型的默认值而赋值(例外:被 final 修饰的成员变量必须在初始化时赋值),局部变量则不会自动赋值。

8,Java 中的方法重写(Overriding)和方法重载(Overload)的含义

方法重写 在Java程序中,类的继承关系可以产生一个子类,子类继承父类,它具备了父类所有的特征,继承了父类所有的方法和变量。子类可以定义新的特征,当子类需要修改父类的一些方法进行扩展,增大功能,程序设计者常常把这样的一种操作方法称为重写,也叫称为覆写或覆盖。

方法重写有如下一些特点:

  • 方法名,参数列表必须相同,返回类型可以相同也可以是原类型的子类型

  • 重写方法不能比原方法访问性差(即访问权限不允许缩小)。

  • 重写方法不能比原方法抛出更多的异常。

  • 重写发生在子类和父类之间。

  • 重写实现运行时的多态性。

方法重载 方法重载是让类以统一的方式处理不同类型数据的一种手段。调用方法时通过传递给它们的不同个数和类型的参数来决定具体使用哪个方法,这就是多态性。所谓方法重载是指在一个类中,多个方法的方法名相同,但是参数列表不同。参数列表不同指的是参数个数、参数类型或者参数的顺序不同。

  • 方法名必须相同,参数列表必须不同(个数不同、或类型不同、参数类型排列顺序不同等)。

  • 方法的返回类型可以相同也可以不相同。

  • 重载发生在同一类中。

  • 重载实现编译时的多态性。

9,简单介绍下传递和引用传递

按值传递:值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。简单来说就是直接复制了一份数据过去,因为是直接复制,所以这种方式在传递时如果数据量非常大的话,运行效率自然就变低了,所以Java在传递数据量很小的数据是值传递,比如Java中的各种基本类型:int、float、double、boolean等类型。

引用传递:引用传递其实就弥补了上面说的不足,如果每次传参数的时候都复制一份的话,如果这个参数占用的内存空间太大的话,运行效率会很底下,所以引用传递就是直接把内存地址传过去,也就是说引用传递时,操作的其实都是源数据,这样的话修改有时候会冲突,记得用逻辑弥补下就好了,具体的数据类型就比较多了,比如Object,二维数组,List,Map等除了基本类型的参数都是引用传递。

10,为什么重写 equals 时必须重写 hashCode 方法

下面是使用hashCode()与equals()的相关规定:

  • 如果两个对象相等(即用 equals 比较返回 true),则 hashcode 一定也是相同的;

  • 两个对象有相同的 hashcode 值,它们也不一定是相等的(不同的对象也可能产生相同的 hashcode,概率性问题);

  • equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。

为什么必须要重写 hashcode 方法?其实就是为了保证同一个对象,保证在 equals 相同的情况下 hashcode 值必定相同,如果重写了 equals 而未重写 hashcode 方法,可能就会出现两个没有关系的对象 equals 相同的(因为 equals 都是根据对象的特征进行重写的),但 hashcode 确实不相同的。

11,接口和抽象类的区别和相同点是什么

相同点

  • 接口是绝对抽象的,不可以被实例化,抽象类也不可以被实例化。

  • 类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。

异同点:

  • 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

  • 定义接口的关键字是 interface ,抽象类的关键字是 abstract class

  • 接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。

  • 类可以实现很多个接口,但是只能继承一个抽象类,接口可以继承多个接口

  • Java 接口中声明的变量默认都是 public static final 的。抽象类可以包含非 final 的变量。

  • 在JDK1.8之前,接口中不能有静态方法,抽象类中可以有普通方法和静态方法;在 JDK1.8后,接口中可以有默认方法和静态方法,并且有方法体。

  • 抽象类可以有构造方法,但是不能直接被 new 关键字实例化。

  • 在 JDK1.8 前,抽象类的抽象方法默认访问权限为 protected,1.8默认访问权限为 default,共有 default,protected 、 public 三种修饰符,非抽象方法可以使用四种修饰符;在 JDK1.8 前,接口方法默认为 public,1.8时默认为 public,此时可以使用 public 和 default,1.9时接口方法还支持 private。

12,简述下HashMap

HashMap底层采用了数组+链表的数据结构,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。

如果定位到的数组位置不含链表,那么执行查找、添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值initialCapacity默认为16,loadFactory默认为0.75。

public HashMap(int initialCapacity, float loadFactor) {

//此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1<<30(230)

if (initialCapacity < 0)

throw new IllegalArgumentException("Illegal initial capacity: " +

initialCapacity);

if (initialCapacity > MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

if (loadFactor <= 0 || Float.isNaN(loadFactor))

throw new IllegalArgumentException("Illegal load factor: " +

loadFactor);

this.loadFactor = loadFactor;

threshold = initialCapacity;

init();//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现

}

加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。

Put过程

  • 判断当前数组是否要初始化。

  • 如果key为空,则put一个空值进去。

  • 根据key计算出hashcode。

  • 根据hsahcode定位出在桶内的位置。

  • 如果桶是链表,则需要遍历判断hashcode,如果key和原来的key是否相等,相等则进行覆盖,返回原来的值。

  • 如果桶是空的,说明当前位置没有数据存入,新增一个 Entry 对象写入当前位置.当调用 addEntry 写入 Entry 时需要判断是否需要扩容。如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。而在 createEntry中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。

Get过程

  • 根据key计算出hashcode,并定位到桶内的位置。

  • 判断是不是链表,如果是则需要根据遍历直到 key 及 hashcode 相等时候就返回值,如果不是就根据 key、key 的 hashcode 是否相等来返回值。

  • 如果啥也没取到就返回null。

JDK 1.8的HashMap底层采用的是链表+红黑树,增加一个阈值进行判断是否将链表转红黑树,HashEntry 修改为 Node,目的是解决hash冲突造成的链表越来越长、查询慢的问题。

Get过程

  • 判断当前桶是不是空,空就需要初始化;

  • 根据key,计算出hashcode,根据hashcode,定位到具体的桶中,并判断当前桶是不是为空,为空表明没有hsah冲突创建一个新桶即可;

  • 如果有hash冲突,那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回;

  • 如果当前位置是红黑树,就按照红黑树的方式写入数据;

  • 如果当前位置是链表,则需要把key,value封装一个新的节点,添加到当前的桶后面(尾插法),形成链表;

  • 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树;

  • 如果在遍历过程中找到 key 相同时直接退出遍历;

  • 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖;

  • 最后判断是否需要进行扩容;

Get过程

  • 首先将 key hash 之后取得所定位的桶。

  • 如果桶为空则直接返回 null 。

  • 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。

  • 如果第一个不匹配,则判断它的下一个是红黑树还是链表。

  • 红黑树就按照树的查找方式返回值。

  • 不然就按照链表的方式遍历匹配返回值。

13, CurrentHashMap

JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作,那什么是CAS。

CAS是compare and swap的缩写,中文称为【比较交换】。CAS是一种基于锁的操作,而且是乐观锁。在Java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,性能较悲观锁有很大的提高。

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

14,介绍下什么是乐观锁、悲观锁

Java 按照锁的实现分为乐观锁和悲观锁,乐观锁和悲观锁并不是一种真实存在的锁,而是一种设计思想。

悲观锁 悲观锁是一种悲观思想,它总认为最坏的情况可能会出现,它认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。悲观锁的实现往往依靠数据库本身的锁功能实现。

Java 中的 Synchronized 和 ReentrantLock 等独占锁(排他锁)也是一种悲观锁思想的实现,因为 Synchronzied 和 ReetrantLock 不管是否持有资源,它都会尝试去加锁,生怕自己心爱的宝贝被别人拿走。

乐观锁 乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过(具体如何判断我们下面再说)。乐观锁的实现方案一般来说有两种: 版本号机制 和 CAS实现 。乐观锁多适用于多度的应用类型,这样可以提高吞吐量。

在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

15,谈谈对Java线程的理解

线程是进程中可独立执行的最小单位,也是 CPU 资源(时间片)分配的基本单位,同一个进程中的线程可以共享进程中的资源,如内存空间和文件句柄。线程有一些基本的属性,如id、name、以及priority。 id:线程 id 用于标识不同的线程,编号可能被后续创建的线程使用,编号是只读属性,不能修改。 name:线程的名称,默认值是 Thread-(id) daemon:分为守护线程和用户线程,我们可以通过 setDaemon(true) 把线程设置为守护线程。守护线程通常用于执行不重要的任务,比如监控其他线程的运行情况,GC 线程就是一个守护线程。setDaemon() 要在线程启动前设置,否则 JVM 会抛出非法线程状态异常,可被继承。 priority:线程调度器会根据这个值来决定优先运行哪个线程(不保证),优先级的取值范围为 1~10,默认值是 5,可被继承。Thread 中定义了下面三个优先级常量:

  • 最低优先级:MIN_PRIORITY = 1

  • 默认优先级:NORM_PRIORITY = 5

  • 最高优先级:MAX_PRIORITY = 10

一个线程被创建后,会经历从创建到消亡的状态,下图是线程状态的变更过程。 在这里插入图片描述

下表是展示了线程的生命周期状态变化:

| 状态 | 说明 |

| — | — |

| New | 新创建了一个线程对象,但还没有调用start()方法。 |

| Runnable | Ready 状态 线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,等待被线程调度选中 获取 cpu 的使用权。Running 绪状态的线程在获得 CPU 时间片后变为运行中状态(running)。 |

| Blocked | 线程因为某种原因放弃了cpu 使用权(等待锁),暂时停止运行。 |

| Waiting | 线程进入等待状态因为以下几个方法: Object#wait()、 Thread#join()、 LockSupport#park() |

| Terminated | 该线程已经执行完毕。 |

16, Synchronized、volatile、Lock并发

线程同步和并发通常会问到Synchronized、volatile、Lock的作用。其中,Lock是一个类,而其余两个则是Java关键字。

Synchronized

Synchronized是Java的关键字,也是Java的内置特性,在JVM层面实现了对临界资源的同步互斥访问,通过对对象的头文件来操作,从而达到加锁和释放锁的目的。使用Synchronized修饰的代码或方法,通常有如下特性:

  • Synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。

  • 不能响应中断。

  • 同一时刻不管是读还是写都只能有一个线程对共享资源操作,其他线程只能等待,性能不高。

正是因为上面的特性,所以Synchronized的缺点也是显而易见的:即如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,因此效率很低。

volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。并且volatile是禁止进行指令重排序。

所谓指令重排序,指的是处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

volatile为了保证原子性,必须具备以下条件:

  • 对变量的写操作不依赖于当前值

  • 该变量没有包含在具有其他变量的不变式中

17,锁

按照作用的不同,Java的锁可以分为如下:

悲观锁、乐观锁

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁。悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是 CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

这里说到了CAS算法,那么什么是CAS算法呢?

CAS算法

一个线程失败或挂起并不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。而CAS就是一种非阻塞算法实现,也是一种乐观锁技术,它能在不使用锁的情况下实现多线程安全,因此是一种无锁算法。

CAS算法的定义:CAS的主要作用是不使用加锁就可以实现线程安全,CAS 算法又称为比较交换算法,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS具体包括三个参数:当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。

原子更新的基本操作包括:

  • AtomicBoolean:原子更新布尔变量;

  • AtomicInteger:原子更新整型变量;

  • AtomicLong:原子更新长整型变量;

以AtomicInteger为例,代码如下:

public class AtomicInteger extends Number implements java.io.Serializable {

//返回当前的值

public final int get() {

return value;

}

//原子更新为新值并返回旧值

public final int getAndSet(int newValue) {

return unsafe.getAndSetInt(this, valueOffset, newValue);

}

//最终会设置成新值

public final void lazySet(int newValue) {

unsafe.putOrderedInt(this, valueOffset, newValue);

}

//如果输入的值等于预期值,则以原子方式更新为新值

public final boolean compareAndSet(int expect, int update) {

return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

}

//原子自增

public final int getAndIncrement() {

return unsafe.getAndAddInt(this, valueOffset, 1);

}

//原子方式将当前值与输入值相加并返回结果

public final int getAndAdd(int delta) {

return unsafe.getAndAddInt(this, valueOffset, delta);

}

}

再如,下面是使用多线程对一个int值进行自增操作的代码,如下所示。

public class AtomicIntegerDemo {

private static AtomicInteger atomicInteger = new AtomicInteger(0);

public static void main(String[] args){

for (int i = 0; i < 5; i++){

new Thread(new Runnable() {

public void run() {

//调用AtomicInteger的getAndIncement返回的是增加之前的值

System.out.println(atomicInteger.getAndIncrement());

}

}).start();

}

System.out.println(atomicInteger.get());

}

}

自旋锁、适应性自旋锁

阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程【稍等一下】,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁。

死锁

当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,都不放弃自己拥有的资源。

18,谈谈你对Java 反射的理解

所谓反射,指的是在运行状态中,对于任意一个类,都能够获取这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性,而这种动态获取的信息以及动态调用对象的方法的功能就被称为Java语言的反射机制。

使用反射前需要事先获取到的字节码,在Java中,获取字节码的方式有三种:

  1. Class.forName(className)

  2. 类名.class

  3. this.getClass()

19, 注解

Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。根据作用时机的不同,Java的注解可以分为三种:

  • SOURCE:注解将被编译器丢弃(该类型的注解信息只会保留在源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的class文件里),如 @Override。

  • CLASS:注解在class文件中可用,但会被 VM 丢弃(该类型的注解信息会保留在源码里和 class 文件里,在执行的时候,不会加载到虚拟机中),请注意,当注解未定义 Retention 值时,默认值是 CLASS。

  • RUNTIME:注解信息将在运行期 (JVM) 也保留,因此可以通过反射机制读取注解的信息(源码、class 文件和执行的时候都有注解的信息),如 @Deprecated。

20,单例

为了保证只有一个对象存在,可以使用单例模式,网上有,单例模式的七种写法。我们介绍一下常见的几种:

懒汉式 懒汉式使用的是static关键字,因此是线程不安全的。

public class Singleton {

private static Singleton instance;

private Singleton (){}

public static Singleton getInstance() {

if (instance == null) {

instance = new Singleton();

}

return instance;

}

}

如果要线程安全,那么需要使用synchronized关键字。

public class Singleton {

private static Singleton instance;

private Singleton (){}

public static synchronized Singleton getInstance() {

if (instance == null) {

instance = new Singleton();

}

return instance;

}

}

不过,使用synchronized锁住之后,运行效率明显降低。

静态内部类 静态内部类利用了classloder的机制来保证初始化instance时只有一个线程。

public class Singleton {

private static class SingletonHolder {

private static final Singleton INSTANCE = new Singleton();

}

private Singleton (){}

public static final Singleton getInstance() {

return SingletonHolder.INSTANCE;

}

}

双重校验锁

public class Singleton {

private volatile static Singleton singleton;

private Singleton (){}

public static Singleton getSingleton() {

if (singleton == null) {

synchronized (Singleton.class) {

if (singleton == null) {

singleton = new Singleton();

}

}

}

return singleton;

最后

Java架构进阶面试及知识点文档笔记

这份文档共498页,其中包括Java集合,并发编程,JVM,Dubbo,Redis,Spring全家桶,MySQL,Kafka等面试解析及知识点整理

image

Java分布式高级面试问题解析文档

其中都是包括分布式的面试问题解析,内容有分布式消息队列,Redis缓存,分库分表,微服务架构,分布式高可用,读写分离等等!

image

互联网Java程序员面试必备问题解析及文档学习笔记

image

Java架构进阶视频解析合集

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

tic Singleton getSingleton() {

if (singleton == null) {

synchronized (Singleton.class) {

if (singleton == null) {

singleton = new Singleton();

}

}

}

return singleton;

最后

Java架构进阶面试及知识点文档笔记

这份文档共498页,其中包括Java集合,并发编程,JVM,Dubbo,Redis,Spring全家桶,MySQL,Kafka等面试解析及知识点整理

[外链图片转存中…(img-JuOFjKc2-1715807810110)]

Java分布式高级面试问题解析文档

其中都是包括分布式的面试问题解析,内容有分布式消息队列,Redis缓存,分库分表,微服务架构,分布式高可用,读写分离等等!

[外链图片转存中…(img-1HS56LJu-1715807810110)]

互联网Java程序员面试必备问题解析及文档学习笔记

[外链图片转存中…(img-Ba8wtX5j-1715807810111)]

Java架构进阶视频解析合集

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值