[toc]

1.请你说说Java的特点和优点,为什么要选择Java

  • Java语言的特点是:一次编译,到处运行,即平台无关性;是纯粹的面向对象的语言。

  • JAVA语言的优点有:内置的类库简化了开发人员的设计工作;具有较好的安全性和健壮性;开发人员不需要对内存的使用进行管理。

  • 选择JAVA的原因是:使用范围很广,安卓操作系统的应用软件目前大部分还是使用JAVA语言编写。

2.请你说说Java基本数据类型和引用类型

Java的数据类型分为基本数据类型和引用数据类型两大类。 基本数据类型共有八大类,这八大数据类型又可分为四小类,分别是整数类型(byte/short/int/long)、浮点类型(float、double)、字符类型(char)和布尔类型(boolean)。其中,int是最常用的整数类型,double是最为常用的浮点类型,除了布尔类型之外的其他7个类型,都可以看做是数字类型,它们相互之间可以进行类型转换。 引用类型包括数组、类、接口类型,还有一种特殊的null类型,所谓引用数据类型就是对一个对象的引用,对象包括实例和数组两种。

8种基本数据类型的变量的值存放在栈内存,有char(16位),byte(8位),short(16位),int(32位),long(64位)float(32位),double(64位),boolean。 引用类型的变量存放引用地址,堆内存才是真正的值。类,接口,数组,string等为引用类型。

3.请你说一下抽象类和接口的区别

相同点:

  • 1、两者都不能实例化;
  • 2、可以拥有抽象方法。

区别:

  • 1、抽象类定义的关键字是abstract class,接口定义的关键字是interface;
  • 2、属性上,抽象类可以有静态变量、常量和成员变量,接口只能有常量;
  • 3、抽象方法可以有普通方法,而接口在 JDK1.8 之前只能有抽像方法(1.8之后,增加了静态方法和默认方法);
  • 4、抽象方法可以有构造方法,接口不可以有构造方法。
  • 5、一个类只能单继承一个父类,而一个接口可以继承多个父接口,同时,一个类可以实现多个接口却没有实现多个父类这一说法;
  • 6、抽象方法在业务编程上更像一个模板,有自己的功能,同时也可以有优化补充的多种形式,而接口更像是一种规范和要求,实现就要按照要求来进行。

4.请你说一下final关键字

final可以修饰类,方法,变量。 final修饰类,该类不可被继承。 final修饰方法,该方法不能被重写。 final修饰变量,如果是基本变量则值不能再改变,如果是引用变量则引用地址不能改变,但值可以改变。

5.说说static修饰符的用法

Java类中包含了成员变量、方法、构造器、初始化块和内部类(包括接口、枚举)5种成员,static关键字可以修饰除了构造器外的其他4种成员。static关键字修饰的成员被称为类成员。类成员属于整个类,不属于单个对象。 static关键字有一条非常重要的规则,即类成员不能访问实例成员,因为类成员属于类的,类成员的作用域比实例成员的作用域更大,很容易出现类成员初始化完成时,但实例成员还没被初始化,这时如果类成员访问实力成员就会引起大量错误。 加分回答 static修饰的部分会和类同时被加载。被static修饰的成员先于对象存在,因此,当一个类加载完毕,即使没有创建对象也可以去访问被static修饰的部分。 静态方法中没有this关键词,因为静态方法是和类同时被加载的,而this是随着对象的创建存在的。静态比对象优先存在。也就是说,静态可以访问静态,但静态不能访问非静态而非静态可以访问静态。

6.请你说说String类,以及new string和使用字符串直接量

String类被final修饰,所以不能被继承。创建String对象时可以使用字符串直接量,如String str=”1abc”, 另一种String str=new String(“1abc”),前者使用常量池来管理,后者先判断常量池中是否已存在此字符串,不存在就也在常量池创建,再在堆内存创建一个新的对象,因此后者开销更大。

7.String、StringBuffer、Stringbuilder有什么区别

StringBuilder和StringBuffer非常类似,均代表可变的字符序列,而且方法也一样

String:不可变字符序列,效率低,但是复用率高。

StringBuffer:可变字符序列、效率较高(增删)、线程安全

StringBuilder:可变字符序列、效率最高、线程不安全

8.请你说说\==与 equals()的区别

== 比较基本数据类型时,比较的是两个数值是否相等;比较引用类型是,比较的是对象的内存地址是否相等。 equals() 没有重写时,Object 默认以\==来实现,即比较两个对象的内存地址是否相等;重写以后,按照对象的内容进行比较

9.请你说说hashCode()和equals()的区别,为什么重写equals()就要重写hashcod()

得分点 hashCode()用途,equals()用途,hashCode()、equals()约定

  • 1、hashCode():获取哈希码,equals():比较两个对象是否相等。
  • 2、二者两个约定:如果两个对象相等,它们必须有相同的哈希码;若两个对象的哈希码相同,他们却不一定相等。也就是说,equals()比较两个对象相等时hashCode()一定相等,hashCode()相等的两个对象equqls()不一定相等。
  • 3、加分回答:由于hashCode()与equals()具有联动关系,equals()重写时,hashCode()进行重写,使得这两个方法始终满足相关的约定。

10.请你讲一下Java 8的新特性

得分点 Lambda表达式、Java8对接口的改进

  • 1、Lambda表达式:可将功能视为方法参数,或者将代码视为数据。使用 Lambda 表达式,可以更简洁地表示单方法接口(称为功能接口)的实例。
  • 2、方法引用:提供了非常有用的语法,可直接引用已有Java类或对象(实例)的方法或构造器。与Lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
  • 3、对接口进行了改进:允许在接口中定义默认方法,默认方法必须使用default修饰。
  • 4、Stream API:新添加的Stream API(java.util.stream)支持对元素流进行函数式操作。Stream API 集成在 Collections API 中,可以对集合进行批量操作,例如顺序或并行的 map-reduce 转换。
  • 5、Date Time API:加强对日期与时间的处理。

11.介绍一下包装类的自动拆装箱与自动装箱

得分点 包装类的作用,应用场景

  • 1、自动装箱、自动拆箱是JDK1.5提供的功能。
  • 2、自动装箱:把一个基本类型的数据直接赋值给对应的包装类型;
  • 3、自动拆箱是指把一个包装类型的对象直接赋值给对应的基本类型;
  • 4、通过自动装箱、自动拆箱功能,简化基本类型变量和包装类对象之间的转换过程

12.请你说说Java的异常处理机制

  • 1、异常处理机制让程序具有容错性和健壮性,程序运行出现状况时,系统会生成一个Exception对象来通知程序
  • 2、处理异常的语句由try、catch、finally三部分组成。try块用于包裹业务代码,catch块用于捕获并处理某个类型的异常,finally块则用于回收资源。
  • 3、如果业务代码发生异常,系统创建一个异常对象,并将其提交给JVM,由JVM寻找可以处理这个异常的catch块,并将异常对象交给这个catch块处理。如果JVM没有找到,运行环境终止,Java程序退出。
  • 4、Java也允许程序主动抛出异常。当业务代码中,判断某项错误的条件成立时,可以使用throw关键字向外抛出异常

13.说说你对面向对象的理解

  • 1、面向对象三大基本特征:封装、继承、多态。
  • 2、封装:将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,让外部程序通过该类提供的方法来实现对内部信息的操作和访问,提高了代码的可维护性;
  • 3、继承:实现代码复用的重要手段,通过extends实现类的继承,实现继承的类被称为子类,被继承的类称为父类;
  • 4、多态的实现离不开继承,在设计程序时,我们可以将参数的类型定义为父类型。在调用程序时根据实际情况,传入该父类型的某个子类型的实例,这样就实现了多态。

多态实现的三个条件:1.需要有继承关系的存在。 2. 需要有方法的重写。 3. 需要有父类的引用指向子类对象。

14.请你说说重载和重写的区别,构造方法能不能重写

重写和重载的区别:

  • 1.重载发生在同一类中,而重写发生在子类中。
  • 2.重载要求方法名相同,参数列表,返回值,访问修饰符都可以不同。重写要求方法名相同,参数列表相同,返回值类型要小于等于父类的方法,抛出的异常要小于等于父类方法抛出的异常,访问修饰符权限大于等于父类方法的访问修饰符权限。
  • 3.final,private修饰的方法不能重写,构造方法也不能重写。

构造方法不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名。如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与构造方法的要求是矛盾的。

15.请介绍一下访问修饰符

Java中的访问修饰符有四种,分别为private,default,protected,public。

  • private:类中被private修饰的只能在被当前类的内部访问
  • default:类中被default修饰的只能在当前类和当前类所在包的其他类访问
  • protected:类中被protected修饰的可以被当前类和当前类所在的包的其他类以及子类访问
  • public:类中被public修饰的能被当前项目下的所有类访问。

16.请你说说泛型、泛型擦除

  • 1.泛型:Java在jdk1.5引入了泛型,在没有泛型之前,每次从集合中读取的对象都必须进行类型转换,如果在插入对象时,类型出错,那么在运行时转换处理的阶段就出错;在提出泛型之后就可以明确的指定集合接受哪些对象类型,编译器就能知晓并且自动为插入的代码进行泛化,在编译阶段告知是否插入类型错误的对象,程序会变得更加安全清晰。
  • 2.泛型擦除:Java泛型是伪泛型,因为Java代码在编译阶段,所有的泛型信息会被擦除,Java的泛型基本上都是在编辑器这个层次上实现的,在生成的字节码文件中是不包含泛型信息的,使用泛型的时候加上的类型,在编译阶段会被擦除掉,这个过程称为泛型擦除。

17.请说说你对反射的了解

在程序运行期间动态的获取对象的属性和方法的功能叫做反射。

它能够在程序运行期间,对于任意一个类,都能知道它所有的方法和属性,对于任意一个对象,都能知道他的属性和方法。

获取Class对象的三种方式:getClass(); xx.class; Class.forName(“xxx”);

反射的优缺点:

  • 优点:运行期间能够动态的获取类,提高代码的灵活性。
  • 缺点:性能比直接的Java代码要慢很多。
  • 应用场景:spring的xml配置模式,以及动态代理模式都用到了反射。

18.请你说说多线程

进程是操作系统资源调度的基本单位,线程是处理器任务调度和执行的基本单位,一个进程可以创建多个线程,每个线程有自己独立的程序计数器,本地方法栈和虚拟机栈,线程之间共享进程的堆和方法区。线程之间是通过时间片算法来争夺CPU的执行权的。

  • 多线程的优点:当一个线程进入阻塞或者等待状态时,其他的线程可以获取CPU的执行权,提高了CPU的利用率。
  • 多线程的缺点:可能产生死锁;频繁的上下文切换可能会造成资源的浪费;在并发编程中如果因为资源的限制,多线程串行执行,可能速度会比单线程更慢。

死锁四个条件:

(1) 互斥条件:一个资源每次只能被一个进程使用。

(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

19.说说线程的创建方式

创建线程有三种方式,分别是继承Thread类,实现Runnable接口,实现Callable接口

  • 继承Thread类之后我们需要重写run()方法,方法中是我们希望这个线程执行什么操作,再创建对象的实例,通过实例对象的start())方法开启这个线程

  • 实现runnable接口之后,我们需要实现run()方法,方法中同样写我们需要执行的操作,然后将实现了接口的类作为参数创建一个Thread对象,通过这个对象的start方法开启线程

  • 实现Callable之后,需要实现call()方法,方法中写我们需要的操作,然后创建实现接口类的对象,将对象作为参数创建FurtureTask对象,再将task对象作为参数创建thread对象,调用start方法开启线程,还可以使用task对象的get方法获取返回值。他们的区别是前二者不能获取返回值,callable接口可以获得返回值,一般在实际使用中,更多使用实现接口的方式开启线程,因为接口不会占用类的继承位置

20.说说线程的状态

  • 1:新建态(NEW):当一个线程被创建成功后,但并没有执行它的start方时处于该状态
  • 2:就绪态(RUNNABLE):一个线程执行了start方法进入就绪态开始竞争cpu调度权但还没有竞争到以完成它的任务
  • 3:运行态:一个线程对象获取到了cpu的资源调度权,并进入允许态开始完成它的任务
  • 4:阻塞态(BLOCKED):若一个运行中的线程存在同步操作,此时锁被其他线程占用,该线程就会进入阻塞态等待获取锁
  • 5:限期等待(TIMED WAITING):正在运行的线程执行了Thread.spleep()方法或者设置了timeout的wait()方法,join方法等进入一定时间的等待,系统自动唤醒。
  • 6:不限期等待(WAITING):正在运行的线程执行了未设置timeout的wait方法或join方法进入等待,只有通过其他线程使用interrupt()或notify方法对其进行唤醒。
  • 7:死亡态(TEMINATED):线程成功执行完毕或执行中抛出异常中断了线程会进入死亡态。

21.说说wait()和sleep()的区别

  • 1.所属的类型不同 - wait()是Object类的实例方法,调用该方法的线程将进入WAITING状态。 - sleep()是Thread类的静态方法,调用该方法的线程将进入TIMED_WTING状态。
  • 2.对锁的依赖不同 - wait()依赖于synchronized锁,通过监视器进行调用,调用后线程会释放锁。 - sleep()不依赖于任何锁,所以在调用后它也不会释放锁
  • 3.返回的条件不同 - 调用wait()进入等待状态的线程,需要由notify()/notifyAll()唤醒,从而返回。 - 调用sleep()进入超时等待的线程,需要在超时时间到达后自动返回。

22.说说怎么保证线程安全

线程的安全主要是原子性、可见性、有序性三个问题。

  • 1、可见性:可见性问题是由处理器核心的缓存导致的,每个核心均有各自的缓存,而这些缓存均要与内存进行同步,volatile在多处理器开发中保证了共享变量的“可见性”
  • 2、原子性:一个或者多个操作在CPU执行的过程中不被中断的特性。线程切换可能造成原子性问题。synchronized关键字可以解决原子性、有序性和可见性问题,同时lock和Atomic开头的类也可以解决原子性问题
  • 3、有序性:程序执行的顺序按照代码的顺序执行,编译优化可能造成这个问题。volatile关键字通过禁止指令重排来解决有序性问题

23.说说你了解的线程同步方式

java主要通过加锁的方式实现线程同步:主要有两种方式:synchronized关键字和lock接口。

  • synchronized可以加在三个不同的位置,对应着三种不同范围,区别是锁对象的不同:
    • 1.加在实例方法上,锁的就是当前的实例。
    • 2.加在静态方法和类上,锁的就是当前整个类。
    • 3.加在代码块上,锁的就是代码块里面的内容。我们应该合理的选择锁的对象。
  • lock锁接口除了支持上述功能外,还支持了响应中断,超时机制,阻塞队列等。

24.说说你了解的线程通信方式

>

25.说说Java中常用的锁及原理

synchronized关键字和lock锁接口:

  • synchronized关键字底层采用java对象头来存储锁信息的。
  • lock锁接口是基于AQS实现的。AQS内部定义一个先进先出的队列实现锁的同步,同时还定义了同步状态来记录锁信息。

26.synchronized和Lock有什么区别

  • synchronized是同步锁,可以修饰静态方法、普通方法和代码块。修饰静态方法时锁住的是类对象,修饰普通方法时锁住的是实例对象。当一个线程获取锁时,其他线程想要访问当前资源只能等当前线程释放锁。
  • synchronized是java的关键字,Lock是一个接口。
  • synchronized可以作用在代码块和方法上,Lock只能用在代码里。
  • synchronized在代码执行完或出现异常时会自动释放锁,Lock不会自动释放,需要在finally中释放。
  • synchronized会导致线程拿不到锁一直等待,Lock可以设置获取锁失败的超时时间。
  • synchronized无法获知是否获取锁成功,Lock则可以通过tryLock判断是否加锁成功。

27.说说synchronize的用法及原理

synchronized可以修饰静态方法、普通方法、代码块。 能够保证同一个时刻只有一个线程执行该段代码,保证线程安全。 在执行完或者出现异常时自动释放锁。 synchronized作用在代码块时,它的底层是通过monitorenter、monitorexit指令来实现的。

28.说说你对AQS的理解

1、AQS队列同步器,用来构建锁的基础框架,Lock实现类都是基于AQS实现的。

2、AQS是基于模板方法模式进行设计的,所以锁的实现需要继承AQS并重写它指定的方法。

3、AQS内部定义了一个FIFO的队列来实现线程的同步,同时还定义了同步状态来记录锁的信息。

4、AQS的模板方法,将管理同步状态的逻辑提炼出来形成标准流程,这些方法主要包括:独占式获取同步状态、独占式释放同步状态、共享式获取同步状态、共享式释放同步状态

29.说说你对线程池的理解

ThreadPoolExecutor构造器:
public ThreadPoolExecutor(int corePoolSize
int maximumPoolSize,
long keepAliveTIme,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数一:指定线程池的线程数量(核心线程):corePoolsize ——-> 不能小于0
参数二:指定线程池可支持的最大线程数:maximumPoolSize ———> 最大数量>=核心线程数量
参数三:指定临时线程的最大存活时间:keepAliveTime ———> 不能小于0
参数四:存活时间的单位(秒,分,时,天):unit ———> 时间单位
参数五:指定任务队列:workQueue ———> 不能为null
参数六:指定用那个线程工厂创建线程:threadFactory ———> 不能为null
参数七:指定线程忙,任务忙的时候,新任务来了怎么办,即拒绝策略:handler ———> 不能为null

(重要!!!)注意两点:
临时线程什么时候创建:新任务提交时发现核心线程都在忙,任务队列满,还可以创建临时线程时才会创建。
什么时候开始拒绝任务:核心线程和临时线程都在忙,任务队列都满,新任务过来会拒绝。

拒绝策略:

策略 详解
ThreadPoolExecutor.AbortPolicy 丢弃任务抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy 丢弃任务不抛出异常(不推荐)
ThreadPoolExecutor.DiscardOldestPolicy 抛弃等待最久的任务并把当前任务加入队列
ThreadPoolExecutor.CallerRunsPolicy 由主线程负责调用的run()方法绕过线程池直接执行,即来新任务主线程亲自服务

30.说说volatile的用法及原理

1.修饰被不同线程访问和修改的变量

2.被其修饰的变量,系统每次用到它时都是直接从对应的内存中提取,而不会利用缓存(如寄存器),所有线程在任何时候所看到变量的值都是相同的

3.volatile不能保证操作的原子性,因此不能代替synchronized

4.能不使用就尽量不要使用

*31.说说你对ThreadLocal的理解

ThreadLocal即线程变量,它用于共享变量在多线程中的隔绝,即每个线程都有一个该变量的副本彼此互不影响也就不需要同步机制了,

实现原理:每个Thread对象中都有一个ThreadLocal类的内部类ThreadLocalMap对象,他是一个键值形式的容器,以ThreadLocal对象的get和set方法来存取共享变量值,原理时:以ThreadLocal对象作为key来存取共享变量值。一个ThreadLocal用完后必须remove,否则会造成内存泄漏。

*32.请你说说JUC

JUC是java.util.concurrent的缩写,这个包中包含了支持并发操作的各种工具。

1.原子类:遵循比较和替换原则。可以用于解决单个变量的线程安全问题。

2.锁:与Synchronized类似,在包含synchronized所有功能的基础上,还支持超时机制,响应中断机制,主要用于解决多个变量的线程安全问题。

3.线程池:可以更方便的管理线程,同时避免重复开线程和杀线程带来的消耗,效率高。

4.并发容器:例如ConcurrentHashMap,支持多线程操作的并发集合,效率更快。

?33.Java哪些地方使用了CAS

1、CAS 比较并交换,比较典型的使用场景有原子类、AQS、并发容器。

2、AQS:在向同步队列的尾部追加节点时,它首先会以CAS的方式尝试一次,如果失败则进入自旋状态,并反复以CAS的方式进行尝试。

3、并发容器:以ConcurrentHashMap为例,它的内部多次使用了CAS操作。在初始化数组时,以CAS的方式修改初始化状态,避免多个线程同时进行初始化。在执行put方法初始化头节点时,它会以CAS的方式将初始化好的头节点设置到指定槽的首位,避免多个线程同时设置头节点。

34.请说说你对Java集合的了解

java中的集合类主要都有Collection和Map这两个接口派生而出,其中Collection又派生出List,Set,Queue。所有的集合类都是List,set,queue,map这四个接口的实现类。其中,list代表有序的,可重复的数据集合;set代表无序的,不可重复的数据集合,queue代表先进先出的队列;map是具有映射关系的集合。最常用的实现类又ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,ArrayQueue。

35.你知道哪些线程安全的集合?

java.util包中的集合类大部分都是非线程安全的,例如:ArrayList/LinkedList/HashMap等等,但也有少部分是线程安全的,像是Vector和Hashtable,它们属于很古老的API了,是基于Synchronized实现的,性能很差,在实际的开发中不常用。一般可以使用collections工具类中的syncheronizedXxx()方法将非线程安全的集合包装成线程安全的类。在java5之后可以使用concurrent包提供的大量的支持并发访问的集合类,例如ConcurrentHashMap/CopyOnWriteArrayList等

36.请你说说HashMap底层原理

在1.8之前,HashMap的底层是数组加链表,在1.8之后是数组+链表+红黑树; 它的put流程是:基于哈希算法来确定元素位置,当我们向集合存入数据时,他会计算传入的key的哈希值,并利用哈希值取绝对值再根据集合长度取余来确定元素的位置,如果这个位置已经存在其他元素了,就会发生哈希碰撞,则hashmap就会通过链表将这些元素组织起来,如果链表的长度达到8时,就会转化为红黑树,从而提高查询速度。 扩容机制:HashMap中数组的默认初始容量为16,当达到默认负载因子0.75时,会以2的指数倍进行扩容。 Hashmap时非线程安全的,在多线程环境下回产生循环死链,因此在多线程环境下建议使用ConcurrentHashMap。

37.请你说说HashMap和Hashtable的区别

  • 1.Hashtable在实现Map接口时保证了线程安全性,而HashMap则是非线程安全的。所以Hashtable的性能不如HashMap,因为为了保证线程它牺牲了一些性能。

  • 2.Hashtable不允许存入null,无论是以null作为key或value,都会引发异常但,HashMap是允许的。Hashtable是很古老的API,性能不好,不推荐使用,要在多线程下使用ConcurrrntHashMap,它不但保证了线程安全,也通过降低锁的粒度提高了并发访问时的性能

38.HashMap是线程安全的吗?如果不是该如何解决

HashMap不是线程安全的,在添加数据的时候,会根据key和value计算出在底层数组中的位置,然后封装成entrey对象插入,但由于HashMap并没有做对应的线程安全处理,所以如果恰好两个线程同时操作的话,就有点会将其中一个数据覆盖掉,这不符合要求。 那么解决方法就是说,你可以在操作这个HashMap的时候手动的上锁,可以通过Lock也可以通过synchronized关键字。当然我更推荐直接使用java已经提供了的ConcurrentHashMap,其内部使用了Lock来解决线程安全问题,并且底层结构也进行了一些变动,上锁的时候只会锁对应下标的元素,不会对其他位置造成影响,即保证了线程安全,又保证了性能。

*39.请你说说ConcurrentHashMap

ConcurrentHashMap是一个线程安全的集合,它的底层是数组+链表/红黑树构成的。 在1.7的时候采用segment数组+hashEntry的方式实现的,lock加在Segment的上面,在size计算的时候,首先是不加锁的,最多计算三次,前后两次的结果是相同的话那么结果就是准确的,如果不一样的话,那么就加锁,重新计算。 在1.8的时候废弃了这种算法,采用Synchronized+CAS+Node来保证并发安全的进行,使用一个volatile类型的变量baseCount来记录元素的个数,集合每次增加或者删除的时候,basecount就会通过addCount()来对baseCunt产生相应的变化,最后得到元素的个数。 初始值为16,每次扩容都是之前的二倍,不支持null值和null为key

40.说说你对ArrayList的理解

arraylist在jdk7.0的时候,创建容器的时候会在底层创建一个长度为10的object数组,在jdk8.0的时候,在创建容器的时候底层并不会立刻创建,只有在第一次调用add方法的时候才会创建一个长度为10的数组,默认情况下,扩容为原来容量的1.5倍,同时将原有数组中的值复制到新的数组中,并且arraylist属于有序的,可重复的集合,提供了iterator方法,增强了迭代能力。

41.请你说说ArrayList和LinkedList的区别

  • 1.ArrayList的实现是基于数组,LinkedList的实现是基于双向链表。
  • 2.对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问,而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是O(N)。
  • 3.对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引
  • 4.LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

42.请你说说List与Set的区别

list和set都是接口collection的子接口,list代表有序的可重复的集合,每个元素都有对应的顺序索引,可以通过索引来访问指定位置的集合元素。而set表示无序,不可重复的集合元素。但是它有支持排序的实现类treeset,treeset可以确保元素处于排序状态,并支持自然排序和定制排序两种方式,treeset是非线程安全的,内部元素的值不能为null

?43.请你说说BIO、NIO、AIO

BIO是阻塞IO,当用户线程发送请求后会一直阻塞知道内核将数据准备;NIO是非阻塞IO,用户线程发送请求后,可以做其他工作,并不断询问内核数据,但在数据复制阶段,用户线程依然属于阻塞状态。BIO和NIO都属于同步IO。AIO是异步IO,当用户线程发送请求后,内核会返回一个回调函数,但该回调函数不包含数据,之后用户线程可以去处理其他操作,当数据准备好后,内核会将数据发送给用户线程,而不必像同步IO中用户线程自己去读取。BIO只能处理一个请求,NIO可以处理多个请求。IO多路复用在NIO的基础上加入了事件机制,将用户请求注册到多路复用器上,然后监视是否有IO事件发生,如果有,会通知用户线程,IO多路复用的方式主要有select、poll、epoll。

?44.请你说说IO多路复用

IO多路复用指的是单个线程能够同时完成对多个IO事件的监听处理。linux提供了select、poll和epoll三种多路复用方式。本质上是利用内核缓存fd描述文件,并内核完成对文件描述符的监听操作。selec是将所用文件描述符的集合从用户态拷贝到内核空间,底层采用的是数组。poll和select相似,主要区别是poll底层使用的是链表,所以其能够监听的文件描述符不受限制。但是这两种方法都需要多次的内核与用户空间的复制拷贝,并且用户空间还需要在O(N)的时间复杂度下对描述符进行遍历才具体知道哪一个文件描述符发生了事件。epoll在内核开辟空间底层采用红黑树,用户可以直接在内核创建需要需要关注的文件描述的节点,当事件发送内核将对应文件描述符直接存入队列并将其返回到用户空间。epoll这种方式可以减少每次调用时从用户空间复制到内核的操作,并且因为内核返回的发送事件描述符的队列,可以减少每次轮询的操作,使得在O(1)的时间复杂度就能找到发送事件的描述符。

?45.请你讲一下Java NIO

NIO弥补了原来同步阻塞IO的不足,它在标准Java代码中提供了高速的、面向块的IO。通过定义包含数据的类,以及通过以块的形式处理这些数据。NIO包含三个核心的组件:Buffer(缓冲区)、Channel(通道)、Selector(多路复用器)。Buffer是一个对象,它包含一些写入或者要读出的数据。在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中,任何时候访问NIO中的数据,都是通过缓冲区进行操作。Channel是一个通道,可以通过它的读取和写入数据,它就像自来水管一样,网络数据通过Channel读取和写入。通道和流的不同之处在于通道是双向的,流只是在一个方向上移动而且通道可以用于读、写或者同时读写。Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有心的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的IO操作

46.简单说下你对JVM的了解

JVM是Java语言跨平台的关键,Java在虚拟机层面隐藏了底层技术的复杂性以及机器与操作系统的差异性。运行程序的物理机千差万别,而JVM则在千差万别的物理机上面简历了统一的运行平台,实现了在任意一台JVM上编译的程序,都能在任何其他JVM正常运行。 JVM由三部分组成:类加载子系统、执行引擎和执行时数据区。

  • 1)类加载子系统:可以根据指定的全限定名来载入类或接口。
  • 2)执行引擎:负责执行那些包含在被载入类的方法中的指令。
  • 3)当程序运行时,JVM需要内存来存储许多内容,例如:字节码、对象、参数、返回值、局部变量、运算的中间结果等,JVM会把这些东西都存储到运行时数据区中,以便于管理。而运行时数据区又可以分为方法区、堆、虚拟机栈、本地方法栈、程序计数器。

47.说说你了解的JVM内存模型

JVM由三部分组成:类加载子系统、执行引擎、运行时数据区 1、类加载子系统:可以根据指定的全限定名来载入类或接口。 2、执行引擎:负责执行那些包含在被载入类的方法中的指令。 3、运行时数据区:分为方法区、堆、虚拟机栈、本地方法栈、程序计数器。当程序运行时,JVM需要内存来存储许多内容,例如:字节码、对象、参数、返回值、局部变量、运算的中间结果等,把这些东西都存储到运行时数据区中,以便于管理。

48.说说Java运行时数据区

Java运行时数据区由五部分组成:程序计数器、Java栈、本地方法栈、Java堆和方法区。程序计数器控制着程序下一步的执行,如循环、分值判断等,Java栈保存着方法运行时的数据结构,方法的调用和结束对应着一个帧栈的入栈和出栈,本地方法栈与Java栈作用类似,其作用对象是本地方法,Java堆中存储new的对象,也是垃圾回收的重要管理区域,方法区中保存着全局变量、静态变量等。方法区和Java堆是线程共享的,而程序计数器和Java栈是线程私有的。

49.说说JVM的垃圾回收机制

JVM的垃圾回收机制是遵循分代收集理论进行设计的,主要分为四种收集方式:

  • 1.新生代收集,目标为新生代的垃圾收集。
  • 2.老年代收集:目标为老年代的垃圾收集,目前只有CMS收集器会有这种行为。
  • 3.混合收集:目标为整个新生代及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。
  • 4.整堆收集:目标为整个方法区和堆的垃圾收集。
  • 常见的垃圾回收算法包括:标记清除算法:缺点:内存碎片化,优点:速度快。
  • 标记复制算法:缺点占用内存大,优点:内存连续。
  • 标记整理算法:优点:内存连续,缺点:整理效率低。

50.说说JVM的垃圾回收算法

  • 1.引用计数法,每次赋值时均要维护引用计数器且计数器本身也有一定的消耗,较难处理循环引用,一般不采用这种方式
  • 2.复制算法,将内存分为两块,每次只使用其中一块,当这块内存用完,就将还活着的对象复制到另外一块上面,效率高且没有碎片,但是需要双倍的空间,年轻代中使用复制算法
  • 3.标记-清除,先标记要清除的对象,然后统一回收这些对象,不需要额外的空间,但是需要两次扫描耗时严重并且会产生内存碎片
  • 4.标记-整理,标记存活对象,然后将标记的存活对象按内存地址依次排序,清除边界外未标记的对象,没有内存碎片,但是需要移动对象。老年代一般用标记-清除和标记-整理的混合实现

51.说说GC的可达性分析

可达性分析算法用于判断对象是否可以被回收,程序通过GC Roots中的对象为起点,以类之间的引用关系简历引用链,最终形成一个类似于数据结构中森林的一个结果,不存在与森林中的对象便是需要被回收的对象。这里的GC Roots主要包括线程栈中引用的变量,本地方法栈中引用的变量,方法区中的静态引用对象,常量池中的常量引用对象和被锁引用的对象。对一个对象真正的宣告回收需要经历两次标记过程,如果一个对象不再引用链上就会对它进行第一次标记,并判断它是否重新了finalize方法,若未重新或finalize方法已经被执行过了则会直接回收对象,否则会创建一个F-queue队列来存储这些对象,并启动一个低优先级的Finalizer线程去执行它们的finalize方法。第二次标记,稍后收集器会对队列中的对象进行可达性分析并标记,若仍然存在标记则表明该对象没有通过finalize方法实现自救则直接回收,否则对象复活。任何对象的finalize方法都只能被调用一次。

52.请你说说Java的四种引用方式

java中的四种引用方式分别是:1,强引用,以new关键字创建的引用都是强引用,被强引用引用的对象永远都不会被回收。2,软引用:以SoftRererenc引用对象,被弱引用引用的对象只有在内存空间不足时会被垃圾回收。3,弱引用,以WeakReference引用对象,被弱引用引用的对象一定会被回收,它只能存活到下一次垃圾回收。4,虚引用:以PhantomReference引用对象,一个对象被引用引用后不会有任何影响,也无法通过该引用来获取该对象,只是其再被垃圾回收时会收到一个系统通知。

53.请你讲下CMS垃圾回收器

CMS垃圾收集器采用标记清除算法,使用多线程实现,所以它的应用场景一般为服务端系统的老年代。它是一个以达到在垃圾回收期间用户线程低停顿为目标的垃圾收集器。CMS垃圾收集器垃圾回收分为四个阶段:1,初始标记:只对与GCRoots有直接关键的对象进行可达性分析的标记。2,并发标记:标记整个GCRoots引用链中的对象,与用户线程并发执行。3,重新标记:用于更新在并发标记过程中被复活的对象。4,并发清除:清除标记阶段判断的已死亡的对象。该流程与用户线程并发执行。缺点:1,它使用标记清除算*导致内存碎片化,2,会产生在并发清除阶段的浮动垃圾,只有到下一次垃圾回收时才会被清除。

54.请你讲下G1垃圾回收器

G1回收器是一个多线程,可以同时收集新生代和老年代的一个垃圾收集器。G1将整个堆内存区域划分为多个大小相等的region,追踪每个Region可以回收对象的大小和预估时间,并以region为单位进行垃圾收集并获取每个region的收集效率和收集收益,通过一张优先级表对其进行维护。 G1可以设置垃圾回收的预期停顿时间(STW)。 G1的年轻代和老年代空间并不是固定的,当现有年轻代分区占满时,JVM会分配新的空闲Region加入到年轻代空间,老年代也是如此。 G1 GC的回收过程中有内存整理,理论上不会产生内存碎片! G1垃圾收集器垃圾回收主要包括四个流程:1,初始标记 2,并发标记(类似于CMS) 3,最终标记 4,并发筛选回收:根据优先级表选择分区进行垃圾回收,用户线程不停顿。

55.说说类加载机制

类加载的过程中首先判断这个是否被加载过,如果没有被加载过,那么调用类加载器进行加载,判读这个类是否符合规范,如果不符合就抛出异常,加载成功就会生成class对象。 接下来是链接过程,分为三步:准备,验证,准备,解析。 验证:确保文件符合规范,不会危害虚拟机自身的安全,对文件格式,字节码,元数据,符号引用进行验证。 准备:为类变量分配初始空间以及默认初始值,即零值。这里不会为实例变量分配,类变量分配在方法区中,实例变量跟随对象分配在堆中,final修饰的在编译期间就分配了,在准备阶段会显式的初始化。 解析:将常量池内的直接引用转为直接引用的过程。 链接过程完成之后开始初始化的过程: 初始化阶段就是执行类构造器方法的过程。此方法不需要定义,一个类只会被加载一次,虚拟机必须保证在多线程条件下类的构造方法是被加锁的。

56.说说JVM的双亲委派模型

双亲委派模型的工作过程是,如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载

57.说说类的实例化过程

类实例化的过程。类加载,分配内存,初始化零值,状态设置,构造函数。

58.请你说说内存溢出

内存溢出:指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。引起内存溢出的原因:1.内存加载的数据量过于庞大。如一次从数据库取出的过多的数据。2.代码中存在死循环或者死循环中产生大量的对象实体。3.启动内存值设定过小。解决内存溢出的方案:修改JVM启动参数,直接增加内存。2.检查错误日志,查看“OutOfMemory”错误之前是否存在异常。3.对代码进行debug分析。4.使用内存工具动态查看内存使用情况。常见的内存溢出出现在:1.堆。对象创建过多2.栈溢出3.方法区和运行时常量池;创建大量动态类。

59.请你说说内存泄漏

内存泄露:指不再使用的对象仍然被引用,导致垃圾收集器无法回收它们的内存,最终导致OOM。

60.请你说说进程间的通信方式

1.管道:管道本质是内核中维护的一块内存缓冲区。2.命名管道:因为无名管道只适用于具有亲缘关系的线程,所以就衍生出来命名管道,这样就可以实现了非亲缘关系进程之间的通信。3.信号:一种通知机制。4.消息队列:是一个消息链表,既可以读消息,也可以写消息。5.共享内存:多个线程共享一片内存区域。6.内存映射:就是将磁盘文件数据映射到内存,通过修改内存就能修改磁盘文件。7socket接口,socket接口一般用于不同主机上进程之间的通信

61.请你说说线程和协程的区别

协程与线程的区别: 1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程。 2) 线程进程都是同步机制,而协程则是异步。 3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。 4)线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。 5)协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。 6)线程是协程的资源。协程通过Interceptor来间接使用线程这个资源。

62.请你说说线程和协程的区别

  1. 线程是操作系统的资源,线程的创建、切换、停止等都非常消耗资源,而创建协程不需要调用操作系统的功能,编程语言自身就能完成,所以协程也被称为用户态线程,协程比线程轻量很多;

  2. 线程在多核环境下是能做到真正意义上的并行,而协程是为并发而产生的;

  3. 一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行;

  4. 线程进程都是同步机制,而协程则是异步;

  5. 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力;

  6. 操作系统对于线程开辟数量限制在千的级别,而协程可以达到上万的级别。

63.请你说说死锁定义及发生的条件

死锁:两个或者两个以上的线程互相争夺对方的资源而不释放自己的资源,从而导致死锁的产生。 死锁产生的条件: 互斥:一个资源在同一个时刻只能由一个线程执行 请求与保持:一个线程在请求被占用资源时,对已经获得的资源保持不放。 循环等待:发生死锁时所有的线程都会形成一个死循环,一直阻塞。 不可剥夺条件:线程对所获得的资源在未使用完时不能被其他线程剥夺,只能自己释放。 避免死锁的方法就是破坏死锁产生的条件。

64.请你说说内存管理

linux操作系统采用段页式内存管理方式:页式存储管理可以有效的提高内存利用率,段式内存管理能反映程序的逻辑结构并有利于段的共享。将这两种方法结合起来就形成了段页式储存管理方式。段页式储存管理方式就是先建好用户程序分成若干个段,再把每个段分成若干页,并为每个段赋予一个段名

65.请你说说虚拟内存和物理内存的区别

  • 物理内存 以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于 CPU 的地址线条数。比如在 32 位平台下,寻址的范围是 2^32 也就是 4G。并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给 4G 物理内存,就可能会出现很多问题: - 因为物理内存是有限的,当有多个进程要执行的时候,都要给 4G 内存,很显然内存不够,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作效率很低 - 由于指令都是直接访问物理内存的,那么任何进程都可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是不安全的
  • 虚拟内存 由于物理内存有很多问题,所以出现了虚拟内存。虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

66.请你说说IO多路复用(select、poll、epoll)

IO多路复用指的是单个进程或者线程能同时处理多个IO请求,select,epoll,poll是LinuxAPI提供的复用方式。本质上由操作系统内核缓冲IO数据,使得单个进程线程能监视多个文件描述符。select是将装有文件描述符的集合从用户空间拷贝到内核空间,底层是数组,poll和select差距不大,但是底层是链表,这就代表没有上限,而select有数量限制。epoll则是回调的形式,底层是红黑树,避免轮询,时间复杂度从O(n)变为O(1)

67.epoll原理

epoll是一种高效地IO多路复用技术。调用epoll_create()会创建一个结构体数据,里面包含一个用于遍历扫描文件描述符状态的红黑树和一个就绪列表。调用epoll_ctr()可以进行增删改要监听的文件描述符及事件。调用epoll_wt()就会让内核检测就绪事件,将就绪事件到该表列表返回。epoll有两种触发机制。水平触发:当文件描述符状态改变时就立即进行IO操作,如果不进行处理将继续通知。边沿触发:是高速工作方式。该机制默认你已经知道了状态描述符改变,再你改变IO状态后描述状态改变后不会通知。该种方式减少了epoll的重复触发次数,提升了效率。必须使用非阻塞接口防止因一个文件描述符阻塞读写其他任务饿死

68.请你说说MySQL的事务隔离级别

事务隔离级别是为了解决脏读、不可重复读、幻读 脏读:一个事务读取了另一个事务未提交的数据 不可重复读:事务A两次读取的数据不一致,读第二次之前可能有其他事务修改了这个数据并提交了 幻读:事务A两次读取数据库,两次查询结果的条数不同,称为幻读。行数变了即为幻读,数据变了即为不可重复度 事务隔离级别如下: 读未提交:以上三个问题都解决不了 读已提交:只能解决脏读 可重复读:mysql的默认隔离级别,能解决脏读和不可重复读,包含了间隙锁,可以防止幻读 串行化:都可以解决。(为每个读取操作加一个共享锁)

69.请你说说innodb和myisam的区别

1.innodb支持事务,myisam不支持。2.Innodb支持行级锁;myisam支持表级锁。3.Innodb的增删改性能更优;Myisam的查询性能更优。4.Innodb不支持全文索引,myisam默认支持。5.Innodb默认支持外键,而myisam不支持。

70.请你说说MySQL索引,以及它们的好处和坏处

MySQL索引是一种帮助快速查找数据的数据结构,可以把它理解为书的目录,通过索引能够快速找到数据所在位置。场景的索引数据结构有:Hash表(通过hash算法快速定位数据,但不适合范围查询,因为需要每个key都进行一次hash)、二叉树(查找和修改效率都比较高),但是在InnoDB引擎中使用的索引是B+Tree,相较于二叉树,B+Tree这种多叉树,更加矮宽,更适合存储在磁盘中。使用索引增加了数据查找的效率,但是相对的由于索引也需要存储到磁盘,所以增加了存储的压力,并且新增数据时需要同步维护索引。但是合理的使用索引能够极大提高我们的效率!

71.请你讲讲B树和B+树

B树和B+树都是多路平衡查找树。B树中所有节点都存放数据。B+树只有叶子结点存放数据,其他节点存放key。B树中的叶子结点是独立的,B+书中的叶子结点通过链与相邻叶子结点连接。B树查找使用的是二分查找,没有查找到叶子结点就可能结束,而B+树必须从根节点进行查找,查询效率更稳定。

72.MySQL主从同步是如何实现的

复制(replication)是MySQL数据库提供的一种高可用高性能的解决方案,一般用来建立大型的应用。总体来说,replication的工作原理分为以下3个步骤: 1. 主服务器(master)把数据更改记录到二进制日志(binlog)中。 2. 从服务器(slave)把主服务器的二进制日志复制到自己的中继日志(relay log)中。 3. 从服务器重做中继日志中的日志,把更改应用到自己的数据库上,以达到数据的最终一致性

73.请你说说数据库索引的底层数据结构

数据库索引用的是B+树,它的叶子节点存储所有的数据,并且叶子节点通过指针进行链接,树的查找效率与其高度有关,树的高度越高,查找效率越低,B+树的高度一般在2-4层,这意味着查找到某一键值只需要2-4次IO操作,而且现在的数据库1s中至少可以做100次IO操作,2-4次的IO操作意味着查询时间只需要0.02-0.04s,所以其查找效率很高。

74.请你说说聚簇索引和非聚簇索引

它们两个的最大区别就是索引和数据是否存放在一起。 聚簇索引:索引和数据存放在一起,叶子节点保留数据行。 非聚簇索引:索引和数据分开存放,叶子节点存放的是指向数据行的地址。

75.请你说说数据库引擎有哪些,各自有什么区别

1.InnoDB引擎支持MySQL事务,具有提交,回滚和崩溃恢复功能能够更加安全的保护用户数据;支持行级锁,提高多用户并发和性能;支持外键,维护数据完整性。 2.MyISAM引擎,占用空间较小,支持表级锁,能够限制读写工作的负载的性能,查询效率较高,常用于只读场景。 3.Memory引擎,将所有数据存储在RAM(主存)中,在非关键字查询时,查询效率较高。

76.数据库为什么不用红黑树而用B+树

索引的数据结构会被存储在磁盘中,每次查询都需要到磁盘中访问,对于红黑树,树的高度可能会非常的高,会进行很多次的磁盘IO,效率会非常低,B+树的高度一般为2-4,也就是说在最坏的条件下,也最多进行2到4次磁盘IO,这在实际中性能时非常不错的

77.请你介绍一下数据库的ACID

原子性

事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。

回滚可以用回滚日志(Undo Log)来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。

一致性

数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。

隔离性

一个事务所做的修改在最终提交以前,对其他事务是不可见的。

持久性

一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。

系统发生崩溃可以用重做日志(Redo Log)进行恢复,从而实现持久性。与回滚日志记录数据的逻辑修改不同,重做日志记录的是数据页的物理修改。

事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:

  • 只有满足一致性,事务的执行结果才是正确的。
  • 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
  • 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
  • 事务满足持久化是为了能应对系统崩溃的情况。

78.请你说说数据库的索引是什么结构,为什么不用哈希表

MySQL中的索引是采用B+树的,哈希表的查询效率很高,但是哈希表最大的问题就是不支持范围和顺序查找,还会产生哈希冲突问题

79.请你说说InnoDB的MVCC

多版本并发控制,最大的优点是无锁并发,读不加锁,因此读写不冲突,并发性好,它为每个数据都根据事务维护了多个版本,使其在并发事务中解决了读写冲突,同时使用快照读为MVCC提供了非阻塞读功能,所以它是一个用户解决读写冲突的无锁并发控制机制,它通过数据表的三个隐藏字段分别为 db_trx_id(最长操作事务id),roll_point(undolog指针),db_row_id(唯一性自增张列,可能没有),undolog和readview实现。

80.请你说说索引怎么实现的B+树,为什么选这个数据结构

  • 如何实现: - 索引的本质上就是通过预排序+树型结构来加快检索效率,而MySQL中使用InnoDB和MyISAM引擎时都使用了B+树实现索引 - 为什么选取: - 在二叉查找树上查找一个数据时,当出现海量信数据时,查找效率将大大折扣 - B+树是一种一棵平衡多路查找树,可以有效减少磁盘IO同时B+树增加了叶子节点间的连接,能保证范围查找时找到起点和终点后能快速取出需要的数据

81.请你说说乐观锁和悲观锁

乐观锁:乐观锁总是假设最好的情况,每次去拿数据的时候默认别人不会修改,所以不会上锁,只有当更新的时候会判断一下在此期间有没有人更新了这个数据。适用于多读,可以使用版本号机制进行控制 悲观锁:悲观锁总是假设最坏的情况,每次去拿数据是都认为别人会修改,所以每次在拿数据时都会上锁,这样别人想拿这个数据时会阻塞直到拿到锁。mysql数据库的共享锁和排他锁都是悲观锁的实现。

82.说说你对Spring Boot的理解,以及它和Spring的区别

(1)1、从本质上来说,Spring Boot就是Spring,它帮你完成了一些Spring Bean配置。 2、Spring Boot使用“习惯优于配置”的理念让你的项目快速地运行起来 3、但Spring Boot本身不提供Spring的核心功能,而是作为Spring的脚手架框架,达到快速构建项目的目的

(2)Spring Boot优点, 可以快速构建项目 - 可以对主流开发框架的无配置集成 - 项目可独立运行,无需外部依赖Servlet容器 - 提供运行时的应用监控 - 可以极大地提高开发、部署效率 - 可以与云计算天然集成

(3)核心功能: 1. 自动配置 针对很多Spring应用程序常见的应用功能,Spring Boot能自动提供相关配置。 2. 起步依赖 Spring Boot通过起步依赖为项目的依赖管理提供帮助。起步依赖其实就是特殊的Maven依赖和Gradle依赖,利用了传递依赖解析,把常用库聚合在一起,组成了几个为特定功能而定制的依赖。 3. 端点监控 Spring Boot 可以对正在运行的项目提供监控。

83.说说Spring Boot常用的注解

@SpringBootApplication:它是SpringBoot的核心注解,用于开启自动配置,准确的说是通过该注解内的@EnablAutoConfiguration注解实现的自动配置。 @EnableAutoConfiguration:自动配置注解,在启动Spring应用程序上下文时进行自动配置,自动配置通常是基于项目classpath中引入的类和已定义的bean来实现的。 @Import:@EnableAutoConfiguration的关键功能是通过@Import注解导入的ImportSelector来完成的。 @Congiguration:配置类注解,根据一些特定条件来控制bean的实例化的行为。 @ComponentScan:位置在SpringBoot的启动类上,Spring包扫描。

84.说说Soring Boot的起步依赖

SpringBoot将日常企业应用研发中的各种常见都抽取出来,做成一个个的starter(启动器),starter中整合了该场景下各种可能用到的依赖,用户只需要在Maven中引入starter依赖,SpringBoot就能自动扫描到要加载的信息并启动相应的默认配置。starter提供了大量的自动配置,让用户摆脱了处理各种依赖和配置的困扰。所有这些starter都遵循这约定俗成的默认配置,并允许用户调整这些配置,即遵循“约定大于配置”的原子

85.说说Spring Boot的启动流程

调用run方法,run方法执行流程 =》获取监听器参数配置-打印Banner信息-创建并初始化容器-监听器发送通知

86.说说Spring Boot的自动装配

靠EnableAutoConfigurations注解,首先会从spring.factories中寻找有没有AutoConfiguration类满足Conditional注解的生效条件,有的话,就是实例化该AutoConfiguration类,然后加载到spring容器就实现了spring的自动装配

87.简单介绍Spring

Spring是一个轻量级的免费框架,它有两大核心功能,分别是ioc和aop,ioc控制反转是将创建对象的权限交给spring容器来进行管理,可以很好的起到解耦和的作用,aop是一种编程思想,底层使用的是动态代理,可以在程序原有的功能上进行增强,常用的地方有日志记录,权限验证等

88.说说你对IoC的理解

IoC:控制反转。控制:对象的创建的控制权限;反转:将对象的控制权限交给spring。之前我们创建对象时用new,现在直接从spring容器中取,维护对象之间的依赖关系,降低对象之间的耦合度。 实现方式为DI,依赖注入,有三种注入方式:构造器、setter、接口注入

89.说说你对AOP的理解

AOP面向切面编程。是spring两大核心之一,它是一种编程思想,是对OOP的一种补充。它可以对业务逻辑的各个部分进行隔离,降低耦合,提高代码的可重用性。它的底层是通过动态代理实现的。它的应用场景有事务、日志管理等。

90.说说Bean的生命周期

创建,初始化,调用,销毁; bean的创建方式有四种,构造器,静态工厂,实例工厂,setter注入的方式。 spring在调用bean的时候因为作用域的不同,不同的bean初始化和创建的时间也不相同。 在作用域为singleton的时候,bean是随着容器一起被创建好并且实例化的, 在作用域为pritotype的时候,bean是随着它被调用的时候才创建和实例化完成。 然后程序就可以使用bean了,当程序完成销毁的时候,bean也被销毁

91.说说@Autowired和@Resource注解的区别

@Autowied是Spring提供的注解,@Resource是JDK提供的注解。@Autowied是只能按类型注入,@Resource默认按名称注入,也支持按类型注入。@Autowired按类型装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它required属性为false,如果我们想使用按名称装配,可以结合@Qualifier注解一起使用

92.说说Spring事务管理

spring支持编程式事务管理和声明式事务管理两种方式: ①编程式事务管理使用TransactionTemplate。 ②声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。 声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。

93.说说Bean的作用域,以及默认的作用域

作用域分为singleton、prototype、request、session、globalsession。singleton表示在spring容器中仅存在一个实例,以单例的方式存在,prototype:每次调用都会返回一个新的实例,request:每次HTTP请求都会创建一个新的bean,session:同一个HTTP session共享一个bean,不同的HTTP请求使用不同的session,global session:同一个全局session共享一个bean。

94.说说BeanFactory和FactoryBean的区别

BeanFactory是最基础的IOC容器,给Spring 的容器定义一套规范,给IOC容器提供了一套完整的规范; FactoryBean只是SpringIOC容器创建Bean的一种形式;

95.说说你对MVC的理解

Spring mvc 是一个基于java的实现了mvc设计模式的轻量级web框架,在这种模式下软件被分为三层,即model、view、Controller。将软件分层的好处是 可 以将对象之间的耦合度降低,便于代码的维护。model封装了数据和对数据的操作,是实际进行数据处理的地方,view负责进行模型的展示,一般就是我们见 到的用户界面 Controller控制器负责视图和模型之间的交互主要负责两方面的动作,一是把用户的请求分发到相应的模型,二是把模型的改变及时地响应到 视图上。Spring mvc框架已经成为了mvc模式地最主流实现,前端控制器是DispatcherServlet接口实现类,映射处理器是HandlerMapping接口实现类,视图 解析器是ViewResolver接口实现类,页面控制器是Controller接口实现类

MVC是一种设计模式,将软件分为三层,分别是模型层,视图层,控制器层。其中模型层代表的是数据,视图层代表的是界面,控制器层代表的是逻辑处理,是连接视图与模型之前的桥梁。降低耦合,便于代码的维护

96.介绍一下Spring MVC的执行流程

SpringMVC 的执行流程如下。

  1. 用户点击某个请求路径,发起一个 HTTP request 请求,该请求会被提交到 DispatcherServlet(前端控制器);
  2. 由 DispatcherServlet 请求一个或多个 HandlerMapping(处理器映射器),并返回一个执行链(HandlerExecutionChain)。
  3. DispatcherServlet 将执行链返回的 Handler 信息发送给 HandlerAdapter(处理器适配器);
  4. HandlerAdapter 根据 Handler 信息找到并执行相应的 Handler(常称为 Controller);
  5. Handler 执行完毕后会返回给 HandlerAdapter 一个 ModelAndView 对象(Spring MVC的底层对象,包括 Model 数据模型和 View 视图信息);
  6. HandlerAdapter 接收到 ModelAndView 对象后,将其返回给 DispatcherServlet ;
  7. DispatcherServlet 接收到 ModelAndView 对象后,会请求 ViewResolver(视图解析器)对视图进行解析;
  8. ViewResolver 根据 View 信息匹配到相应的视图结果,并返回给 DispatcherServlet;
  9. DispatcherServlet 接收到具体的 View 视图后,进行视图渲染,将 Model 中的模型数据填充到 View 视图中的 request 域,生成最终的 View(视图);
  10. 视图负责将结果显示到浏览器(客户端)。

97.在MyBatis中$和#有什么区别

$设置参数时,MyBatis会创建普通的SQL语句,然后在执行SQL 语句时将参数拼入SQL #设置参数时,MyBatis会创建预编译的SQL语句,然后在执行SQL时MyBatis会为预编译SQL中的占位符赋值,预编译的SQL语句执行效率高,并且可以防止注入攻击,效率和安全性都大大优于前者

98.介绍一下MyBatis的缓存机制

MyBatis的缓存机制,一级缓存也称为本地缓存,它默认启用且不能关闭。一级缓存存在于SqlSession的生命周期中,即它是SqlSession级别的缓存,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中,如果同一个SqlSession中执行的方法和参数完全一致,则会将缓存的对象返回;二级缓存则为SqlSessionFactory,mybaits的全局配置setting有一个参数cacheEnabled,这个参数是二级缓存的全局开关,默认值是true,初始状态为启用状态,映射语句文件中的所有SELECT 语句将会被缓存。 - 映射语句文件中的所有时INSERT 、UPDATE 、DELETE 语句会刷新缓存。 - 缓存会使用Least Recently Used ( LRU ,最近最少使用的)算法来收回

99.说说你对Redis的了解

Redis是一款基于键值对的NoSQL数据库,Redis中拥有string(字符串),hash(哈希)、list(列表)、set(集合)等多种数据结构,redis将数据写进内存的性能很快,不仅如此,如遇到系统崩溃,内存中的数据不会丢失;redis访问速度快、支持的数据类型丰富,很适合用来储存热点数据、 而且适用业务广,如可以运用expire命令来做限时业务,设置一个键的生存时间,到时间后redis会自动删除它,,如排行榜可以借住redis的SortedSet进行热点数据的排序,还有分页查询,模糊查询,点赞好友等

100.请你说说Redis的数据类型

Redis拥有五种基本数据类型和四种特殊的数据类型。 五种基本数据类型: 1)String:String是Redis中最基本的数据类型,可以存储任何数据,包括二进制数据、序列化的数据、JSON化的对象甚至是图片。 2)List:List是字符串列表,按照插入的顺序排序,元素可以重复,你可以添加一个元素到哦列表的头部或者尾部,底层是一个链表结构。 3)Set:Set是一个无序不重复的集合。 4)Hash:Hash是String类型的filed和value的集合,适合用于存储对象。 5)Zset:Zset和set一样也是String类型元素的集合,且不允许有重复的元素,但不同的是Zset的每个元素都会关联一个分数,分数可以重复,Redis通过分数来为集合汇总的成员进行从小到大的排序。 四种特殊数据类型 1)bitmap 2)hyperloglog 3)geo 4)stream

101.请你说说Redis数据类型中的zset,它和set有什么区别?底层是怎么实现的?

zset是有序的,而set是无序的。 zset底层使用的是压缩列表以及跳跃表,当元素数量小于128个,所有member的长度都小于64字节,是使用压缩列表。不满足这两个条件时使用跳跃表。 set底层是hashtable和inset.

102.详细的说说Redis的数据类型

redis中常用的五种数据结构:string、list、set、zset、hash。String结构底层是一个简单动态字符串,支持扩容,存储字符串。list存储线性有序且可重复的元素,底层数据结构可以是双向链表/压缩列表。set存储不可重复的元素,一般用于求交集、差集等,底层数据结构可以是hash和整数数组,zset存储的是有序不可重复的元素,zset为每个元素添加了一个score属性作为排序依据,底层数据结构可以是ziplist和跳表,hash类型存储的是键值对,底层数据结构是ziplist和hash。redis会在性能以及节省内存间考虑,选择最适合当前状态的底层数据结构实现

list(有序可重)的底层数据结构是双向链表/压缩列表

set(不可重)=hash+整数数组,

zset(有序不重)=ziplist+跳表

hash(存键值对)=ziplist+hash

103.说说Redis的单线程架构

redis采用的是单线程+IO多路复用技术。这里单线程指的是redis中读写操作和网络IO使用的是是有一个线程来完成,但是其他操作是有其他线程完成,例如持久化操作。单线程既可以简化数据结构和算法的实现,同时也消除了线程切换和锁竞争所带来的消耗。redis中采用的IO多路复用技术实现了单线程下同时处理多个IO请求。redis为什么这么快:1.单线程进行读写操作,避免线程切换和锁竞争带来的消耗。2:redis操作是在内存中进行的。3.最重要的就是:采用了IO多路复用技术,实现了在网络IO中能够处理大量并发请求,实现高吞吐率。

104.说说Redis的持久化策略

1.RDB: redis database 在指定的时间间隔内,将内存中的数据集的快照写入磁盘,文件名dump.rdb 适合大规模的数据恢复,对数据库的完整性和一致性要求不是很高 一定时间间隔备份一次,如果数据库意外down掉,就会失去最后一次快照的所有修改 2.AOF: append only file 以日志的形式记录每个写操作,只允许追加文件,不允许改写文件,redis启动时会读取这个文件,并从头到尾执行一遍,以此来恢复数据,文件名appendonly.aof 在最恶劣的环境下,也丢失不会超过2秒的数据,完整性较高,但是会对磁盘持续的进行IO,代价太大。企业级最少需要5G才能支持 如果.aof文件大小超过原来的一倍,会进行重写压缩,保留最小的指令集合 3.优先级 aof>rdb

105.说说Redis的主从同步机制

主从同步分为全量同步和增量同步,从机第一次连接主机时不会携带主机id和数据偏移量,主机会对从机的主机id进行校验,如果不是则说明是第一次连接需要进行全量同步,原理就是将当前数据写到RDB文件发送给从机,从机接收到文件之后将数据读取到从机的内存中,增量同步是第二次和之后连接才会发生,当从机第一次同步完成之后,主机在这期间数据发生变化,会将命令存储在缓冲区,当校验到从机的id正确时会获取从机的偏移量,主机从偏移量记录的命令开始将从机没同步的数据的操作命令发送给从机执行,执行完成后即完成了数据同步

106.说说Redis的缓存淘汰策略

惰性删除: 当你访问一个key的时候,redis检查它的过期时间,如果过期就会删除。 定期删除:redis会将设置了过期事件的key全都放入一个字典中,然后进行扫描,过期了的key就会被删除。但是扫描不是全局扫描,而是一种简单的贪心算法,从字典中随机选择20个key,将过期的key删除,当过期的key超过一定的比例时,将会再次随机选择,知道随机选择的key小于一定比例。 当写入数据超过最大内存限制时,redis也有8种淘汰方案: 1. 直接报错 2. 删除拥有过期时间的key中快要过期的 3. 在所有key中随机删除 4. 在具有过期时间的key中随机删除 5. lru 从所有key中删除使用时间离现在最久的 6. lru 从拥有过期时间的key中删除使用时间离现在最久的 7. lfu 从所有key中删除使用次数最少的 8. lfu 从拥有过期时间的key中删除使用次数最少的

107.说说缓存穿透、击穿、雪崩的区别

缓存穿透:客户端访问不存在的数据,使得请求直达存储层,导致负载过大,直至宕机。原因可能是业务层误删了缓存和库中的数据,或是有人恶意访问不存在的数据。解决方式:1.存储层未命中后,返回空值存入缓存层,客户端再次访问时,缓存层直接返回空值。2.将数据存入布隆过滤器,访问缓存之前经过滤器拦截,若请求的数据不存在则直接返回空值。 缓存击穿:一份热点数据,它的访问量非常大,在它缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。解决方案:1.永不过期:对热点数据不设置过期时间。2.加互斥锁,当一个线程访问该数据时,另一个线程只能等待,这个线程访问之后,缓存中的数据将被重建,届时其他线程就可以从缓存中取值。 缓存雪崩:大量数据同时过期、或是redis节点故障导致服务不可用,缓存层无法提供服务,所有的请求直达存储层,造成数据库宕机。解决方案:1.避免数据同时过期,设置随机过期时间。2.启用降级和熔断措施。3.设置热点数据永不过期。4.采用redis集群,一个宕机,另外的还能用

108.Redis如何与数据库保持双写一致性

共有四种同步策略:1.先更新数据库再更新缓存。缺点:多线程并发下会存在数据库中数据和缓存不一致的的现象。可能出现2.先更新缓存在更新数据库,优点就是每次数据变化都可以及时的更新缓存,但是消耗很大,影响服务器性能。3.先删除缓存在更新数据库。缺点:也会导致缓存和数据库数据不一致。4.先更新数据库再删除缓存。缺点仍然可能存在缓存和数据库中数据不一致的情况,但是,我们可以使用重试机制进行操作。,所以说这是效果最好的解决方案。

109.如何实现Redis高可用

主从复制:写一定是在主服务器上,然后主服务器同步给从服务器。缺点:当主服务器挂掉的时候,不能自动切换到从服务器上。主从服务器存储数据一样,内存可用性差。优点:在一定程度上分担主服务器读的压力。哨兵模式:构建多个哨兵节点监视主从服务器,当主服务器挂掉的时候,自动将对应的从服务器切换成主服务器。优点:实现自动切换,可用性高。缺点:主从服务器存储数据一致,内存可用性差。还要额外维护一套哨兵系统,较为麻烦。集群模式:采用无中心节点的方式实现。多个主服务器相连,一个主服务器可以有多个从服务器,不同的主服务器存储不同的数据。优点:可用性更高,内存可用性高。

?110.如何利用Redis实现一个分布式锁

最简单redis分布式锁的实现方式:加锁:setnx(key,1),解锁:del(key),问题:如果客户忘记解锁,将会出现死锁。第二种分布式锁的实现方式:setnx(key,1)+expire(key,30),解锁:del(key).问题:,由于setnx和expire的非原子性,当第二步挂掉,仍然会出现死锁。第三种方式:加锁:将setnx和expire变成原子性操作,set(key,1,30,NX),解锁:del(key)。同时考虑到线程A还在执行,但是锁已经到期,当线程A执行结束时去释放锁时,可能就会释放别的线程锁,所以在解锁时要先判断一下value值,看是不是该锁,如果是,再进行删除

111.设计模式了解么

常用的设计模式有单例模式、工厂模式、代理模式、适配器模式、装饰器模式、模板方法模式等等。像sping中的定义的bean默认为单例模式,spring中的BeanFactory用来创建对象的实例,他是工厂模式的体现。AOP面向切面编程时代理模式的体现,它的底层就是基于动态代理实现的。适配器模式在springMVC中有体现,它的处理器适配器会根据处理器规则适配相应的处理器执行,模板方法模式用来解决代码重复的问题等

112.请你讲讲单例模式、请你手写一下单例模式

单例模式是一个类只能创建一个对象,单例模式又分为饿汉式和懒汉式,饿汉式就是在类加载的的时候不管后边会不会用到都创建一个对象并且做初始化,浪费资源。懒汉式是在类加载的的时候创建对象,但不进行初始化,只有在用的时候,先判断对象是否为空,如果为空,对对象进行初始化,然后返回初始化后的值。为了解决线程安全问题,又分为懒汉式线程安全、懒汉式双重检验锁、静态内部类等

public class Singleton{

​ private static Singleton instance;

​ private Singleton(){}

​ private static synchronized Singleton getInstance(){

​ if(instance == null){

​ instance = new Singleton();

​ }

​ return instance;

​ }

}

113.请你讲讲工厂模式,手写实现工厂模式

工厂模式其用意是定义一个创建产品的接口将具体创建推迟到子类中去。工厂模式可以分为简单工厂、工厂方法、抽象工厂。简单工厂就是定义一个工厂类根据传入的参数返回不同的实例,被创建的实例具有共同的父类或接口。工厂方法工厂方法是定义一个工厂接口,但创建过程让子类去实现,去决定哪一个产品被实例化。抽象工厂模式是对工厂方法的进一步深化,在工厂类中可以创建一组对象。实现方式是提供一个创建一系列相关或相互依赖对象的接口而无需指定具体的类

114.说说垃圾收集器

Serial(新生代)、Serial Old(老年代):适用于单核小CPU,单核工作,回收时会暂停其他工作stop the word。

PawNew(新生代)、CMS(老年代):适用于多核CPU,最求短暂停时间,多核工作,使用标记清除算法,最短的暂停时间。

Parallel Scavenge(新生代-标记复制算法)、Parallel Old(老年代-标记整理算法):1.7,1.8默认的组合,适用于多核CPU,追求最大吞吐量

G1 jdk1.9默认,适用于大内存多核CPU服务器,它不按整个新生代或老年代去回收,而是开辟了面向局部收集,实现了较小的收集暂停时间和高吞吐量。

Java中的error和exception有什么区别

Error(错误):程序无法处理,通常指程序中出现的严重问题。

例如java.lang.VirtualMachineError(Java虚拟机运行错误):当 Java虚拟机崩溃或用尽了它继续操作所需的资源时,抛出该错误
例如java.lang.StackOverflowError(栈溢出错误):当应用程序递归太深而发生堆栈溢出时,抛出该错误。
例如java.lang.OutOfMemoryError(内存溢出):内存溢出或没有可用的内存提供给垃圾回收器时,产生这个错误。
Error(错误)是不可查的,而且也常常在应用程序的控制和处理能力之外,因此当Error(错误)出现时,程序会立即奔溃,Java虚拟机立即停止运行,
Exception(异常):是指程序本身可以处理的异常(可以向上抛出或者捕获处理)。

Java处理异常的默认方式是中断处理。

以java.lang.NullPointerException为例,当程序出现空指针异常时,会创建一个空指针异常对象,并向外抛出,并被虚拟机捕获,从而导致程序中断执行。

进程间通信的方式有几种

一、进程间的通信方式

管道( pipe ):

管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

有名管道 (namedpipe) :

有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

信号量(semophore ) :

信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

消息队列( messagequeue ) :

消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

信号 (sinal ) :

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

共享内存(shared memory ) :

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

套接字(socket ) :

套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。

DNS域名解析过程

  1. 浏览器先检查自身缓存中有没有被解析过的这个域名对应的ip地址,如果有,解析结束。同时域名被缓存的时间也可通过TTL属性来设置。
  2. 如果浏览器缓存中没有(专业点叫还没命中),浏览器会检查操作系统缓存中有没有对应的已解析过的结果。而操作系统也有一个域名解析的过程。在windows中可通过c盘里一个叫hosts的文件来设置,如果你在这里指定了一个域名对应的ip地址,那浏览器会首先使用这个ip地址。
  3. 如果至此还没有命中域名,才会真正的请求本地域名服务器(LDNS)来解析这个域名,这台服务器一般在你的城市的某个角落,距离你不会很远,并且这台服务器的性能都很好,一般都会缓存域名解析结果,大约80%的域名解析到这里就完成了。
  4. 如果LDNS仍然没有命中,就直接跳到Root Server 域名服务器请求解析
  5. 根域名服务器返回给LDNS一个所查询域的主域名服务器(gTLD Server,国际顶尖域名服务器,如.com .cn .org等)地址
  6. 此时LDNS再发送请求给上一步返回的gTLD
  7. 接受请求的gTLD查找并返回这个域名对应的Name Server的地址,这个Name Server就是网站注册的域名服务器
  8. Name Server根据映射关系表找到目标ip,返回给LDNS
  9. LDNS缓存这个域名和对应的ip
  10. LDNS把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结束

浏览器输入一个网址后发生了什么

1.在客户端浏览器中输入URL.
2.发送到DNS(域名服务器)获得域名对应的web服务器的lP地址。
3.客户端浏览器与web服务器建立tcp连接.
4.客户端浏览器向对应IP地址的WEB服务器发送相应的http或https请求。
5.WEB服务器响应请求,返回指定的URL数据或错误信息。如果设定重定向,则重定向到新的URL地址。
6.客户端浏览器下载数据,解析HTML源文件,解析的过程中实现对页面的排版,解析完成后,在浏览器中显示基础界面。
7.分析页面中的超链接,显示在当前页面,重复以上过程直至没有超链接需要发送,完成页面的全部显示。

Jvm内存划分

面经01-jvm内存划分

第一,是程序计数器(Program Counter Register),在JVM规范中,每个线程都有自己的程序计数器。这是一块比较小的内存空间,存储当前线程正在执行的Java方法的JVM指令地址,即字节码的行号。如果正在执行Native方法,则这个计数器为空。该内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM情况的内存区域。

第二,Java虚拟机栈(Java Virtal Machine Stack),同样也是属于线程私有区域,每个线程在创建的时候都会创建一个虚拟机栈,生命周期与线程一致,线程退出时,线程的虚拟机栈也回收。虚拟机栈内部保持一个个的栈帧,每次方法调用都会进行压栈,JVM对栈帧的操作只有出栈和压栈两种,方法调用结束时会进行出栈操作。

该区域存储着局部变量表,编译时期可知的各种基本类型数据、对象引用、方法出口等信息。

第三,本地方法栈(Native Method Stack)与虚拟机栈类似,本地方法栈是在调用本地方法时使用的栈,每个线程都有一个本地方法栈。

第四,堆(Heap),几乎所有创建的Java对象实例,都是被直接分配到堆上的。堆被所有的线程所共享,在堆上的区域,会被垃圾回收器做进一步划分,例如新生代、老年代的划分。Java虚拟机在启动的时候,可以使用“Xmx”之类的参数指定堆区域的大小。

第五,方法区(Method Area)。方法区与堆一样,也是所有的线程所共享,存储被虚拟机加载的元(Meta)数据,包括类信息、常量、静态变量、即时编译器编译后的代码等数据。这里需要注意的是运行时常量池也在方法区中。

根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。由于早期HotSpot JVM的实现,将CG分代收集拓展到了方法区,因此很多人会将方法区称为永久代。Oracle JDK8中已永久代移除永久代,同时增加了元数据区(Metaspace)。

第六,运行时常量池(Run-Time Constant Pool),这是方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时,会抛出OutOfMemoryError异常。

第七,直接内存(Direct Memory),直接内存并不属于Java规范规定的属于Java虚拟机运行时数据区的一部分。Java的NIO可以使用Native方法直接在java堆外分配内存,使用DirectByteBuffer对象作为这个堆外内存的引用。

可重入锁

什么是“可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。例如

使用ReentrantLock的注意点
ReentrantLock和synchronized不一样,需要手动释放锁,所以使用ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要—样

Java锁升级

  • Java中的锁有几种状态:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

无锁状态

程序不会有锁的竞争。那么这种情况我们不需要加锁,所以这种情况下对象锁状态为无锁。

偏向锁

偏向锁,顾名思义,它会偏向于第一个访问锁的线程

如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。线程第二次到达同步代码块时,会判断此时持有锁的线程是否就是自己,如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。偏向锁通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致STW(stop the word)操作;

轻量级锁(自旋锁)

自旋锁:自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

在轻量级锁状态下继续锁竞争,如果成功就成功获取轻量级锁。否则进入锁膨胀阶段,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁。
自旋锁:竞争锁失败的线程,并不会真实的在操作系统层面挂起等待,而是JVM会让线程做几个空循环(基于预测在不久的将来就能获得),在经过若干次循环后,如果可以获得锁,那么进入临界区,如果还不能获得锁,才会真实的将线程在操作系统层面进行挂起。这样的好处就是快,坏处就是消耗cpu资源。

重量级锁

当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起,等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

重量级锁的特点:其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行速度较长。

锁升级场景

  • 场景1: 经常只有某一个线程来加锁。

加锁过程:也许获取锁的经常为同一个线程,这种情况下为了避免加锁造成的性能开销,加偏向锁。
偏向锁的执行流程如下:
1、线程首先检查该对象头的线程ID是否为当前线程;
2、A:如果对象头的线程ID和当前线程ID一致,则直接执行代码;B:如果不是当前线程ID则使用CAS方式替换对象头中的线程ID,如果使用CAS替换不成功则说明有线程正在执行,存在锁的竞争,这时需要撤销偏向锁,升级为轻量级锁。
3、如果CAS替换成功,则把对象头的线程ID改为自己的线程ID,然后执行代码。
4、执行代码完成之后释放锁,把对象头的线程ID修改为空。

  • 场景2: 有线程来参与锁的竞争,但是获取锁的冲突时间很短。

当开始有锁的竞争了,那么偏向锁就会升级到轻量级锁;
线程获取锁出现冲突时,线程必须做出决定是继续在这里等,还是先去做其他事情,等会再来看看,而轻量级锁的采用了继续在这里等的方式。当发现有锁竞争,线程首先会使用自旋的方式循环在这里获取锁,因为使用自旋的方式非常消耗CPU。当一定时间内通过自旋的方式无法获取到锁的话,那么锁就开始升级为重量级锁了。

  • 场景3: 有大量的线程参与锁的竞争,冲突性很高。

当获取锁冲突多,时间越长的时候,线程肯定无法继续在这里死等了,所以只好先挂起,然后等前面获取锁的线程释放了锁之后,再开启下一轮的锁竞争,而这种形式就是我们的重量级锁。

为什么jdk9要将String的底层由char[]改为byte[]

JDK 9并没有将String的底层实现由char[]改成byte[],而是在JDK 9中,引入了一个CompactStrings的优化,优化的目的是减少String对象的内存消耗,在大多数Java程序的堆里,String占用的空间最大,并且绝大多数String只有Latin-1字符,这些Latin-1字符只需要1个字节就够了。JDK9之前,JVM因为String使用char数组存储,每个char占2个字节,所以即使字符串只需要1字节/字符,它也要按照2字节/字符进行分配,浪费了一半的内存空间。

JDK9是怎么解决这个问题的呢?一个字符串出来的时候判断,它是不是只有Latin-1字符,如果是,就按照1字节/字符的规格进行分配内存,如果不是,就按照2字节/字符的规格进行分配,提高了内存使用率。

单点登录

>

手写单例模式、工厂模式

一、单例模式的定义

定义: 确保一个类只有一个实例,并提供该实例的全局访问点。

这样做的好处是:有些实例,全局只需要一个就够了,使用单例模式就可以避免一个全局使用的类,频繁的创建与销毁,耗费系统资源。

二、单例模式的设计要素
  • 一个私有构造函数 (确保只能单例类自己创建实例)
  • 一个私有静态变量 (确保只有一个实例)
  • 一个公有静态函数 (给使用者提供调用方法)

简单来说就是,单例类的构造方法不让其他人修改和使用;并且单例类自己只创建一个实例,这个实例,其他人也无法修改和直接使用;然后单例类提供一个调用方法,想用这个实例,只能调用。这样就确保了全局只创建了一次实例。

三、单例模式的6种实现及各实现的优缺点
(一)懒汉式(线程不安全)

实现:

public class Singleton {
private static Singleton uniqueInstance;

private Singleton() {

}

public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}

说明: 先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。

优点: 延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。

缺点: 线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例;

(二)饿汉式(线程安全)

实现:

public class Singleton {

private static Singleton uniqueInstance = new Singleton();

private Singleton() {
}

public static Singleton getUniqueInstance() {
return uniqueInstance;
}

}

说明: 先不管需不需要使用这个实例,直接先实例化好实例 (饿死鬼一样,所以称为饿汉式),然后当需要使用的时候,直接调方法就可以使用了。

优点: 提前实例化好了一个实例,避免了线程不安全问题的出现。

缺点: 直接实例化好了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会操作系统的资源浪费。

(三)懒汉式(线程安全)

实现:

public class Singleton {
private static Singleton uniqueInstance;

private static singleton() {
}

private static synchronized Singleton getUinqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}

}

说明: 实现和 线程不安全的懒汉式 几乎一样,唯一不同的点是,在get方法上 加了一把 锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。

优点: 延迟实例化,节约了资源,并且是线程安全的。

缺点: 虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方法,会使线程阻塞,等待时间过长。

(四)双重检查锁实现(线程安全)

实现:

public class Singleton {

private volatile static Singleton uniqueInstance;

private Singleton() {
}

public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}

说明: 双重检查数相当于是改进了 线程安全的懒汉式。线程安全的懒汉式 的缺点是性能降低了,造成的原因是因为即使实例已经实例化,依然每次都会有锁。而现在,我们将锁的位置变了,并且多加了一个检查。 也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。 而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。

为什么使用 volatile 关键字修饰了 uniqueInstance 实例变量 ?

uniqueInstance = new Singleton(); 这段代码执行时分为三步:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。 单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。 例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。

解决办法就是加一个 volatile 关键字修饰 uniqueInstance ,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。

优点: 延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。

缺点: volatile 关键字,对性能也有一些影响。

(五)静态内部类实现(线程安全)

实现:

public class Singleton {

private Singleton() {
}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}

}

说明: 首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。当调用 getUniqueInstance() 方法时,会运行 return SingletonHolder.INSTANCE; ,触发了 SingletonHolder.INSTANCE ,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 INSTANCE 实例,而且 JVM 会确保 INSTANCE 只被实例化一次。

优点: 延迟实例化,节约了资源;且线程安全;性能也提高了。

(六)枚举类实现(线程安全)

实现:

public enum Singleton {

INSTANCE;

//添加自己需要的操作
public void doSomeThing() {

}

}

说明: 默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。

优点: 写法简单,线程安全,天然防止反射和反序列化调用。

  • 防止反序列化**序列化:把java对象转换为字节序列的过程; 反序列化: 通过这些字节序列在内存中新建java对象的过程; 说明: 反序列化 将一个单例实例对象写到磁盘再读回来,从而获得了一个新的实例。 我们要防止反序列化,避免得到多个实例。 枚举类天然防止反序列化。** 其他单例模式 可以通过 重写 readResolve() 方法,从而防止反序列化,使实例唯一重写 readResolve() :
private Object readResolve() throws ObjectStreamException{
return singleton;
}
四、单例模式的应用场景

应用场景举例:

  • 网站计数器。
  • 应用程序的日志应用。
  • Web项目中的配置对象的读取。
  • 数据库连接池。
  • 多线程池。
  • ……

使用场景总结:

  • 频繁实例化然后又销毁的对象,使用单例模式可以提高性能。
  • 经常使用的对象,但实例化时耗费时间或者资源多,如数据库连接池,使用单例模式,可以提高性能,降低资源损坏。
  • 使用线程池之类的控制资源时,使用单例模式,可以方便资源之间的通信。
1. 简单工厂模式(静态工厂)

某种程度上不符合设计原则,但实际使用最多。

原始方法
public interface Car {   
public void name();
}
public class Tesla implements Car{
public void name(){
System.out.println("特斯拉!");
}}
public class WuLing implements Car{
public void name(){
System.out.println("五菱宏光!");
}}
public class Consumer {
public static void main(String[] args) {
// 接口,所有实现类
Car car=new WuLing();
Car car2=new Tesla();
car.name();
car2.name();
// Car car = CarFactory.getCar("五菱");
// car.name();
}}
静态工厂方法

因为CarFactory类中的所有方法都是静态的,需要向其中添加参数来返回不同的对象实例。

增加一个新的产品,如果不修改CarFactory代码就做不到。

public class CarFactory {  
// 方法一
public static Car getCar(String car){
if(car.equals("五菱")){
return new WuLing();
}else if(car.equals("特斯拉")){
return new Tesla();
}else {
return null;
}
}
// 方法二
public static Car getWuLing(){
return new WuLing();
}
public static Car getTesla(){
return new Tesla();
}}
public class Consumer {
public static void main(String[] args) {
// 接口,所有实现类
// Car car=new WuLing();
// Car car2=new Tesla();
// car.name();
// car2.name();
Car car = CarFactory.getCar("五菱");
Car car2 = CarFactory.getCar("特斯拉");
car.name();
car2.name();
}}

静态工厂方法

因为CarFactory类中的所有方法都是静态的,需要向其中添加参数来返回不同的对象实例。

增加一个新的产品,如果不修改CarFactory代码就做不到。

public class CarFactory {  
// 方法一
public static Car getCar(String car){
if(car.equals("五菱")){
return new WuLing();
}else if(car.equals("特斯拉")){
return new Tesla();
}else {
return null;
}
}
// 方法二
public static Car getWuLing(){
return new WuLing();
}
public static Car getTesla(){
return new Tesla();
}}
public class Consumer {
public static void main(String[] args) {
// 接口,所有实现类
// Car car=new WuLing();
// Car car2=new Tesla();
// car.name();
// car2.name();
Car car = CarFactory.getCar("五菱");
Car car2 = CarFactory.getCar("特斯拉");
car.name();
car2.name();
}}

新增一个产品

public class DaZhong implements Car{ 
@Override
public void name() {
System.out.println("大众!");
}}
public class DaZhongFactory implements CarFactory {
@Override
public Car getCar() {
return new DaZhong();
}}
public class Consumer {
public static void main(String[] args) {
Car car = new WuLingFactory().getCar();
Car car1 = new TeslaFactory().getCar();
car.name();
car1.name();
Car car2 = new DaZhongFactory().getCar();
car2.name();
}}

对比

  • 结构复杂度:simple更简单
  • 代码复杂度:simple更简单
  • 编程复杂度:simple更简单
  • 管理上的复杂度:simple更简单

根据设计原则:工厂方法模式

根据实际业务:简单工厂模式

3、抽象工厂模式

  • 定义:抽象工厂模式提供了一个创建一系列相关或者相互依赖对象的接口,无需指定它们具体的类

  • 适用场景:

    • 客户端(应用层)不依赖于产品类实例如何被创建、实现等细节
    • 强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量的重复代码(一次一起全部创建,很少更改)
    • 提供一个产品类的库,所有的产品以同样的接口出现,从而使得客户端不依赖于具体的实现
  • 优点:

    • 具体产品在应用层的代码隔离无需关心创建的细节
    • 将一个系列的产品统一到一起创建
  • 缺点:

    • 规定了所有可能被创建的产品集合,产品簇中扩展新的产品困难;

IO多路复用

LRU手写

设计模式的原则

setnx底层实现

可见性问题?volatile,那么volatile底层如何实现的

迭代器失效

dns基于什么协议

osi和tcp/ip,有什么主要的协议

java跨平台如何实现

https加密通信的过程

流量控制和拥塞控制

topk