zoukankan      html  css  js  c++  java
  • 面试题总结-Java部分

    1 集合

    1.1 hashmap原理

    HashMap是基于哈希表实现的,每一个元素是一个key-value对,实现了Serializable、Cloneable接口,允许使用null值和null键。不保证映射的顺序,内部通过单链表解决冲突问题,容量超过(容量*加载因子)时,会自动增长。(除了不同步和允许使用null之外,HashMap类与Hashtable大致相同)。HashMap不是线程安全的。

    1.2 ConcurrentHashMap实现原理

    首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段(Segment)数据的时候,其他段的数据也能被其他线程访问。 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。 Segment继承了ReetrantLock,表示Segment是一个可重入锁,因此ConcurrentHashMap通过可重入锁对每个分段进行加锁。

    1.3 Hashmap hashtable区别

    • 继承的父类不同
      Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
    • 线程安全性不同
      Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。
    • 是否提供contains方法
      HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。
    • key和value是否允许null值
      Hashtable中,key和value都不允许出现null值。HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。
    • 两个遍历方式的内部实现上不同
      Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
    • hash值不同
      哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
    • 内部实现使用的数组初始化和扩容方式不同
      HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

    1.4 hashmap的resize

    当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
    那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.751000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

    2 数据结构

    2.1 链表和数组内存中区别

    • 1.占用的内存空间
      链表存放的内存空间可以是连续的,也可以是不连续的,数组则是连续的一段内存空间。一般情况下存放相同多的数据数组占用较小的内存,而链表还需要存放其前驱和后继的空间。
    • 2.长度的可变性
      链表的长度是按实际需要可以伸缩的,而数组的长度是在定义时要给定的,如果存放的数据个数超过了数组的初始大小,则会出现溢出现象。
    • 3.对数据的访问
      链表方便数据的移动而访问数据比较麻烦;数组访问数据很快捷而移动数据比较麻烦。 链表和数组的差异决定了它们的不同使用场景,如果需要很多对数据的访问,则适合使用数组;如果需要对数据进行很多移位操作,则设和使用链表。

    3 线程

    3.1 synchronized使用

    • 修饰一个方法
    • 修饰一个代码块
    • 修改一个静态的方法
    • 修饰一个类

    3.2 synchronized与Lock的区别

    • Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
    • synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
    • Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
    • 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
    • Lock可以提高多个线程进行读操作的效率。
    • 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择

    3.3 volatile

    volatile作为java中的关键词之一,用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。volatile具有可见性、有序性,不具备原子性。

    3.4 线程等待

    在Object.java中,定义了wait(), notify()和notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。

    3.5 ThreadPoolExecutor执行顺序

    • 当线程数小于核心线程数时,创建线程。
    • 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
    • 当线程数大于等于核心线程数,且任务队列已满
      • 若线程数小于最大线程数,创建线程
      • 若线程数等于最大线程数,抛出异常,拒绝任务

    3.6 ThreadPoolExecutor参数默认值

    • corePoolSize=1
    • queueCapacity=Integer.MAX_VALUE
    • maxPoolSize=Integer.MAX_VALUE
    • keepAliveTime=60s
    • allowCoreThreadTimeout=false
    • rejectedExecutionHandler=AbortPolicy()

    3.7 线程池类型:

    • newCachedThreadPool:

      • 底层:
        返回ThreadPoolExecutor实例,corePoolSize为0;maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为60L;unit为TimeUnit.SECONDS;workQueue为SynchronousQueue(同步队列)
      • 通俗:
        当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。
      • 适用:
        执行很多短期异步的小程序或者负载较轻的服务器
    • newFixedThreadPool:

      • 底层:
        返回ThreadPoolExecutor实例,接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为:new LinkedBlockingQueue() 无解阻塞队列
      • 通俗:
        创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
      • 适用:
        执行长期的任务,性能好很多
    • newSingleThreadExecutor:

      • 底层:
        FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例,corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为:new LinkedBlockingQueue() 无解阻塞队列
      • 通俗:
        创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
      • 适用:
        一个任务一个任务执行的场景
    • newScheduledThreadPool:

      • 底层:
        创建ScheduledThreadPoolExecutor实例,corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为:new DelayedWorkQueue() 一个按超时时间升序排序的队列
      • 通俗:
        创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构
      • 适用:
        周期性执行任务的场景

    4 Java虚拟机

    4.1 工作原理

    从宏观上介绍一下Java虚拟机的工作原理。首先Java源文件经过前端编译器(javac或ECJ)将.java文件编译为Java字节码文件,然后JRE加载Java字节码文件,载入系统分配给JVM的内存区,然后执行引擎解释或编译类文件,再由即时编译器将字节码转化为机器码。

    4.2 运行时数据区

    程序计数器:      
    线程私有,用来指示当前线程所执行的字节码的行号,就是用来标记线程现在执行的代码的位置;
    对Java方法,它存储的是字节码指令的地址;对于Native方法,该计数器的值为空。
    栈:      
    线程私有,一个方法的执行和退出就是用一个栈帧的入栈和出栈表示的,通常我们不允许你使用递归就是因为,方法就是一个栈,太多的方法只执行而没有退出就会导致栈溢出,不过可以通过尾递归优化。栈又分为虚拟机栈和本地方法栈,一个对应Java方法,一个对应Native方法。
    堆:      
    用来给对象分配内存的,几乎所有的对象实例(包括数组)都在上面分配。它是垃圾收集器的主要管理区域,因此也叫GC堆。它实际上是一块内存区域,由于一些收集算法的原因,又将其细化分为新生代和老年代等。
    方法区:      
    方法区由多线程共享,用来存储类信息、常量、静态变量、即使编译后的代码等数据。运行时常量池是方法区的一部分,它用于存放编译器生成的各种字面量和符号引用,比如字符串常量等。
    复制代码

    4.3 是否回收判断

    • 引用记数法
      给对象添加一个引用计数器,被引用时计数器加1,引用失效时减1。 这种方法不常用,因为它难以解决两个变量相互引用的问题。
    • 可达性分析
      通过一系列GC Roots的对象作为起始点,从节点向下搜索, 当一个对象没有任何一条可到GC Roots的引用链,则该对象可回收。

    4.4 垃圾回收算法

    • 标记-清除算法,这种算法直接在内存中把需要回收的对象“抠”出来。 好好的内存被它搞成了马蜂窝,所以效率不高,清除之后会产生内容碎片,造成内存不连续,当分配较大内存对象时可能会因内存不足而触发垃圾收集动作。

    • 复制算法:将内存分成两块,一次只在一块内存中进行分配,垃圾回收一次之后, 就将该内存中的未被回收的对象移动到另一块内存中,然后将该内存一次清理掉。 比如将内存分成A和B,先在A中分配,当垃圾回收的时候把A中需要回收的内存清理掉,然后把不需要清理的所有对象复制到B里面。 复制算法常被用来回收新生代,而且分配空间也不是1:1,而是较大的Eden空间和较小的Survivor空间。在HotSpot中,其比例是8:1。

      • 标记整理算法:类似于标记-清除算法,只是回收了之后,它要对内存空间进行整理,以使得剩余的对象占用连续的存储空间。

      • 分代收集算法:上面是三种基本的垃圾回收算法,但实际上,我们通常根据对象存活周期的不同将内存划分成几块,然后根据其特点采用不同的回收算法。

    4.5 内存分配与回收策略

    对象内存分配,往大方向上讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配,少数情况下可能直接分配在老年代。

    • 对象优先在Eden分配:
      大多数情况下,对象在新生代Eden区非中分配,当Eden区没有足够空间时,虚拟机发起一次Minor GC。

    • 大对象直接进入老年代:
      大对象指需要大量连续内存空间的Java对象,比如很大的数组或者字符串。经常出现大对象会导致内存还有不少空间时就提前触发垃圾收集来获取足够的连续空间来安置它们。 虚拟机提供了-XX:PretenureSizeThreshold参数,当对象的大小大于它的值的时候将直接分配在老年代。这样做是为了避免在Eden区和Survivor区之间发生大量的内存复制。

    • 长期存活对象将进入老年代:
      若对象出生在Eden区并经过一次Minor GC后仍然存活,并且能被Survivor容纳,将被移动到Survivor空间中,并且对象年龄将加1。 在Survivor中,每熬过一次Minor GC,则年龄加1,当年龄达到一定程度时(默认15岁),就会被晋升到老年代。该年龄的阈值通过参数-XX:MaxTenuringThreshold设置。

    5 HTTP

    5.1 Http连接复用原理

    当一个http请求完成后,tcp连接不会立即释放,如果有新的http请求,并且host和上次一样,那么可以复用tcp连接,省去重新连接的过程。

    5.2 Http中keep alive与TCP区别

    • HTTP Keep-Alive
      在HTTP 1.0以前,每个http请求都要求打开一个TCP socket连接,并且使用一次之后就断开这个TCP连接,这会导致频繁地创建和销毁TCP。HTTP 1.1通过使用keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。
    • TCP KEEPALIVE
      这是TCP协议栈为了检测连接状况的保活机制,当TCP空闲一定时间后会发送心跳包给对方,如果对端回复ACK后,就认为对端是存活的,重置定时器;如果对端回复RST应答(对端崩溃或者其他原因,导致的复位),那就关闭该连接;如果对端无任何回应,那就会出发超时重传,直到达到重传的次数,如果对端依然没有回复,那么就关闭该连接。

    HTTP位于网络协议栈的应用层,而TCP位于网络协议栈的传输层,两者的KEEP-ALIVE虽然名称相同,但是作用不同。HTTP是为了重用TCP,避免每次请求,都重复创建TCP;而TCP的KEEP-ALIVE是一种保活机制,检测对端是否依然存活。

    6 git

    • git cherry-pick
      将某一段commit粘贴到另一个分支上
    • git revert
      通过反做创建一个新的版本,这个版本的内容与我们要回退到的目标版本一样,但是HEAD指针是指向这个新生成的版本,而不是目标版本。 如果我们想恢复之前的某一版本(该版本不是merge类型),但是又想保留该目标版本后面的版本,记录下这整个版本变动流程,就可以用这种方法。
    • git rebase
      合并多个commit为一个完整commit
    • git reset
      修改HEAD的位置,即将HEAD指向的位置改变为之前存在的某个版本。如果想恢复到之前某个提交的版本,且那个版本之后提交的版本我们都不要了,就可以用这种方法。

    7 基本数据类型相关

    7.1 replace() replaceAll() replaceFirst()

    • replace(CharSequence target, CharSequence replacement) 用replacement替换掉target。这两个参数都是字符串
    • replaceAll(String regex, String replacement) 用replacement所有regex匹配的字符串。很明显regex参数是个正则匹配式,replacement是个字符串。
    • replaceFirst(String regex, String replacement),基本和replaceAll相同,区别是只替换第一个匹配项

    7.2 java中int与Integer用==比较详解

    ①、无论如何,Integer与new Integer不会相等。不会经历拆箱过程,因为它们存放内存的位置不一样。(要看具体位置,可以看看这篇文章:点击打开链接)

    ②、两个都是非new出来的Integer,如果数在-128到127之间,则是true,否则为false。

    ③、两个都是new出来的,则为false。

    ④、int和integer(new或非new)比较,都为true,因为会把Integer自动拆箱为int,其实就是相当于两个int类型比较。

    8 其他

    8.1 xml解析都有哪些 区别

    • SAX
      sax是一个用于处理xml事件驱动的“推”模型;
      优点:解析速度快,占用内存少,它需要哪些数据再加载和解析哪些内容。
      缺点:它不会记录标签的关系,而是需要应用程序自己处理,这样就会增加程序的负担。
    • DOM
      dom是一种文档对象模型;
      优点:dom可以以一种独立于平台和语言的方式访问和修改一个文档的内容和结构,dom技术使得用户页面可以动态的变化,如动态显示隐藏一个元素,改变它的属性,增加一个元素等,dom可以使页面的交互性大大增强。
      缺点:dom解析xml文件时会将xml文件的所有内容以文档树方式存放在内存中。
    • PULL
      pull和sax很相似,区别在于:pull读取xml文件后触发相应的事件调用方法返回的是数字,且pull可以在程序中控制,想解析到哪里就可以停止解析。 (SAX解析器的工作方式是自动将事件推入事件处理器进行处理,因此你不能控制事件的处理主动结束;而Pull解析器的工作方式为允许你的应用程序代码主动从解析器中获取事件,正因为是主动获取事件,因此可以在满足了需要的条件后不再获取事件,结束解析。pull是一个while循环,随时可以跳出,而sax不是,sax是只要解析了,就必须解析完成。)
  • 相关阅读:
    文件上传.py
    文件下载.py
    Visual SVN IIS反向代理设置
    开发者工具
    Linux
    装饰器
    面向对象
    django异常--数据库同步
    浅谈 Mysql
    python网络编程系列
  • 原文地址:https://www.cnblogs.com/twodog/p/12135052.html
Copyright © 2011-2022 走看看