java超详细面试题个人归纳

一、java基础

1.string 为什么是final类型?
  1) 为了实现字符串池
  2) 为了线程安全
  3) 为了实现String可以创建HashCode不可变性
2.HashMap源码,底层结构,实现原理?
  底层结构:java7由数组+链表构成(时间复杂度为O(n)),java8后新添红黑树(降低时间复杂度,O(logN))
  实现原理:由哈希表构成。
  拓展:非线程安全,使用synchronizedMap或者concurrentHashMap来实现多线程安全。
     concurrentHashMap由segment(继承reentrantLock加锁,即可重入锁)数组组成。(后续继续理解)
3.java集合类:List、Set、Map、Quene实现类
  List: (有序有重复)
    1) ArrayList 数组实现,适合随机查找和遍历,不适合插入和删除,线程不安全
     2) Vector 数组实现,线程安全,在数据量很大的情况下,数组扩容能力比ArrayList好
    3) LinkedList 链表实现,适合动态插入和删除
  Set:(无序无重复)
    1) HashSet 哈希表实现,取数按照hashcode值来取。优先比较hashCode值,再比较equals() 。(对于hashCode值一样的话,使用哈希桶存放相同的)
    2) TreeSet 二叉树实现,可实现排序,每增加一个对象会重新进行一次排序。Integer、String可以使用默认的TreeSet进行排序,自定义的类需要实现 1、实现Comparable接口 2、覆写相应的compareTo()方法。
  Map:
    1) hashTable 基于陈旧的Dictionary类, 线程安全。新项目已经抛弃使用,完全可以使用hashmap和concurrentHashMap替代。
    2) TreeMap 实现sortedMap接口,key默认按照升序排序
4.java反射中,class.forName与classLoader的区别
  反射,动态的加载类,动态的获取对象属性和方法,forName加载 .class到jvm ,对类进行解释,执行static代码块
  classLoader只做加载.class到jvm中,只有在newInstance时才会执行static代码块
5.java7、java8新特性
  1) Lambda表达式
    允许把函数作为一个方法的参数(闭包)
    语法形式为 () -> {},其中 () 用来描述参数列表,{} 用来描述方法体,-> 为 lambda运算符 ,读作(goes to)
     lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在lambda 内部修改定义在域外的局部变量,

    //多个参数有返回值
    ReturnMultiParam returnMultiParam = (int a, int b) -> {
        System.out.println("ReturnMultiParam param:" + "{" + a + "," + b +"}");
        return 1;
    };

  2) stream流 distinct方法去重,底层使用hashset实现去重,hashcode与equals方法
  3) 上面提到的,对hashmap的底层结构的修改(新添了红黑树)
  …

6.java内存泄露问题定位,了解jmap、jstack使用
7.String,Stringbuilder,Stringbuffer区别
  Stringbuffer线程安全,适合多线程使用,大量数据,使用了synchronized修饰

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

  Stringbuilder线程不安全,适合单线程使用,大量数据,StringBuffer,StringBuilder每次修改只会在同一处内存上修改,不会创建新的对象
  String字符串常量,每次操作都会重新开辟空间,浪费内存
8.java引用类型
  强引用:eg,Object obj = new Object(); 不会被内存回收,容易出现OOM问题
  软引用:有用,但不是必须的对象,内存不够才会去回收,适合保存缓存数据
  弱引用:弱引用关联的对象,只能存活到下一次的垃圾收集之前
  虚引用:跟踪垃圾回收过程,随时可被回收
9.hash冲突怎么办?
  hash冲突造成的原因是,通过生成函数生成的hashcode不够用了(数据太多了),导致不同的数据对应相同的hashcode。
  1) 开放地址法,有三种探测,归纳一句,在按照顺序决定值的时候,某hashcode重复,就按不同探测的规则加上(或减去)某个值,直到两个hashCode不一样。
  2) 链式地址法 (hashMap解决冲突的采取办法,基本结构就是数组+链表,相同的hashCode通过链表连接,存在一个数组元素里
  3) 再哈希法 重复的hashcode继续哈希处理
  4) 公共溢出区
10.如何理解深克隆与浅克隆呢?
  1)如何实现克隆?
    实现Cloneable接口
    覆盖clone()方法,访问修饰符设为public,默认是protected
    调用super.clone()
  2)浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
    深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

二、java IO

1.递归读取文件夹下的文件,代码怎么实现
  
2.String编码UTF-8与GBK的区别
  UTF-8包含所有国家字符,支持所有语言,英文一个字符一个字节(8位),中文一个字符3个字节
  GBK包含中文所有字符
3.什么时候使用字节流,什么时候使用字符流?
  InputStream和OutputStream为字节流,一般用来处理二进制数据或者字节类数据
  Writer和Reader为字符流,一般用来处理字符或者字符串数据

三、java多线程

1.java创建线程之后,直接调用start()和run()的区别
  直接调用thread.start()方法后,程序会新创建一条新的线程,执行run()方法里的逻辑
  直接调用run()方法的话,程序不会新建新线程,run()方法等待该线程其他任务结束才开始执行,意义不大。
2.常用的线程池模式以及不同线程池的使用场景
  四种常见的线程池
    1) newCachedThreadPool 创建可缓存线程池,数量超出,可以灵活回收(如果线程都处于sleep,一直在创建,造成堆内存溢出)
    2) newFixedThreadPool 创建定长线程池,数量超出则进入等待状态(底层原理是什么? 底层使用阻塞队列实现,默认是一个无界队列int.MAX_VALUE)
    3) newScheduledThreadPool 创建定长线程池,定时或周期性执行任务
    4) newSingleThreadExecutor 创建单线程化线程池,只用唯一的工作线程来执行任务,保证按照顺序执行
3.了解可重入锁的含义,synchronized与reentrantLock区别?
  可重入锁(也叫递归锁)允许线程可重复进入任意一个已经拥有它线程的代码块。
  synchronized锁:独占式的悲观锁,可重入锁。
    1) 作用于方法时,锁住的是对象的实例(this);
    2) 作用于静态方法是,锁住的是class实例,静态方法锁又相当于类的一个全局锁,会锁所有调用该方法的线程;
    3) 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块
  上述特点的详细解释
  reentrantLock继承接口Lock并实现了接口中定义的方法…
  reentrantLock是API级别的,synchronized是JVM级别的
4.同步的数据结构,例如concurrentHashMap的源码理解、内部实现原理、为什么是同步的而且效率那么高呢?
  concurrentHashMap和hashMap其实差不多,但是支持多线程。也是有数组+链表的基本结构组成,但是每一段数组都会由一个segment去管理,所以concurrentHashmap又是由一个segment数组组成,segment继承的是reentrantLock,是一个可重入锁,可重入锁是可以重复进入任意已拥有他锁的代码,不用每次加锁、解锁,效率比较高。
5.atomicinteger和volatile等线程安全操作的关键字的理解和使用
  atomicinteger:提供原子操作的Integer的类。那什么是原子操作呢?一旦开始,就会一直执行直到结束。讲白了,要么不执行,否则必定执行完。atomicBoolean、atomicLong等等实现原理类似。程序中的i++、++i等都是不安全的线程操作,atomicInteger简化了加synchronized的操作。
  volatile:
    1) 可见性:一个变量的修改,新值对于其他线程是立即可见的。普通变量是不可以的,普通变量在线程之间的传递都是要通过主内存进行的,线程1修改了变量A后,需要去主内存中回写,线程2等待线程1回写结束再去获取,这是一般过程。但并不能保证i++这种操作的原子性,本质上i++是读写两次操作。
    2) 禁止重排序:普通变量不能保证变量赋值操作的顺序与程序代码中的执行顺序一致…
6.线程间通信,wait与notify
  线程基本方法:
    1) 线程等待wait():调用该方法的线程进入waitting状态,只有等待另外线程的通知或者中断才会返回,需要注意的是,调用wait()后,会释放对象的锁,因此wait一般用在同步方法或者同步代码块中
    2) 线程睡眠sleep():导致当前进程休眠,与wait不同的是sleep不会释放当前占有的锁,进入timed-waitting状态
    3) 线程让步yield():使当前线程让出CPU执行时间片,让其他线程一起重新竞争CPU时间片。
    4) 线程中断interrupt():并不会中断一个正在运行的线程。只是改变了这个线程内部维护的中断标识位,再根据thread.isInterrupted()的值来终止线程
    5) 等待其他线程终止join():t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。(很多情况下,主线程生成并启动了子线程,需要子线程返回结果,即主线程需要需要在子线程结束后再结束,需要用到join方法)
    6) 线程唤醒notify(): 唤醒在此对象上等待的单个线程,notifyAll()是唤醒此对象等待的所有线程
7.实现多线程有几种方式?多线程同步怎么做?
  实现线程:1) 继承Thread类,重写run()方法;
    2) 实现Runnable接口,重写run()方法
    3) 通过Callable和FutureTask创建线程
    4) 通过线程池的创建
  多线程同步:synchronized…
8.在一个主线程中,要求有大量子线程执行完之后,主线程才执行完成?多种方式,考虑效率。
  1) 在主函数中使用join()方法
  2) CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,允许一个或多个线程进入等待
  3) 使用线程池
9.java线程池常用参数有哪些?
  corePoolSize:核心线程数(会一直存活,即使没有任务在执行)
  queueCapicity:任务队列容量(当核心线程数达到最大时,新任务会放在队列中排队等待执行)
  maxPoolSize:最大线程数(当线程数=maxPoolSize,且任务队列已满,线程池会抛出异常)
  keepAliveTime:线程空闲时间(空闲时间达到keepAliveTime时,线程会退出,直到线程数量等于corePoolSize)
  allowCoreThreadTimeout:允许核心线程超时(=true,线程数量会退为0)
  rejectedExecutionHandler:任务拒绝处理器(1.当线程数=maxPoolSize,且任务队列已满   2.线程池调用shutdown(),线程池会等任务都结束,会真正shutdown,在此期间,拒绝新任务)

四、数据库Mysql

1.Mysql存储引擎的不同
  以Innodb和MyISAM为例:
    1)Innodb支持事务,MyISAM不支持。
    2)innodb支持外键,而myisam不支持外键。
    3)InnoDB支持表、行(默认)级锁,而MyISAM支持表级锁(每次更新增加删除都会锁住表)。
    4)两者索引都是基于B+数实现的,但是innodb的B+数的叶子节点是存放所有数据的,而myisam的B+数的叶子节点是存放数据文件的指针的
    5)innodb不存储表的行数,所以select count( * )的时候会全表查询,而myisam会存放表的行数,select count(*)的时候会查的很快。
    …
2.单个索引、联合索引、主键索引的概念
  首先,索引是为了加快访问数据速度的。再来了解下面概念:
  1)主键索引,在创建主键的时候,系统就会自动为该主键创建索引
  2)单个索引,为某条字段加上索引,注意区分索引和主键的区别
  3)联合索引,多个字段组合,创建一个索引,支持最左原则。
3.索引的数据结构,B+树
  这篇博客写的很不错,可以借鉴一下啦,我就不细说了
4.Mysql怎么分表,分表后如何按条件分页查询?
  如何分表呢?
    1)数据库集群(缓解了sql压力,但是本质不是分表,一张表的数据量还是那么多,而且会增加成本)
    2)按照自定义规则区分表,根据某个字段去分,再转移数据(这样的话,可能会增加后台持久层开发量,毕竟一张表变多张表了)
    3)利用merge存储引擎来实现分表(第三种没有尝试过)
  分表后,如何分页查询?
    同步到离线数仓或者es…
5.数据库事务的几种粒度
事务隔离级别:
  未提交读:事务未提交的修改,对于其他事务是可见的。事务可以读取未提交的修改,这叫脏读,不建议使用这个隔离级别
  提交读:大多数数据库默认的这个级别(但Mysql不是)。一个事务从开始到提交之前,所做的一些修改对其他事务是不可见的。也叫不可重复读,因为两次查询的结果可能会不一致。
  可重复读:Mysql默认的事务隔离级别,解决了脏读的问题。但无法解决幻读的问题(幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,之前的事务再次读取该范围的记录时,会产生幻行)
  可串行化:最高的隔离级别。该级别会对读取的每一行都加锁,避免幻读,但是会造成锁争用和超时问题,实际很少用到。
6.分表之后想让一个id多个表是自增的,效率实现
  1)先去一个库一张表里创建一条无任何意义的记录,拿到这个id再去给业务数据使用,新增到分表当中
  2)利用redis去生成id,通过Redis的INCR/INCRBY自增原子操作命令,能保证生成的ID肯定是唯一有序的
  3)利用UUID、GUID去生成主键,但是太长了,可能会影响性能
  4)snowflake(雪花)算法,也是一种生成id方式。
7.数据库的锁:行锁、表锁、乐观锁、悲观锁
  表锁:某一用户进行读操作,允许其他用户读请求,阻塞其他用户对同一表的写请求。
     某一用户进行写操作,阻塞其他用户对同一表的读和写请求。
  行锁:当某一事务对一行进行更新,但是不提交的话,其他事务也要对该行进行更新,则需要等待。但其他事务更新别的行是不会收到影响的。
8.union与union all的区别是什么?
  Union:对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序;
  Union All:对两个结果集进行并集操作,包括重复行,不进行排序;
9.表和视图的区别有哪些?
  视图说白了,它只是一个 SELECT 的结果而已
  1)视图是虚拟的表,数据都来自于基础表;
  2)视图中存储的是sql逻辑,不存放具体的数据,如果基本表的数据发生变化,视图的展示结果也会发生变化。视图只站小部分的物理空间,表占大多数的物理空间;
  3)对视图的操作和表是一样的,对视图create、drop等,对视图的创建和删除只会影响视图本身,不会影响基础表;
  4)视图的使用:一般都是把基表的子查询sql语句 封装成视图,方便使用,而且更高效;
  5)若视图中的字段数据是来自于基表的话,一般是可以对视图中的数据 进行更新的,对视图数据进行添加、删除和修改操作会直接影响基本表。其他情况不允许更新,如:若视图中的字段使用了函数avg(age)等,就不能进行更新;
  有哪些不能更新的状态?
  在创建视图的时候,如果新加了一个with check option这个选项的话,会在更新操作的时候进行检查,是否可以更新。
  在构建视图的时候,出现如下情况,都不可以进行更新操作:

  • 聚合函数
  • DISTINCT关键字
  • GROUP BY子句
  • HAVING子句
  • UNION运算符
  • from子句中包含多个表
  • select语句中引用了不可更新操作的视图
  • 只要视图中的数据不是来源于基表,就不能去更新
    在这里插入图片描述

10.mysql中的in关键字后面支持多少数量?
  1000
11.如何进行sql优化?
  1)创建索引
   避免全表扫描,在经常需要进行检索的字段上创建索引,但并不是越多越好,尽量不要超过6个
  2)避免在索引上使用计算,例如:

//效率低:(salary是索引列)
select * from user where salary*22 > 11000 
//效率高:(salary是索引列)
select * from user where salary > 11000/22 

  3)尽量将多条SQL语句压缩到一句SQL中
   程序每次与数据库打交道都需要建立连接,非常耗时,依赖网络,尽量将结果通过子查询,关联查询等一次查出来
  4)在多表查询时,使用表的别名,也尽量避免歧义
  5)select查询避免使用*,不要返回用不到的任何字段
  6)in 和 not in 也要慎用,很多时候可以用exist代替
  7)尽量避免使用having,where可以在出结果前进行筛选,而having是在检索出结果集之后进行过滤
12.什么是聚簇索引?
  注意: innodb来说,
    1)主键索引 既存储索引值,又在叶子中存储行的数据
    2)如果没有主键, 则会Unique key做主键
    3)如果没有unique,则系统生成一个内部的rowid做主键.
    4)像innodb中,主键的索引结构中,既存储了主键值,又存储了行数据,这种结构称为”聚簇索引”

13.redis相关的一些问题(扩展,但redis不属于关系型数据库,nosql)
1)Redis支持的数据类型?
  string、set、list、hash、zset(Sorted Set)

2)什么是Redis持久化?Redis有哪几种持久化方式?
  持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失,说白了,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  两种持久化方式:
    RDB(默认,Redis DataBase):功能核心函数rdbSave(生成RDB文件)和rdbLoad(从文件加载内存)两个函数
  在这里插入图片描述

    AOF (Append-only file):可以分为命令追加、文件写入、文件同步三个步骤:
      命令追加: 当AOF持久化功能打开时,服务器在执行完一个写命令之后,会以协议格式(RESP)将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
      AOF文件的写入与同步: 每当服务器常规任务函数被执行、 或者事件处理器被执行时, aof.c/flushAppendOnlyFile 函数都会被调用, 这个函数执行以下两个工作:
      WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
      SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。

3)Redis分布式锁,它是怎么实现的?
  分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
  分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。
  情景:有这样一个情境,线程A和线程B都共享某个变量X。
如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。
如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。

  锁要满足的四个特点:

  1. 互斥性:在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人:加锁和解锁必须是同一个客户端,一个客户端不能把别人加的锁给解掉。

  正确的加锁方式:

public class RedisTool {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}
  • 第一个为key,我们使用key来当锁,因为key是唯一的。
  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。‘
  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

  正确的解锁方式:

public class RedisTool {
    private static final Long RELEASE_SUCCESS = 1L;
    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
 
    }
 
}

  第一行代码,我们写了一个简单的Lua脚本代码。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

  • 使用Lua语言来实现呢?因为要确保上述操作是原子性的。
  • 执行eval()方法可以确保原子性,在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

4)Redis是单线程的,但Redis为什么这么快?
  Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库。
   完全基于内存,绝大部分请求是纯粹的内存操作
   数据结构简单,对数据操作也简单
   单线程操作,避免了多线程切换、获取释放锁甚至死锁导致的问题
   使用多路I/O复用模型,非阻塞IO
   使用底层模型不同,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求

五、设计模式

1.单例模式:懒汉、饿汉。以及懒汉中的延迟加载,双重检查等。
单例模式其目的是保证一个类只有一个实例,并提供一个全局访问点
  懒汉:类加载时,判断对象是否存在,不存在才会去创建对象

//    懒汉式 判断对象是否存在再创建对象
    public class SingleTon {
        private static SingleTon instance;
        private SingleTon() {
        }
        public static SingleTon getInstance() {
            if (instance == null) {
                instance = new SingleTon();
            }
        return instance;
        }
    }

  上述代码有严重的线程安全问题,举个例子:
  多线程情况下,若是A线程调用getInstance,发现instance为null,那么它会开始创建实例,如果此时CPU发生时间片切换,线程B开始执行,调用getInstance,发现instance也为null(因为A并没有创建对象),然后B创建对象,然后切换到A,A因为已经检测过了,不会再检测了,A也会去创建对象,两个对象,单例失败。要采用双重检查策略:(参考博客

public class SingletonClass { 
  private volatile static SingletonClass instance = null; 
  
  public static SingletonClass getInstance() { 
    if (instance == null) { 
      synchronized (SingletonClass.class) { 
        if(instance == null) { 
          instance = new SingletonClass(); 
        } 
      } 
    } 
    return instance; 
  } 
  private SingletonClass() { 
  } 
}

  饿汉:类加载时,就会初始化该对象

public class SingleTon {
    // 静态实例变量,直接初始化
    private static SingleTon instance = new SingleTon();
    // 私有化构造函数
    private SingleTon() {
    }
    // 静态public方法,向整个应用提供单例获取方式
    public static SingleTon getInstance() {
        return instance;
    }
}

  延迟加载就是等到真真使用的时候才去创建实例,不用时不要去创建。
  缺点:1)无法进行懒加载,不能做到按需创建
  2)占用内存空间,应用中有大量单例对象的话,可能会占用较多的内存空间。
  3)可能导致启动缓慢

2.工厂模式、装饰者模式、观察者模式
  1)观察者模式: 定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
  举例:微信用户和公众号的关系。微信用户为观察者,公众号为具体
  抽象观察者(Observer):(定义一个方法,用于收到通知)

public interface Observer {
    public void update(String message);
}

  具体观察者(ConcrereObserver):(微信用户是观察者,实现更新方法)

public class WeixinUser implements Observer {
    private String name;// 微信用户名
    public WeixinUser(String name) {
        this.name = name;
    }
    @Override
    public void update(String message) {
        System.out.println(name + "-" + message);
    }
}

  抽象被观察者(Subject):(抽象主题,提供了attach、detach、notify三个方法)

public interface Subject {
    /**
     * 增加订阅者
     * @param observer
     */
    public void attach(Observer observer);
    /**
     * 删除订阅者
     * @param observer
     */
    public void detach(Observer observer);
    /**
     * 通知订阅者更新消息
     */
    public void notify(String message);
}

  具体被观察者(ConcreteSubject):(微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法)

public class SubscriptionSubject implements Subject {
    //储存订阅公众号的微信用户
    private List<Observer> weixinUserlist = new ArrayList<Observer>();
    @Override
    public void attach(Observer observer) {
        weixinUserlist.add(observer);
    }
    @Override
    public void detach(Observer observer) {
        weixinUserlist.remove(observer);
    }
    @Override
    public void notify(String message) {
        for (Observer observer : weixinUserlist) {
            observer.update(message);
        }
    }
}

  客户端调用:

public class Client {
    public static void main(String[] args) {
        SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();
        //创建微信用户
        WeixinUser user1=new WeixinUser("张三");
        WeixinUser user2=new WeixinUser("李四");
        //订阅公众号
        mSubscriptionSubject.attach(user1);
        mSubscriptionSubject.attach(user2);
        //公众号更新发出消息给订阅的微信用户
        mSubscriptionSubject.notify("王二麻子的专栏更新了");
    }
}

  2)装饰者模式:动态的将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。也就是说,在不使用继承和不改变原类文件的情况下,动态的扩展一个对象的功能。
  抽象组件:

public interface Person {
    void eat();
}

  具体组件:

public class Man implements Person {
    public void eat() {
        System.out.println("男人在吃");
    }
}

  抽象装饰者:

public abstract class Decorator implements Person {
    protected Person person;
    public void setPerson(Person person) {
        this.person = person;
    }
    public void eat() {
        person.eat();
    }
}

  具体装饰者:

//具体装饰者A
public class ManDecoratorA extends Decorator {
    public void eat() {
        super.eat();
        reEat();
        System.out.println("ManDecoratorA类");
    }
    public void reEat() {
        System.out.println("再吃一顿饭");
    }
}

//具体装饰B
public class ManDecoratorB extends Decorator {
    public void eat() {
        super.eat();
        System.out.println("===============");
        System.out.println("ManDecoratorB类");
    }
}

  测试类:

public class Test {
    public static void main(String[] args) {
        Man man = new Man();
        ManDecoratorA md1 = new ManDecoratorA();
        ManDecoratorB md2 = new ManDecoratorB(); 
        md1.setPerson(man);
        md2.setPerson(man);
        md2.eat();
    }
}

  3)工厂模式:用于封装和管理对象的创建。简单工厂模式、工厂方法模式、抽象工厂模式

六、Spring

1.@Autowired与@Resource的区别
  1)@Autowired可以对类成员变量、方法及构造函数进行标注,完成自动装配。来消除get、set方法。@Autowired是Spring提供的注解,只按照byTpye注入,如果想按照byName来装配的话,需要配合@Qualifier来使用

public class TestServiceImpl {
  @Autowired
  @Qualifier("userDao")
  private UserDao userDao; 
}

  2)@Resource默认按照byName装配,但不是Spring提供的注解,由J2EE提供,需要提供javax.annotation.Resource包。@Resource有两个属性:name,type。Spring将name解析为bean的名字,将type解析为bean的类型。注意@Resource的装备顺序
2.SpringBoot启动原理
  1)首先,SpringBoot的启动类是 *application,以注解@SpringBootApplication注明
  2)再来分析@SpringBootApplication这个注解。@Configuration,@EnableAutoConfiguration,@ComponentScan三个注解的集成,分别表示Springbean的配置bean,开启自动配置spring的上下文,组件扫描的路径。所以这个启动类要放在项目根目录,@ComponentScan才能扫描到整个项目。
  3)再来看启动类里的这个run()方法。深层扒取代码,会发现如下代码:

public static ConfigurableApplicationContext run(Class<?>[] primarySources,
			String[] args) {
		return new SpringApplication(primarySources).run(args);
	}

  由SpringApplication的实例化以及对象调用run()方法两部分组成,先看new SpringAppliaction():

	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		// 把SpringDemoApplication作为primarySources属性存储起来
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		// 从classpath中推断是否为web应用
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		// 获取启动加载器
		this.bootstrappers = new ArrayList<>(getSpringFactoriesInstances(Bootstrapper.class));
		// 设置初始化器(Initializer),最后会调用这些功能
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		// 设置监听器(Listener)
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		// 获取main方法所在的类
		this.mainApplicationClass = deduceMainApplicationClass();
	}

  基本就是做如下几件事情:

  • 配置primarySources
  • 配置环境是否为web环境
  • 创建初始化构造器setInitializers
  • 创建应用监听器
  • 配置应用主方法所在类(就是main方法所在类)

  接下来看run()方法中执行了什么任务:

	/**
	 * 运行spring应用程序,创建并刷新一个新的 {@link ApplicationContext}.
	 *
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 */
	public ConfigurableApplicationContext run(String... args) {
		// 计时工具
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		// 创建启动上下文对象
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();
		ConfigurableApplicationContext context = null;
		configureHeadlessProperty();
		// 第一步:获取并启动监听器
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 第二步:准备环境
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
			configureIgnoreBeanInfo(environment);
			// 第三步:打印banner,就是启动的时候在console的spring图案
			Banner printedBanner = printBanner(environment);
			// 第四步:创建spring容器
			context = createApplicationContext();
			context.setApplicationStartup(this.applicationStartup);
			// 第五步:spring容器前置处理
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
			// 第六步:刷新容器
			refreshContext(context);
			// 第七步:spring容器后置处理
			afterRefresh(context, applicationArguments);
			stopWatch.stop(); // 结束计时器并打印,这就是我们启动后console的显示的时间
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			// 发出启动结束事件
			listeners.started(context);
			// 执行runner的run方法
			callRunners(context, applicationArguments);
		} catch (Throwable ex) {
			// 异常处理,如果run过程发生异常
			handleRunFailure(context, ex, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		} catch (Throwable ex) {
			// 异常处理,如果run过程发生异常
			handleRunFailure(context, ex, null);
			throw new IllegalStateException(ex);
		}
		// 返回最终构建的容器对象
		return context;
	}

  基本做下面几件事:

  • 启动一个计时器,启动完成后会打印耗时
  • 获取并启动监听器 SpringApplicationRunListeners
  • 配置环境 ConfigurableEnvironment
  • Banner配置,就是控制台的那个spirng(那个很大的图案,也可以自定义一个图案)
  • 应用上下文模块(前置处理、刷新、后置处理) ConfigurableApplicationContext
  • 发出启动结束事件并结束计时

3 .拦截器与过滤器的区别是什么?
  1)过滤器(Filter)依赖severlet,基于函数回调实现。几乎可以过滤任何请求,使用过滤器的目的是为了,做一些过滤操作,获取想要的数据。对传入的request、response提前过滤掉一些信息,或者提前设置一些参数,在传入到controller层。eg,设置字符编码、过滤非法文字…Filter只能在容器初始化时调用一次。
  2)拦截器(Interceptor)依赖Web框架,基于Java反射机制实现属于AOP的一种应用。拦截器只能对action请求起作用,在对请求权限鉴定方面确实很有用处。可以拿到你请求的控制器和方法,却拿不到请求方法的参数。在action的生命周期中,拦截器可以多次被调用

七、中间件

1.RabbitMQ主要概念:
  1)Message:消息由消息头和消息体组成。消息体不透明,消息头是由一些可选项组成,比如routing-key(路由键)、delivery-code(指出该消息可能需要持久性存储)、priority(相对于其他消息的优先权)
  2)Publisher:消息生产者,向交换器发布消息
  3)Exchange:交换器,用于接受消息,并且将消息路由给服务器的消息队列
  4)Binding:交换器和消息队列的绑定。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以交换器可以看成由绑定构成的路由表
  5)Queue:保存消息,直到发送给消费者。消息的容器,也是消息的终点。一个消息可以投入一个或者多个队列。
  6)Connection:网络连接,比如一个tcp连接(三次握手,四次挥手的过程建议看看)
  7)Channel:信道,创建和销毁tcp的代价比较昂贵,所以多了信道这个概念,方便复用一个tcp连接。建立在真实tcp中的虚拟连接,所有AMQP命令都要经过信道,不管是发布还是接收消息。所以,是多路复用连接中的一条独立的双向数据流通道。
  8)Consumer:消息的消费者,从消息队列中获取到消息后消费。
  9)Virtual Host:虚拟主机,共享共同的身份认证和加密环境的独立服务器域
  10)Broker:消息队列服务器实体

  rabbitMq有四种类型的交换机fanout、direct、topic、headers。

  • direct键(routing-key):消息中的路由键(routing-key)如果和binding中的binding-key一致,交换器就将消息发送到对应的队列中。他是完全匹配、单播的模式。
  • fanout:很像子网广播。会将接受到的所有消息发给所有与之绑定的消息队列。fanout类型转发消息是最快的。
  • topic:说概念比较啰嗦:先上图叭(源于网络)
    在这里插入图片描述
    在这里插入图片描述

  routingKey于bindingKey匹配规则:
    routingKey必须是由点隔开的一系列的标识符组成。标识符可以是任何东西,但是一般都与消息的某些特性相关。
    *可以匹配一个标识符。
    #可以匹配0个或多个标识符。

  • headers:不依赖routingKey,使用发送消息时basicProperties对象中的headers来匹配的。headers是一个键值对类型,发送者发送消息时将这些键值对放到basicProperties对象中的headers字段中,队列绑定交换机时绑定一些键值对,当两者匹配时,队列就可以收到消息。匹配模式有两种,在队列绑定到交换机时用x-match来指定,all代表定义的多个键值对都要满足,而any则代码只要满足一个就可以了。fanout,direct,topic exchange的routingKey都需要要字符串形式的,而headers exchange则没有这个要求,因为键值对的值可以是任何类型。
    在这里插入图片描述

  如果消息丢失了该怎么办呢?

  1. 发送到rabbitmq的时候,可能在传输过程中因为网络等问题而将数据弄丢:
      1)mq开启事务(同步)。如果消息没有成功被rabbitmq接收到,那么生产者会受到异常报错,这时就可以回滚事物,然后尝试重新发送。
      2)开启confirm模式(异步)。每次写的消息都会分配一个唯一的id,写入了rabbitmq之中,rabbitmq会给你回传一个ack消息,如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。

  2. 没有开启rabbitmq的持久化,那么rabbitmq一旦重启,那么数据就丢了:
      将数据化持久化到磁盘,可以分为队列持久化和消息持久化:
      1)队列持久化是定义在队列的durable参数来实现的,durable为true时,队列才会持久化。
      //第二个参数设置为true,即durable = true
      channel.queueDeclare(“queuel”,true,false,false,null);
      持久化的队列在管理页面可以看到有个**“D”**的标识。
    在这里插入图片描述
      2)消息持久化通过消息属性deliveryMode来设置是否持久化,投递时指定delivery_mode=> 2(1是非持久化)

  3. 消费者消费时,还没有处理,结果消费者就挂了,rabbitmq就认为你已经消费过了,然后就丢了数据:
      关闭rabbitmq自动ack,可以通过一个api来调用就行,然后每次你自己代码里确保处理完的时候,再程序里ack一把。

八、JVM

1.jvm的概念?jvm是做什么的呢?
  jvm是可运行java代码的假想计算机。运行在操作系统之上的,与硬件没有直接交互。
  运行过程:
     1)java源文件 —> 编译器 —> 字节码文件
     2)字节码文件 —>JVM —>机器码
  jvm内存区域:(程序计数器、虚拟机栈、本地方法区、堆、方法区)

  • 程序计数器(线程私有):每条线程都要有一个独立的程序计数器,是当前线程所执行的字节码的行号指示器。这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError(OOM)情况的区域
  • 虚拟机栈(线程私有):描述java方法执行的内存模型。栈是线程私有的,每个线程都有自己的栈,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作栈数、动态链接、方法返回等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。百度了一个好理解的图片,简单了解 线程-栈-栈帧 的关系:

在这里插入图片描述

  • 本地方法区(线程私有):保存native方法进入区域的地址。
  • 堆(线程共享,运行时数据区):线程共享的一块区域,类的对象放在heap(堆)中,创建后在stack(栈)会创建类对象的引用(内存地址)。创建的对象和数组都保存在java堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。从GC角度可分为:新生代和老年代。
  • 方法区:用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。运行时常量池是方法区的一部分,该区域存放类和接口的常量。

2.简单理解一下几个GC算法的概念吧(很少被问到,大厂会问)
  1)引用计数法:
    在java中,引用和对象是有关联的。如果要操作对象必须用引用进行。
    所谓的引用计数法就是给每个对象一个引用计数器,每当有一个地方引用它时,计数器就会加1;当引用失效时,计数器的值就会减1。如果一个对象没有任何与之关联的引用,即他们的引用计数都为0,那么说明该对象不太可能会被用到,那么这个对象是可以被进行回收的。(Java没有用到,Python用到了)
  2)标记清除算法(mark-sweep)
    主要包含两个阶段:标记、清除
在这里插入图片描述
  显而易见,内存碎片化严重,如果后续对象过大,不好找可用空间。
  3)复制算法(copying)
    解决上述算法出现的缺点,将内存划分为相同大小的两块,每次只用其中的一块,当用到的一块内存满后,将仍存活的对象复制到另一块上,把这块内存清掉。
在这里插入图片描述
最大的问题就是可用内存被压缩到了原来的一半,而且存活对象增多的话,全部拷贝也会降低算法效率
  4)标记整理算法(mark-compact)
    结合上述两个算法的缺点,标记阶段和标记清除算法相同,但是标记后不是清理对象,而将存活对象移动到内存的一端。然后清除端边界外的对象。
在这里插入图片描述
  5)分代收集算法
    首先将GC堆划分为老生代和新生代。
    新生代的特点:每次回收垃圾时,都有大量垃圾需要被回收。
    老生代的特点:每次垃圾回收只有少量对象需要被回收。

    大部分新生代采用的是复制算法,因为回收的对象较多,要复制的操作就比较少。Eden/S0/S1默认空间比例Eden:S0:S1为8:1:1,有效内存(即可分配新生对象的内存)是总内存的90%。 在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值