2022春招——Java后端

Java后端

一、Java基础

创建对象的方法

new关键字
反射
clone()
反序列化

解决哈希冲突的方法

拉链法,HashMap使用此方法。
开放地址法
再哈希法

Java的代理模式

静态代理:代理对象和目标对象要实现同一个接口
proxy动态代理:目标对象要实现一个接口,通过反射代理方法
cglib动态代理:无需实现接口,会继承目标对象,重写方法

值传递和引用传递

在这里插入图片描述

Java序列化

在这里插入图片描述
Java序列化的推荐写法

反射机制

  • 概念:在运行期,能动态获取类信息和调用对象方法。
  • 原理:当一个类被加载,JVM自动产生一个Class,Class对象是反射机制的起源。
  • 应用:
    • 注解。
    • 基础框架。先在properties文件里写好类全名,接着代码中读取properties里的类全名来获得类的Class实例,这样就可以在运行期调用对象方法。
    • 编译期不知道类名,运行期从配置文件读取类名配置。Class.forName()。
  • 总结:少用反射。可读性差,性能差,除了AOP中可使用反射(减少业务代码)。

反射能够使用私有的方法属性吗

能,调用AccessibleObject对象的setAccessible方法。AccessibleObject的子类有Method、Field、Constructor。

Statement和PreStatement的区别

Statement执行静态SQL语句。
PreStatement是预编译的SQL语句对象,SQL语句被预编译保存到对象中,数据库把执行后的对象放入命令缓冲区,并且可以重用,因此可以减少编译次数来提高数据库性能。

JDK1.8的新特性

  • 接口的默认方法
public class Main {

    public static void main(String[] args) {
	// write your code here
        Formulate f = new Formulate(){};
        f.eat();
    }
    
    interface Formulate {
        default void eat(){
            System.out.println("eat");
        }
    }
}
  • Lambda表达式
public class Main {

    public static void main(String[] args) {
	// write your code here
        List<String> names = Arrays.asList("a", "b", "c");
        Collections.sort(names, (String a, String b) -> a.compareTo(b));
        System.out.println(names);
    }
}

java编译器自动推导出参数类型

public class Main {

    public static void main(String[] args) {
	// write your code here
        List<String> names = Arrays.asList("a", "b", "c");
        Collections.sort(names, (a, b) -> a.compareTo(b));
        System.out.println(names);
    }
}
  • 函数式接口
public class Main {

    public static void main(String[] args) {
	// write your code here
        Converter<String, Integer> converter = (from)->Integer.valueOf(from);
        Integer converted = converter.convert("123");
        System.out.println(converted);
    }

    @FunctionalInterface
    interface Converter<F, T>{
        T convert(F from);
    }
}
  • 方法与构造函数引用
    使用静态方法引用
public class Main {

    public static void main(String[] args) {
        // write your code here
        Converter<String, Integer> converter = Integer::valueOf;
        Integer converted = converter.convert("123");
        System.out.println(converted); // 123
    }

    @FunctionalInterface
    interface Converter<F, T> {
        T convert(F from);
    }
}

使用构造函数引用将二者关联起来。

public class Main {

    public static void main(String[] args) {
        // write your code here
        PersonFactory<Person> personFactory = Person::new;
        Person person = personFactory.create("Peter", "Parker");
    }
}

class Person {
    String firstName;
    String lastName;

    Person() {
    }

    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}
  • Lambda作用域
  • 访问局部变量
    num可以不被final修饰,但必须是最终变量。
public class Main {

    public static void main(String[] args) {
        // write your code here
        int num = 1;
        Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
        num = 3;
    }

    @FunctionalInterface
    interface Converter<F, T> {
        T convert(F from);
    }
}
  • 访问对象字段和静态变量
  • 访问接口的默认方法
  1. Predicate接口
public class Main {

    public static void main(String[] args) {
        // write your code here
        Predicate<String> predicate = (s) -> s.length() > 0;
        predicate.test("foo"); // true
        predicate.negate().test("foo"); // false
        
        Predicate<Boolean> nonNull = Objects::nonNull;
        Predicate<Boolean> isNull = Objects::isNull;
        Predicate<String> isEmpty = String::isEmpty;
        Predicate<String> isNotEmpty = isEmpty.negate();
    }
}
  1. Function接口
public class Main {

    public static void main(String[] args) {
        // write your code here
        Function<String, Integer> toInteger = Integer::valueOf;
        Function<String, String> backToString = toInteger.andThen(String::valueOf);

        backToString.apply("123"); // "123"
    }
}
  1. Supplier接口
public class Main {

    public static void main(String[] args) {
        // write your code here
        Supplier<Person> personSupplier = Person::new;
        Person person = personSupplier.get();
    }
}

class Person {
    String firstName;
    String lastName;

    Person() {
    }

    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}
  1. Consumer接口
public class Main {

    public static void main(String[] args) {
        // write your code here
        Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
        greeter.accept(new Person("Luke", "Skywalker"));
    }
}

class Person {
    String firstName;
    String lastName;

    Person() {
    }

    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}
  1. Comparator接口
public class Main {

    public static void main(String[] args) {
        // write your code here
        Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);

        Person p1 = new Person("John", "Doe"); 
        Person p2 = new Person("Alice", "Wonderland");

        comparator.compare(p1, p2); // > 0 
        comparator.reversed().compare(p1, p2); // < 0
    }
}

class Person {
    String firstName;
    String lastName;

    Person() {
    }

    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}
  1. Optional接口
public class Main {

    public static void main(String[] args) {
        // write your code here
        Optional<String> optional = Optional.of("bam");

        optional.isPresent(); // true 
        optional.get(); // "bam" 
        optional.orElse("fallback"); // "bam"

        optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
    }
}
  1. Stream接口

串行Stream

public class Main {

    public static void main(String[] args) {
        // write your code here
        List<String> stringCollection = new ArrayList<>();
        stringCollection.add("ddd2");
        stringCollection.add("aaa2");
        stringCollection.add("bbb1");
        stringCollection.add("aaa1");
        stringCollection.add("bbb3");
        stringCollection.add("ccc");
        stringCollection.add("bbb2");
        stringCollection.add("ddd1");

        stringCollection.stream()
                .filter((s) -> s.startsWith("a"))
                .forEach(System.out::println);// "aaa2", "aaa1"

        stringCollection.stream()
                .sorted()// 排序不影响原数据源
                .filter((s) -> s.startsWith("a"))
                .forEach(System.out::println);// "aaa1", "aaa2"

        stringCollection.stream()
                .map(String::toUpperCase)
                .sorted((a, b) -> b.compareTo(a))
                .forEach(System.out::println);// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

        boolean anyStartsWithA = stringCollection.stream()
                .anyMatch((s) -> s.startsWith("a"));
        System.out.println(anyStartsWithA); // true

        boolean allStartsWithA = stringCollection.stream()
                .allMatch((s) -> s.startsWith("a"));
        System.out.println(allStartsWithA); // false

        boolean noneStartsWithZ = stringCollection.stream()
                .noneMatch((s) -> s.startsWith("z"));
        System.out.println(noneStartsWithZ); // true

        long startsWithB = stringCollection.stream()
                .filter((s) -> s.startsWith("b")).count();
        System.out.println(startsWithB); // 3

        Optional<String> reduced = stringCollection.stream()
                .sorted()
                .reduce((s1, s2) -> s1 + "#" + s2);
        reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
    }
}

并行Stream

public class Main {

    public static void main(String[] args) {
        // write your code here
        int max = 1000000;
        List<String> values = new ArrayList<>(max);
        for (int i = 0; i < max; i++) {
            UUID uuid = UUID.randomUUID();
            values.add(uuid.toString());
        }

        test1(values);
        test2(values);
    }

    public static void test1(List values){
        long t0 = System.nanoTime();

        long count = values.stream().sorted().count();
        System.out.println(count);

        long t1 = System.nanoTime();

        long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
        System.out.println(String.format("sequential sort took: %d ms", millis));// 串行耗时: 532 ms

    }

    public static void test2(List values){
        long t0 = System.nanoTime();

        long count = values.parallelStream().sorted().count();
        System.out.println(count);

        long t1 = System.nanoTime();

        long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
        System.out.println(String.format("parallel sort took: %d ms", millis));// 并行排序耗时: 214 ms
    }
}
  1. Map类型的新方法
public class Main {

    public static void main(String[] args) {
        // write your code here
        Map<Integer, String> map = new HashMap<>();

        for (int i = 0; i < 10; i++) {
            map.putIfAbsent(i, "val" + i);
        }

        map.forEach((id, val) -> System.out.println(val));
        map.computeIfPresent(3, (num, val) -> val + num);
        map.get(3); // val33
        map.computeIfPresent(9, (num, val) -> null);
        map.containsKey(9); // false
        map.computeIfAbsent(23, num -> "val" + num);
        map.containsKey(23); // true
        map.computeIfAbsent(3, num -> "bam");
        map.get(3); // val33

        map.remove(3, "val3");
        map.get(3); // val33
        map.remove(3, "val33");
        map.get(3); // null

        map.getOrDefault(42, "not found"); // not found

        map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
        map.get(9); // val9
        map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
        map.get(9); // val9concat
    }
}
  • Date API
public class Main {

    public static void main(String[] args) {
        // write your code here

        // Clock时钟
        Clock clock = Clock.systemDefaultZone();
        long millis = clock.millis();
        Instant instant = clock.instant();
        Date legacyDate = Date.from(instant); // legacy java.util.Date

        // Timezone时区
        System.out.println(ZoneId.getAvailableZoneIds()); // prints all available timezone ids
        ZoneId zone1 = ZoneId.of("Europe/Berlin");
        ZoneId zone2 = ZoneId.of("Brazil/East");
        System.out.println(zone1.getRules());// ZoneRules[currentStandardOffset=+01:00]
        System.out.println(zone2.getRules());// ZoneRules[currentStandardOffset=-03:00]

        // LocalTime
        LocalTime now1 = LocalTime.now(zone1);
        LocalTime now2 = LocalTime.now(zone2);
        System.out.println(now1.isBefore(now2)); // false
        long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
        long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
        System.out.println(hoursBetween); // -3
        System.out.println(minutesBetween); // -239

        LocalTime late = LocalTime.of(23, 59, 59);
        System.out.println(late); // 23:59:59
        DateTimeFormatter germanFormatter = DateTimeFormatter
                .ofLocalizedTime(FormatStyle.SHORT)
                .withLocale(Locale.GERMAN);
        LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
        System.out.println(leetTime); // 13:37

        // LocalDate
        LocalDate today = LocalDate.now();
        LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
        LocalDate yesterday = tomorrow.minusDays(2);
        LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
        DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
        System.out.println(dayOfWeek); // FRIDAY

        DateTimeFormatter germanFormatter1 = DateTimeFormatter
                .ofLocalizedDate(FormatStyle.MEDIUM)
                .withLocale(Locale.GERMAN);
        LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter1);
        System.out.println(xmas); // 2014-12-24

        // LocalDateTime
        LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
        DayOfWeek dayOfWeek1 = sylvester.getDayOfWeek();
        System.out.println(dayOfWeek1); // WEDNESDAY
        Month month = sylvester.getMonth();
        System.out.println(month); // DECEMBER
        long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
        System.out.println(minuteOfDay); // 1439

        Instant instant1 = sylvester.atZone(ZoneId.systemDefault()).toInstant();
        Date legacyDate1 = Date.from(instant1);
        System.out.println(legacyDate1); // Wed Dec 31 23:59:59 CET 2014

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM dd, yyyy - HH:mm");
        LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
        String string = formatter.format(parsed);
        System.out.println(string); // Nov 03, 2014 - 07:13
    }
}
  • Annotation注解
public class Main {

    public static void main(String[] args) {
        // write your code here
        Hint hint = Person.class.getAnnotation(Hint.class);
        System.out.println(hint); // null

        Hints hints1 = Person.class.getAnnotation(Hints.class);
        System.out.println(hints1.value().length); // 2

        Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
        System.out.println(hints2.length); // 2
    }

    @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
    @interface MyAnnotation {
    }

    @Hint("hint1")
    @Hint("hint2")
    class Person {
    }

    @Hints({@Hint("hint1"), @Hint("hint2")})
    class Person1 {
    }

    @interface Hints {
        Hint[] value();
    }

    @Repeatable(Hints.class)
    @interface Hint {
        String value();
    }
}

重写和重载

重写:override,是在运行时实现多态。重写的不能比被重写的声明更多异常(里氏代换原则)。
重载:overload,是在编译时实现多态。

接口和抽象类的区别

  1. 相同点
  • 不能实例化
  • 可以将二者作为引用类型
  • 某类不实现全部的抽象方法,就要声明为抽象类
  1. 不同点
  • 接口
    • 不能有构造器
    • 都是抽象方法
    • 方法都是public
    • 不能有静态方法
    • 成员变量都是常量
    • 可以实现多个接口
  • 抽象类
    • 有构造器
    • 有抽象方法和具体方法
    • 方法可以是默认、public、protected、private
    • 有静态方法
    • 可以定义成员变量
    • 只能继承一个抽象类

声明不被继承的类

用final修饰。此类所有方法都没有重写的需要时。

HashMap和HashTable的区别

  • HashMap非线程同步,HashTable线程同步
  • HashMap允许key和value有空值,HashTable不允许
  • HashMap使用Iterator,HashTable使用Enumeration
  • HashMap数组的默认大小是16,增长方式是2的指数倍;HashTable数组的默认大小是11,增长方式是2n+1
  • HashMap继承AbstractMap,HashTable继承Dictionary

HashMap线程安全的方式

  • 用Collections.synchronizedMap方法
    使用synchronized
    使用代理模式新建一个类
    锁住的是对象,故性能差
  • 用ConcurrentHashMap集合
    使用NonfairSync,使用CAS指令

HashMap在jdk1.7和1.8的区别

capacity:2 ^ n
loadFactor:默认0.75
threshold:capacity * loadFactor

java7

数据结构:数组、链表

java8

数据结构:数据、链表、红黑树
优化:红黑树降低链表查找的开销

高并发下的集合有哪些问题

在这里插入图片描述

Arrays.sort()的底层原理

Arrays.sort()的底层原理

若hashcode方法永远返回1会产生什么结果

丧失HashMap的快速查询优势

HashMap链表过长的解决

扩容。适用于hash函数值不同的情况。
树化。HashMap1.8在链表长度大于8时,当数组长度未达到64,尝试扩容;当数组长度达到64,尝试链表的树化。

HashMap底层

HashMap底层
HashMap底层
HashMap底层
HashMap底层
HashMap底层

依赖倒置原则

  • 程序依赖于抽象接口,不要依赖于具体实现,降低程序与其他模块的耦合度。
  • 抽象的变化很小,因此程序的变化也会小。

10G数据,1G内存,如何排序

外部排序:

  1. 内存中开辟大小为10的最小堆,和一个缓冲区(1G-10)
  2. 取10份排序后数据的首位进入最小堆
  3. 移除堆顶元素并写入缓冲区
  4. 从移除元素所属数组的下一位进入最小堆,继续3,直到缓冲区满,缓冲区回写磁盘,清空缓冲区,继续2
  5. 直到10份数据全部写完,将最小堆元素按顺序回写磁盘

AQS

  1. 抽象队列同步器,内部是FIFO双向队列,即CLH同步队列
  2. JUC中的锁是基于AQS的,比如ReentrantLock、ReentrantReadWriteLock
  3. 六个操作
    抢锁
    释放锁
    入队
    出队
    阻塞
    唤醒
  4. CLH自旋锁算法传送门

Java中的锁

多线程访问同一数据的不一致性问题

synchronized上锁

  • 对象在内存中的布局包括(16个字节):markword、实例数据、类型指针、对齐(保证被8整除)
  • markword:hashcode、GC信息、锁信息
  • 上锁会修改markword

自旋锁

  1. 一种乐观锁、轻量级锁
  2. CAS操作
  3. ABA问题
    给数据加版本号

重量级锁和轻量级锁

  1. 重量级锁:JVM把线程调度交给OS
  2. 轻量级锁:JVM层面的运行线程

轻量级锁一定比重量级锁效率高吗

等待线程数量过多和临界区执行时间过长,轻量级锁效率会很差

偏向锁

  • 偏向锁严格来说不是锁,比轻量级锁还轻
  • JDK15默认已关闭
  • 打开偏向锁是不是效率一定会提升?
    线程抢占锁比较激烈的时候,效率会降低

偏向锁升级

  • 没有竞争是偏向锁,在锁对象上标记执行线程即可
  • 有线程竞争锁的时候,偏向锁升级为轻量级锁
  • 当超出CPU的承受范围时,轻量级锁升级为重量级锁,线程入队列
    在这里插入图片描述

CPU缓存

  • 缓存行64个字节
  • 并发框架Disruptor为什么这么快
    缓存行填充
  • 缓存一致性协议
    缓存行标记四种状态:Modified、Exclusive、Shared、Invalid
    在这里插入图片描述
    在这里插入图片描述

volitile

创建对象的过程

  • 开辟内存空间
  • 初始化变量
  • 变量指向内存空间地址

DCL的单例要加volitile吗?

必须要。
不加volitile会出现指令重排序,此时其他线程可能拿到半初始化的对象。
在这里插入图片描述

硬件级别内存屏障的实现

lock addl

GoF设计模式

设计模式的本质是面向对象设计原则的具体应用,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解。

HashTable和CurrentHashMap的区别

  • HashTable的并发度低,整个HashTable对应一把锁
  • ConcurrentHashMap的并发度高,整个ConcurrentHashMap对应多把锁

分库分表

一般来说,在系统设计阶段就应该根据业务耦合松紧来确定垂直分库垂直分表方案,在数据量及访问压力不是特别大的情况,首先考虑缓存读写分离索引技术等方案。若数据量极大,且持续增长,再考虑水平分库水平分表方案。

线程通信

  • 问题:写两个线程,一个线程打印 1~52,另一个线程打印字母A-Z。打印顺序为12A34B56C……5152Z。
  • 方案:
    1. wait,notifyAll,synchronized
      传送门
    2. lock,condition
      传送门

死锁的必要条件

互斥条件
请求和保持条件
不可剥夺条件
循环等待条件

去重10亿手机号码

1 用BitMap数据结构
2 用String、Long、Bit存储10亿个手机号码的内存消耗如下:
在这里插入图片描述

设计登陆模块

表单设计

  • 登录名+密码+手机验证
    防止恶意攻击
  • 登录名+密码+IP限制
    使用同一IP的会被误封
  • 登录名+密码+登陆限制
    攻击者可以让所有用户都无法登录
  • 登录名+密码+验证码
    普通的图片验证码很难防止攻击
  • 登录名+密码
    高危

防范中间人攻击

  • 更换HTTPS,在HTTP和TCP之间加入TLS/SSL协议,保证数据的安全传输。
  • 优点:内容加密、数据完整性、身份验证

其他

操作日志
异常登陆提醒,登陆提醒
拒绝弱密码
防止用户名被遍历

二、Redis

一致性哈希算法

当分布式系统的节点数量发生变化时,系统对外要提供良好的服务。例如,在分布式缓存中,当节点数量变化,哈希算法会导致缓存雪崩,服务压力瞬间到了后端;一致性哈希算法采用哈希环的数据结构,只会使部分缓存失效,降低后端服务压力。
节点C失效后对象C的错误指向

哈希倾斜

在这里插入图片描述

三、计算机基础

KMP算法

def build_next(patt):
    next = [0]
    prefix_len = 0  # 最长前缀
    i = 1

    while i < len(patt):
        if patt[prefix_len] == patt[i]:
            prefix_len += 1
            next.append(prefix_len)
            i += 1
        else:
            if prefix_len == 0:
                next.append(0)
                i += 1
            else:
                prefix_len = next[prefix_len - 1]
    return next


def kmp_search(string, patt):
    next = build_next(patt)
    print(next)
    i = 0  # 主串的指针
    j = 0  # 字串的指针
    while i < len(string):
        if string[i] == patt[j]:
            i += 1
            j += 1
        elif j > 0:
            j = next[j-1]  # 字串跳过的步数
        else:
            i += 1

        if j == len(patt):
            return i - j
    return -1


if __name__ == '__main__':
    print(kmp_search('ABABAAABABAA', 'ABABAAABABAA'))

TCL/SSL协议

  • 先建立TCP连接
  • 非对称加密
  • 对称加密
    在这里插入图片描述

四、数据库

MVCC解决了什么问题

  • 解决读写冲突
  • 解决脏读、幻读和不可重复读的事务隔离问题,不能解决更新丢失问题

MVCC的实现原理

依赖于记录中的三个隐藏字段,undolog,read view来实现的

MySQL各种索引的差异

B+树索引
  • InnoDB的默认索引
  • 平衡的多叉树
哈希索引
  • 适合单条记录查询
  • 不支持范围查找

聚簇索引和非聚簇索引的概念

都是B+树的数据结构

聚簇索引

  • 叶节点的数据域存放的是数据,其把索引和数据放在一起
  • InnoDB使用

非聚簇索引

  • 叶节点的数据域存放的是数据地址,需要再到硬盘中查找数据
  • MyISAM使用

聚集索引是指数据库表行中数据的物理顺序与键值的逻辑(索引)顺序相同。一个表只能有一个聚集索引,非聚集索引是一种索引,该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同。

自增ID和UUID的区别

  • 自增ID所占的存储空间是UUID的1/2
  • 在范围查询时,自增ID的效率要大于UUID
  • UUID在增加一条记录时,会导致叶分裂,还需要调整树为平衡树

explain里面的字段

  • id
  • type
  • key
  • key_len
  • possible_keys
  • extra
  • select_type
  • table
  • rows

B+树和B树的区别

B+树是B树的优化,增加分叉来减小树的高度,还有提高范围查找的性能

B+树

  • 非叶子节点不存储data,只存储索引,可以放更多索引
  • 叶子节点用双向指针连接,提高区间访问的性能

B树

  • 所有索引元素不重复
  • 索引是递增排列
  • 叶节点的指针域为空
    在这里插入图片描述

非主键索引的叶节点数据域是主键值

节省存储空间和一致性

最左前缀原则

违背此原则,会导致联合索引失效

MySQL主从同步原理

在这里插入图片描述

主要有三个线程,master的【binlog dump thread】、slave的【I/O thread】和【SQL thread】

  • 主库记录变更到binlog
  • 当binlog有变更,binlog dump thread发送binlog给从节点
  • 从节点的I/O thread接受binlog,并写到relay log中
  • 从节点的SQL thread读取relay log并更新数据,最终保证数据的一致性

全同步复制

主库等待所有从库更新完

半同步复制

主库等待一个从库更新完

补充

  1. 主主、主从、主备的区别
  2. 主从延迟的原因和解决方案
  3. 数据库高可用方案
    双机主备
    一主一从
    一主多从
    MarialDB同步多主机
    数据库中间件:Mycat分片存储,每个分片配置一主多从的集群

MySQL的幻读

  • 幻读是读到了其他事务新插入的数据
  • 可重复读的隔离级别下,事务用快照读或当前读能避免幻读,二者不能混用
  • 快照读是读取可见版本的数据,不会加锁;当前读是读取最新版本的数据,会加锁

MySQL的连接

1. 内连接

左右表相符的记录
inner join on

2. 左连接

左表全部显示,右表显示符合条件的(不符合的为NULL)
left join on

3. 右连接

与左连接相反

4. 全连接

左右表全部显示,表中没有则用NULL填充
union

5. 交叉连接

两个表的所有行进行组合,产生的笛卡尔积

6. 自连接

把一张表当成两张表使用

找出班级中同姓名的名字和人数

select name,count(name) from class group by name having count(name)>1

ACID靠什么保证

  • 原子性
    由undolog日志来保证,它记录了回滚的日志信息,能撤销已经执行成功的sql。
  • 一致性
    由其它三个特性保证。
  • 隔离性
    由MVCC保证,即多版本并发控制。
  • 持久性
    由redolog日志保证。在修改数据时会在redolog记录日志,能保证数据不丢失。

3NF的案例

在这里插入图片描述

五、框架

Mybatis插件的运行原理

  • 支持四种接口:ParameterHandler、ResultSetHandler、StatementHandler、Executor
  • 使用JDK的动态代理为需要拦截的接口生成代理对象,当执行代理对象的方法时,就会进入拦截方法。具体是InvocationHandler的invoke方法

Mybatis的优缺点

缺点:

  • 需要SQL功底
  • SQL依赖数据库,导致数据库移植性差

优点:

  • 容易上手
  • 减少JDBC的代码量
  • 与各数据库兼容
  • 提供第三方插件
  • 与Spring很好集成
  • 提供XML标签,支持编写动态SQL语句
  • 提供映射标签

Spring的优势

  • 通过DI、AOP和消除样板式代码来简化开发
  • IoC容器提高组件之间的耦合性
  • AOP支持将通用任务进行集中式处理
  • 低侵入式设计
  • Spring是Spring生态圈的基石,Spring生态圈将Spring扩展到不同的领域

Spring MVC的执行流程

  • 准备阶段
    • 创建DispatcherServlet对象,并执行init方法
    • init方法创建Spring Web容器,并执行refresh方法
    • refresh方法创建并初始化SpringMVC的重要组件,如HandlerMapping、HandleAdapter、HandleExceptionResolver、ViewResolver和MultipartResolver等
  • 匹配阶段
    • 用户请求到达DispatcherServlet
    • 从HandlerMapping找到与路径匹配的处理器
    • 把处理器和匹配到的拦截器,生成HandlerExecutionChain返回
    • 从HandlerAdapter找到能处理对应处理器的适配器对象,开始调用
  • 执行阶段
    • 执行拦截器preHandle
    • 由HandlerAdapter调用处理器方法
    • 返回ModelAndView,执行拦截器postHandle,解析视图得到View并进行视图渲染
    • 执行拦截器的afterCompletion
    • 如果控制器方法标注@ResponseBody注解,生成json,就不会执行视图渲染

Spring的11种设计模式

  1. 单例模式
  2. builder模式
  3. 工厂方法模式
  4. 适配器模式
  5. 组合模式
  6. 装饰器模式
    功能增强
  7. 代理模式
    目标对象的控制访问
  8. 责任链模式
  9. 观察者模式
  10. 策略模式
    线程池的【拒绝策略】中,把任务超出阻塞队列时的处理权下放到定义线程池的地方。
  11. 模板方法模式

面向对象设计原则

  1. 开闭原则
  2. 里氏代换原则
  3. 迪米特原则
  4. 依赖倒置原则
  5. 单一职责原则
  6. 接口隔离原则
  7. 合成复用原则

23种设计模式

创建型:

  1. 单例模式
  2. 工厂方法模式
  3. 抽象工厂模式
  4. 原型模式
  5. 建造者模式

结构型:

  1. 代理模式
  2. 享元模式
  3. 适配器模式
  4. 装饰器模式
  5. 桥接模式
  6. 外观模式
  7. 组合模式

行为型:

  1. 访问者模式
  2. 观察者模式
  3. 策略模式
  4. 命令模式
  5. 职责链模式
  6. 状态模式
  7. 中介者模式
  8. 迭代器模式
  9. 备忘录模式
  10. 模板方法模式
  11. 解释器模式

spring创建bean对象

  • 调用类的推断构造方法得到对象(@AutoWired指定目标方法)
  • 通过依赖注入来初始化属性
  • 初始化前(@PostConstruct),初始化中(实现InitializingBean接口),初始化后(AOP),代理对象
  • 放入map单例池中,返回bean对象。
    注意:依赖注入先by type,再by name。

AOP

AOP,基于动态代理生成代理对象,cglib代理对象的target指向普通对象。

spring事务

使用@transactional注解的类会生成代理类,代理类里做事务的控制,用cglib代理。由事务管理器创建数据库连接、关闭自动提交和回滚操作等。

@Configuration的作用

使用@Configuration注解的类会生成代理类,用super的方式代理。防止事务管理器的数据库连接和jdbcTemplate的不同。

Spring三级缓存解决循环依赖

  1. singletonObject:存成品bean
  2. earlySingletonObject:存半成品bean
  3. singletonFactory:打破循环,判断发生循环依赖后,把依赖的bean存入earlySingletonObject,并移除singletonFactory里依赖bean的对象
    在这里插入图片描述

一级缓存的问题

陷入死循环
在这里插入图片描述

二级缓存的作用

解决循环依赖(非代理情况下)
在这里插入图片描述

二级缓存的问题

对象b中的a没有得到增强
在这里插入图片描述

三级缓存的作用

解决发生循环依赖且需要代理的问题
在这里插入图片描述

Spring中bean的生命周期

在这里插入图片描述

Spring的refresh流程

在这里插入图片描述

Spring事务失效的情况

  1. 抛出检查异常
    解法:配置rollbackfor属性
  2. 业务方法内自己try.catch异常
    解法:异常原样抛出;TransactionStatus.setRollbackOnly()
  3. AOP切面顺序
    解法:同2
  4. 非public方法
  5. 父子容器
    原因:子容器把未加事务配置的service扫描进来,父容器加事务的没有用到
    解法:不用父子容器,SpringBoot是一个容器
  6. 调用本类方法导致传播行为失效
    原因:本类方法不经过代理
    解法:依赖注入自己;通过代理上下文得到代理对象
  7. @Transactional不能保证原子性
    原因:select方法不阻塞
  8. @Transactional导致的synchronized失效
    原因:synchronized只能保证目标方法的原子性
    解法:synchronized扩大至代理方法;select…for update替代select

Configuration注解

  • 配置类相当于工厂,bean注解修饰的方法相当于工厂方法
  • 不支持方法重载
  • 为修饰的类生成代理
  • Configuration注解中如果含有BeanFactory后处理器,则实例工厂方法会导致实例提前创建, 造成其依赖注入失败

Import注解

  1. 同一配置类中, @Import 先解析,@Bean 后解析
  2. 同名定义, 默认后面解析的会覆盖前面解析的
  3. 不允许覆盖的情况下, 如何能够让 MyConfig(主配置类) 的配置优先? (虽然覆盖方式能解决)
  4. DeferredImportSelector 最后工作, 可以简单认为先解析 @Bean, 再 Import

BeanFactory和ApplicationContext的区别

相同点:

  • 都是IOC容器,都是接口,AC继承BF
  • 都能配置XML属性,都支持属性自动注入
  • 都使用getBean方法来获取bean

不同点:

  • 实例化bean的时机不同,AC是在容器启动的时候,BF是调用getBean方法。
  • 国际化i18n,AC支持,BF不支持。
  • 监听器,BF支持,AC不支持。
  • BF的核心实现是XMLBeanFactory,AC的核心实现是ClassPathXmlApplicationContext。Web环境使用WebApplicationContext,并增加getServletContext方法。
  • 如果用自动注入和BF,需要注册AutoWiredBeanPostProcessor;如果用自动注入和AC,就用XML配置。
  • BF提供IOC和DI功能,AC提供高级功能。BF用于测试和非生产使用,而AC是功能丰富的容器实现,AC优于BF。

SpringBoot的自动配置原理

涉及到@SpringBootApplication注解。由@SpringBootConfiguration、@ComponentScan、@EnableAutoConfiguration组成。

  • @SpringBootConfiguration:标识为一个配置类
  • @ComponentScan:扫描组件,其excludeFilters会排除自动配置类
  • @EnableAutoConfiguration
    • @AutoConfigurationPackage:记住扫描的起始包
    • @Import(AutoConfigurationImportSelector.class):加载META-INF/spring.factories中的自动配置类

为什么不使用@Import直接引入自动配置类

原因:

  • 让主配置类和自动配置类变成了强耦合,主配置类不应该知道有哪些从属配置
  • 直接用@Import(自动配置类.class),引入的配置解析优先级较高,自动配置类的解析应该在主配置没提供时作为默认配置

优势:

  • AutoConfigurationImportSelector.class 去读取 META-INF/spring.factories 中的自动配置类,实现了弱耦合。
  • 另外 AutoConfigurationImportSelector.class 实现了 DeferredImportSelector 接口,让自动配置的解析晚于主配置的解析

Spring的refresh方法

在这里插入图片描述

  1. 准备工作
  2. 创建和初始化BeanFactory
  3. 创建和初始化ApplicationContext
  4. 创建和初始化单例Bean
    重要的是第五步,第六步,第十一步
    工厂后处理器添加bean定义到工厂
    bean后处理器加强bean生命周期各阶段

@Autowired失效的情况

当配置类中有@Bean注解时,会提前创建配置类。导致@Autowired注解失效。
在这里插入图片描述
在这里插入图片描述

六、linux

Linux查看进程

  1. ps aux
  2. ps -elf
  3. top
  4. ps -aup

七、docker

Docker解决组件依赖的兼容性问题

  • 将应用、系统函数库、依赖和配置打包,形成可移植镜像;镜像包含完整运行环境,仅依赖Linux内核
  • Docker应用运行在容器中,使用沙箱机制相互隔离
  • 启动和移除通过一行命令完成

Docker和虚拟机的差异

  • docker是系统进程,虚拟机是操作系统里的操作系统
  • docker体积小,启动速度快,性能好

Docker的架构

CS架构的程序,快速构建应用镜像、交付应用、运行应用的技术

  • 服务端:接收命令操作镜像和容器
  • 客户端:发送命令到服务端

Docker工作流

  • 构建自定义镜像或从镜像源拉取镜像
  • 根据镜像创建容器并运行

Docker数据卷的作用

将容器和数据分离,方便操作容器内数据

Docker如何自定义镜像

  • 在dockerfile中通过指令描述镜像的构建过程
  • 第一行必须从一个基础镜像来构建
  • 基础镜像可以是操作系统或其他人制作好的镜像

DockerCompose的作用

快速部署分布式应用,通过指令定义集群中每个容器的运行

八、并发相关

锁和阻塞

悲观锁:synchronized、ReentrantLock
乐观锁(无锁):CAS+自旋
阻塞队列
手写模拟实现一个数组阻塞队列:

public class AxinBlockQueue {
    //队列容器
    private List<Integer> container = new ArrayList<>();
    private volatile int size;
    private volatile int capacity;
    private Lock lock = new ReentrantLock();
    //Condition
    private final Condition notNull = lock.newCondition();
    private final Condition notFull = lock.newCondition();

    AxinBlockQueue(int cap) {
        this.capacity = cap;
    }

    /**
     * 添加方法
     *
     * @param data
     */
    public void add(int data) {
        try {
            lock.lock();
            try {
                while (size >= capacity) {
                    System.out.println("阻塞队列满了");
                    notFull.await();
                }
            } catch (InterruptedException e) {
                notFull.signal();
                e.printStackTrace();
            }
            ++size;
            container.add(data);
            notNull.signal();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 取出元素
     *
     * @return
     */
    public int take() {
        try {

            lock.lock();
            try {
                while (size == 0) {
                    System.out.println("阻塞队列空了");
                    notNull.await();
                }
            } catch (InterruptedException e) {
                notNull.signal();
                e.printStackTrace();
            }
            --size;
            int res = container.get(0);
            container.remove(0);
            notFull.signal();
            return res;
        } finally {
            lock.unlock();
        }
    }
}

锁的辨析(synchronized和Lock)

在这里插入图片描述

ReentrantLock

  1. 底层结构:
    Lock案例
    t1持有一把锁,t2、t3和t4在阻塞队列中,两个等待队列没有线程。
  2. 获取等待状态
  3. 公平锁
  4. 多条件变量

volatile能否保证线程安全

  • volatile不能保证原子性,可以保证可见性和有序性
  • volatile使用内存屏障保证有序性,volatile变量写操作置后,volatile变量读操作置前
    在这里插入图片描述

线程安全三个指标

  • 可见性
    • 其他线程能看到共享变量的修改结果。
    • 不可见现象的案例:
      • 问题:当循环多次读取共享变量时,JIT会优化热点代码,把共享变量赋值为常量,此时读取不到最新的值。
      • 方案:用volatile修饰共享变量,此时JIT不工作。
  • 有序性
    代码按编写顺序执行
  • 原子性
    多行代码以一个整体运行

悲观锁和乐观锁的区别

悲观锁

synchronized、Lock

  • 线程占有锁才能操作共享变量,每次仅有一个线程占锁成功,占锁失败的线程陷入等待。
  • 涉及线程上下文切换。

乐观锁

AtomicInteger、Unsafe,使用CAS保证原子性。

  • 无锁,每次只有一个线程成功修改共享变量,失败的线程不断重试直至成功。
  • 不涉及线程上下文切换。
  • 需要多核CPU支持,线程数不超过CPU核数。

sleep方法和wait方法的区别

共同点:使线程放弃对CPU的使用权,进入阻塞状态
不同点:方法归属、醒来时机、锁特性(wait执行前要加锁,wait执行后自动释放锁)

线程的状态和状态间的转换

Java的六种状态

  • NEW
  • RUNNABLE
    包含:就绪、运行、阻塞I/O
  • TERMINATED
  • BLOCKED
    加锁失败,从RUNNABLE转换为BLOCKED
  • WAITING
    调用wait/sleep方法,从RUNNABLE转换为WAITING
  • TIMED WAITING

操作系统的五种状态

  • 新建
  • 就绪
    可以分到CPU时间
  • 运行
    分到CPU时间
  • 阻塞
    分不到CPU时间
    包含:阻塞I/O、BLOCKED、WAITING、TIMED WAITING
  • 终结

线程池的核心参数

  • corePoolSize
    核心线程数
  • maximumPoolSize
    核心线程数+救急线程数
  • workQueue
  • keepAliveTime
    针对救急线程
  • unit
    针对救急线程
  • threadFactory
    线程命名
  • handler
    拒绝策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy

ThreadLocal的理解

  • 实现线程间的资源隔离
  • 实现线程内的资源共享
  • 每个线程有一个ThreadLocalMap来存储资源对象
  • ThreadLocal作为key,是弱引用
  • GC时,ThreadLocalMap的key释放后,value的释放时机
    • get key时
    • set key时,启发式扫描
    • 推荐用remove来释放,因为ThreadLocal一般作为静态变量,GC无法回收

九、JVM

在这里插入图片描述

1. 内存结构

2. 代码执行流程

在这里插入图片描述

3. 分析出现内存溢出的情况

理论分析:

  • OutOfMemoryError
    堆内存耗尽;
    方法区内存耗尽;
    虚拟机栈累积;
  • StackOverflowError
    虚拟机栈内部的方法调用次数过多;

具体示例:

  • 误用线程池
    堆内存溢出
  • 查询数据量太大
    堆内存溢出
  • 动态生成类
    元空间内存溢出

4. 方法区、永久代和元空间的辨析

方法区:JVM的概念
永久代:Hotspot对JVM的实现,1.8之前
元空间:Hotspot对JVM的实现,1.8之后。存放类元信息。

下面是程序运行期间元空间和堆的内存变化。
在这里插入图片描述
在这里插入图片描述

  • 元空间的内存释放比较苛刻,只有堆内存的对象、类和类加载器不再使用了,才能够释放元空间。
  • 系统类加载器和应用程序类加载器没有机会得到卸载。只有自定义类加载器可以。

5. 内存参数

按大小设置堆内存
  • 设置-Xms和-Xmx相等。
    不需要保留内存,不需要从小到大增长
  • 不设置新生代大小,由JVM自己控制。
    设置-Xmn,相当于设置-XX:NewSize和-XX:MaxNewSize相等
按比例设置堆内存

在这里插入图片描述

  • -XX:NewRatio是 old / new
  • -XX:SurvivorRatio是 eden / from
  • JVM默认 eden : from : to = 8 : 1 : 1
设置方法区内存

在这里插入图片描述

设置代码缓存内存

在这里插入图片描述
与即时编译器有关

设置线程内存
  • -Xss
  • 64位机器默认大小是1M

6. 垃圾回收

GC要点
  • 回收堆内存
  • 判断无用对象,用可达性分析和三色标记法
  • GC的实现是垃圾回收器
  • 用分代回收思想
    新生代采用标记复制法,老年代采用标记整理法
  • GC的规模分为:Minor GC、Mixed GC(G1回收器)、Full GC
垃圾回收算法
  • 标记清理(已废弃)
  • 标记整理
  • 标记复制
分代回收算法
  1. 最初对象分配到伊甸园
  2. 当伊甸园内存不足,标记伊甸园与from的存活对象
  3. 将存活对象采用标记复制算法复制到to,完毕后释放伊甸园和from内存
  4. 互换from和to的位置,继续第1步,当幸存区对象熬过15次回收,晋升到老年代
三色标记
  • 黑色 - 已标记
  • 灰色 - 标记中
  • 白色 - 未标记
并发漏标问题

描述:现代垃圾回收器,当垃圾回收线程运行时,用户线程也可以运行,这就会导致垃圾回收线程产生漏标问题。
方案:

  • 思路:记录标记过程中的变化,在并发标记后,再进行重新标记。
  • 实现:
    • 增量更新:记录被赋值的对象
    • 原始快照:记录新对象,记录被删除引用关系的对象
垃圾回收器
  • Parallel GC
    eden内存不足发生Minor GC,标记复制,STW
    old内存不足发生Full GC,标记整理,STW
    注重吞吐量
  • ConcurrentMarkSweep GC(已废弃)
    old并发标记,重新标记需要STW,并发清除
    Failback Full GC
    注重响应时间
  • G1 GC(JDK9的默认垃圾回收器)
    兼顾响应时间与吞吐量
    划分内存为多个区域,每个区域可充当eden,survivor,old,humongous
    新生代回收:eden内存不足,标记复制,STW
    并发标记:old并发标记,重新标记需要STW
    混合收集:并发标记完成,开始混合收集,参与复制的有eden,survivor,old,其中old选择回收价值高的区域,复制时STW
    Failback Full GC

7. 类加载

  • 加载
    将字节码载入方法区,并创建类的class对象
  • 链接
    验证:验证类是否符合Class规范,合法性和安全性检查
    准备:为static变量分配内存,设置默认值
    解析:将常量池的符号引用解析为直接引用
  • 初始化
    把静态代码块、static变量赋值、static final引用变量赋值合并成cinit方法,然后调用此方法
验证类加载和类初始化是按需的
  • 准备JDK16的运行环境
  • 使用jps查看进程号
  • 使用jhsdb来调试
    • 用Class Browser查看jvm加载的类
    • 用universe查看堆内存
    • 用g1regiondetails查看region
    • 用【scanoops 起始地址 结束地址 对象类型】查看对象地址
    • 用【inpect 地址】查看对象详情
  • 使用【javap -c -v -p】来查看字节码

8. 双亲委派模式

类加载器的工作方式,优先委派上级来加载,上级找不到才由下级加载。
在这里插入图片描述

优点
  • 使上级类加载器的类对下级共享,用户类能依赖到JDK的核心类
  • 使类加载有优先次序,保证核心类优先加载
能假冒一个java.lang.System类吗

不能。自己编写类加载器不能加载一个假冒java.lang.System类。

9. 对象引用类型的分类

强引用:变量赋值
软引用:反射数据
弱应用:ThreadLocalMap的Entry
虚引用:Cleaner释放直接内存,即外部内存

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值