• 笔试题


     

    1.java中的集合类

    ArrayList是如何来实现的?

    Arraylist内部是通过一个动态数组来实现的。

    它实现了Abstractlist,因此是一个数组的队列,提供了相关的添加、删除、遍历等功能,

    也实现了RandomAcceess等接口,提供了随机访问的功能。

    也实现了clone和序列化的接口,能够被克隆,并且支持序列化的操作。

    在arraylist的内部有一个Object类型的ElementData的数组,和vector不同的是,它不是线程安全的,而vector和Copyonwritearraylist是线程安全的。

    Vector的线程安全通过了关键字synchronized来实现,而后者则通过ReentrantLock来实现,在读的效率上更高一些。

    关于Arraylist中的动态扩容操作的话:

    list默认的初始化容量大小是10,Object类型的elementObject数组,

    扩容的过程是首先ensureCapacity对原数组进行扩充,创建一个新的数组,长度为原数组的1.5倍+1,

    然后通过调用arrays类的copyof方法对数组进行拷贝,最后把新数组的地址赋给elementdata引用类即可

    Vector集合类

    也是一个自增长的数组操作,是线程安全的,通过关键字synchronized来实现的,与arraylist的区别是可以修改扩容的参数

    set集合类

    set最大的特性就是不允许元素重复,内部是通过hashmap来实现的,如果我们看jdk源码的话,set里面也维护这一份hashmap的引用,map中对应的键值对对应的键是不能重复的,这个和set集合是保持一致的,于是就利用了map中键不能重复的这个特性来实现。

    在Hashset中键就是我们要存入的对象,值则是一个Object的常量,这样呢就可以确保我们所需要存储的信息是键,而键在hashmap中是不可以重复的。

    而在hashset中判断某个键是否存在,就是通过hashmap中判断键值是否存在,存在的话,返回的是一个object类型的常量PERSENT,否则不存在的话,返回的是一个null值。

    java中的linkedlist

    与arraylist内部是动态数组来相比的话,linkedlist是以链表结构来实现的。因此对于get和set操作,arraylist要优于linkedlist,对于add和remove操作,linkedlist要有优势。

    java中linkedhashmap

    与hashmap的不同之处在于,hashmap内部也有一个Entry对象,而在LinkedHashMap内部同样有一个Entry对象,但是该entry对象继承了hashmap中的entry对象,它重新定义了数组中保存的元素Entry,该Entry除了保持Entry对象引用外,还保存了它上一个元素Before和下一个元素After的引用操作,从而在哈希表的基础上构成了双向链接表的操作。

    java中的treeset

    java中的TreeSet和LinkedHashSet操作,首先两者放入的实体都是有序的,而LinkedHashSet支持放入的顺序操作,而TreeSet支持自动义的顺序,因此放入的实体需要自动的实现Comparable接口中的compareTo方法。至于TreeSet是底层使用了NavigableMap该map类操作。

     

    java中线程安全的map

    hashtable,全锁类型,性能不是很高,而Concurrenthashmap,采用了局部锁的思想,内部维护另一个Segment类型的一个数组,其实一个segment就是一个hashtable的结构,segment内部又维护了一个链表的数据结构操作,因此我们了解到Concurrenthashmap定位一个元素需要两步操作,先定位到某个segment,然后定位到segment中的hashtable操作。

     

    Java中的TreeMap

    TreeMap用的哪种数据结构,时间复杂度多少

    :TreeMap采用红黑树算法进行实现,又称红-黑二叉树。基本规则:

    u  每个节点都只能是红色或者黑色

    u  根节点是黑色

    u  每个叶节点(NIL节点,空节点)是黑色的。

    u  如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。

    u  从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

    Treep操作,比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,其检索效率O(log n)

     

    2.java中锁都有哪些

    ReadLock、ReadWriteLock、WriteLock、ReentrantLock、ReentrantReadWriteLock

    关键字synchronized

    首先java中内置的关键字synchronized

        1.首先执行代码块的时候,获取该代码块的锁,执行完后,然后释放该锁

        2.线程执行异常的话,JVM会让线程自动释放该锁操作

     

        1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

     

      2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

     

     1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

     

      2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

     

      3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

     

      4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

     

      5)Lock可以提高多个线程进行读操作的效率。

     

      在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

    可重入锁的概念

    假如一个方法中调用了另一个方法,两个方法都加入了锁,因此需要重新申请锁,假如此时该锁已经被第一个方法占用的话,则会导致堵塞的操作,那么此时采取可重入锁的话,就可以解决这个问题。java中的内置关键字和lock都具有可重入性,因此不会出现上述问题。

    还有一些类似的公平锁的问题,意思是当多个线程在等待的时候,等待最长时间的线程会优先获得该锁,这就是公平锁。

     

     

    java中都用过哪些队列

    ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue

    ArrayBlockingQueue:是基于数组来实现的,必须需要指定数组的大小

        LinkedBlockingQueue:是以链表来实现的,如果不指定大小的话,默认是Integer.MAX_VALUE

        PriorityBlockingQueue:上面两个都是先进先出的队列,这个是按照元素的优先级进行排序的

        DelayQueue:是一种阻塞延时的队列操作

    下面是常见的队列的阻塞和非阻塞的方法

     1.boolean offer():将Object类型的对象放入到队列中,如果队列有足够的空间,则返回true,否则返回false,该方法不阻塞当前的执行方法的线程,插入返回true,否则返回false

     2.offer(long timeout):只不过是在特定的时间内插入,如果还不能插入则返回false

    3.void put():把对象放置到队列中,如果没有足够的空间,则调用该线程的方法将会出于阻塞的状态,直到队列里面有空间为止

    4.Object poll():取走首尾的对象,若不能立即取走,则可以等tme参数执行的时间,取不到时返回null

     5.take:也是取走操作,但是在方法是阻塞的

     

     

    3.java中的线程

    java中的线程类都有哪些

    java中实现线程有runnable接口,thread类,其实thread类也是实现了runnable接口操作,然后实现其run方法,最后调用thread类的start方法即可

    java中提供了Executor框架,里面有newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool,无论是哪种内部都是对ThreadPoolExecutor的一种实现

    newFixedThreadPool:返回了一个core和max一样大小的线程池,并且使用了linkedblockingqueue,当任务提交频繁的时候,队列可能会迅速膨胀,消耗系统的资源

         newSingleThread:返回的是单线程池,只是简单的将线程池的数量设置为了1

         newCachedThreadPool:缓存的线程池操作

     

         JDK内置的拒绝策略如下

              AbortPolicy:直接抛出异常,阻止系统正常运行

              CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务

              DiscardoLDestPolicy:丢弃最老的一个请求

              DiscardPolicy:丢弃不能处理的任务

         线程池大小

         Ncpu=CPU的数量

         Ucpu=目标CPU的使用率,0<=Ucpu<=1

         W/C=等待时间与计算时间的比率。

         Nthreads=Ncpu*Ucpu*(1+W/C)

     

     

     

    java中线程安全的集合

    像Vector、CopyOnWriteArraySet,CopyOnWriteArrayList都是线程安全的,首先CopyOnWriteArraySet不像hashSet内部是通过hashMap来实现的,而他是通过CopyOnWriteArrayList来实现的。

    CopyOnWriteArrayList内部线程安全的机制:volatile关键字和互斥锁

    CopyOnWriteArraylist的动态数组机制,它内部有一个Volatile数组来保持数据,在添加、修改、删除数据的时候,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给volatile数组。

     

         如果要看CopyOnWriteArraylist的低层源码的话,就会发现,它不想vector用全加锁的方式,读也加锁,写也加锁。而是只是对写加了可重入锁,在写对象的时候,总是先获得锁,然后获得对象的一个副本,进行修改,最后将副本写回操作。

     

        CopyOnWritearraylist的线程安全的机制,内部是通过volatile和互斥锁来实现的,首先是通过volatile数组来保存数据,这样的话,每一个数组来读取volatile数组的时候,总能看到其他线程对该volatile变量最后的写入,因此通过volatile数组,就提供了读取的数据总是最新额操作。而在写或者修改的时候,采用了互斥锁的机制,首先会获取互斥锁,修改完毕后,先将数据更新到volattile数组中,然后在释放互斥锁。这样就达到了保护数据的目的

     

     

     

    java中的线程安全的原子类操作

    AtomicInteger、Atomiclong、AtomicBoolean,内部通过自旋锁来实现的。互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。也就是内部通过一个for循环一直在调度。

    下面是它常用的一些方法

    System. out.println(index.getAndSet(11));//设置新值,并返回旧值

              System. out.println(index.compareAndSet(34, 45));//如果当前值为34,则设置为45

              System. out.println(index.getAndIncrement());//当前值加1,并返回旧值

              System. out.println(index.getAndDecrement());//当前值减一,并返回旧值

              System. out.println(index.getAndAdd(45));//当前值增加45,并返回旧值

              System. out.println(index.incrementAndGet());//当前值加1,并返回新值

              System. out.println(index.decrementAndGet());//当前值减一,并返回新值

              System. out.println(index.addAndGet(12));//当前值加12,并返回新值

     

     

    4.StringBuffer和StringBuilder

    1. java中的stringbuilder和stringbuffer
    2. StringBuffer是线程安全的,所以当它作为成员变量时不存在线程安全问题。StringBuilder是线程不安全的,在作为成员变量时有可能出现线程不安全问题。如果在方法内部使用一个这样的变量,用StringBuilder效率会更高,因为首先StringBuilder没有加锁,其次方法内部不存在线程安全问题。Stringbuffer对内部的方法都加了锁的操作  public synchronized int length();内部通过system.arraycopy函数来完成了字符的拷贝工作。StringBuilder和StringBuffer都继承自AbstractStringBuilder该类,内部是通过char数组来保存append的字符串的,当append字符产超过char数组的时候,会自动的进行扩容操作,扩容完毕后,然后通过System.copyof方法来复制到新的char数组中去,所以这边需要注意,因为每次这边都需要重新申请内存的操作。

     

    5.java中的nio操作

       旧的io操作是基于流的形式的,而在NIO中是基于块的形式,也就是把数据线读入到缓冲区中,缓冲区是一块连续的内存块,然后在读出到通道channel中。

    常用的方法如下:

              buffer.rewind():将position重置为0,并清除标志位,但是并不清除buffer中的数据,如果此时重新读入的话,会覆盖原有的

              buffer.clear():只不过在rewind的基础上,将limit重置为了capacity的大小

              buffer.reset():重置标志位的操作

              buffer.mark():设置标志位的

              buffer.duplicate:复制缓冲区,内容是共享的,但是各自维护自己的position、limit和mark操作

              ByteBuffer. allocateDirect(1024);可以直接访问系统物理内存的类,普通的bytebuffer仍旧在堆上分配,其内存大小受到jvm的影响,而allocateDirect直接在物理内存中分配,并不占用堆空间,并且低层使用更接近操作系统的方法,执行起来更快。

     

    java中的位运算

    左移运算符,num << 1,相当于num乘以2,左移动几就代表乘以2的几次方

    右移运算符,num >> 1,相当于num除以2

     

     

     

     

    7.如何判断两个整形的二维数组是否相等?

    重写equals 方法,在equals方法如果两个数组里的数据完全相同,顺序一样,就返回true。

       8.Transient 变量关键字 ?

               java的关键字,变量修饰符。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。

     

     

    9.java中的异常有哪些?

    Throwable 是所有 Java 程序中错误处理的父类 ,有两种资类: Error 和 Exception 。

          Error :表示由 JVM 所侦测到的无法预期的错误,由于这是属于 JVM 层次的严重错误 ,导致 JVM 无法继续执行,因此,这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息。

    Exception :表示可恢复的例外,这是可捕捉到的。

    Java 提供了两类主要的异常 :runtime exception 和 checked exception 。 checked 异常也就是我们经常遇到的 IO 异常,以及 SQL 异常都是这种异常。 对于这种异常, JAVA 编译器强制要求我们必需对出现的这些异常进行 catch 。所以,面对这种异常不管我们是否愿意,只能自己去写一大堆 catch 块去处理可能的异常。

    但是另外一种异常: runtime exception ,也称运行时异常,我们可以不处理。当出现这样的异常时,总是由虚拟机 接管。比如:我们从来没有人去处理过 NullPointerException 异常,它就是运行时异常,并且这种异常还是最常见的异常之一。

     

     

     

    10.如果一个线程想返回参数,怎么处理

    Runnable和Callable的区别是,

    (1)Callable规定的方法是call(),Runnable规定的方法是run().

    (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得

    (3)call方法可以抛出异常,run方法不可以

    (4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

     

     

     

    11.如何理解java中volatile的关键字

         从java内存模型上进行分析,每个线程执行的时候都有有线程私有的内存空间,在内存空间里面会有局部变量的副本进行操作,而如果通过volatile来修饰的话,代表该线程执行完毕后,立即刷到主内存中去。

     

    12.什么是线程,线程和进程的区别是什么

        线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。Java在语言层面对多线程提供了卓越的支持,它也是一个很好的卖点。欲了解更多详细信息请点击这里

        线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。更多详细信息请点击这里

     

     

    13.谈一下threadLocal的理解

         ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。

     

     

     

    14.关于Java类加载双亲委派机制的思考(附一道面试题)

    1.      JVM预定义的三种类型类加载器:
    • 启动(Bootstrap)类加载器:是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
    • 标准扩展(Extension)类加载器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
    • 系统(System)类加载器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

    除了以上列举的三种类加载器,还有一种比较特殊的类型 — 线程上下文类加载器。

    1. 双亲委派机制描述
      某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

     

    几点思考

    1. Java虚拟机的第一个类加载器是Bootstrap,这个加载器很特殊,它不是Java类,因此它不需要被别人加载,它嵌套在Java虚拟机内核里面,也就是JVM启动的时候Bootstrap就已经启动,它是用C++写的二进制代码(不是字节码),它可以去加载别的类。

    这也是我们在测试时为什么发现System.class.getClassLoader()结果为null的原因,这并不表示System这个类没有类加载器,而是它的加载器比较特殊,是BootstrapClassLoader,由于它不是Java类,因此获得它的引用肯定返回null。

    1. 委托机制具体含义
      当Java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?
    • 首先当前线程的类加载器去加载线程中的第一个类(假设为类A)。
      注:当前线程的类加载器可以通过Thread类的getContextClassLoader()获得,也可以通过setContextClassLoader()自己设置类加载器。
    • 如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器去加载类B。
    • 还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
    1. 委托机制的意义 — 防止内存中出现多份同样的字节码
      比如两个类A和类B都要加载System类:
    • 如果不用委托而是自己加载自己的,那么类A就会加载一份System字节码,然后类B又会加载一份System字节码,这样内存中就出现了两份System字节码。
    • 如果使用委托机制,会递归的向父类查找,也就是首选用Bootstrap尝试加载,如果找不到再向下。这里的System就能在Bootstrap中找到然后加载,如果此时类B也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回内存中的System即可而不需要重新加载,这样内存中就只有一份System的字节码了。

     

    一道面试题

    • 能不能自己写个类叫java.lang.System?

    答案:通常不可以,但可以采取另类方法达到这个需求。
    解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。

    但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

     

     

     

    ==============================================================================================

     

     

     hashCode和identityHashCode的区别

    I、 hashCode()方法是Object类下面的一个方法,供继承类重写,根据对象内存地址计算哈希值,

    String类重写了hashCode方法,并改为根据字符序列来计算哈希值

    III、identityHashCode()方法是System类中的静态方法,根据对象内存地址来计算哈希值

    方法示例:

    public static void main(String[] args)

    {

    //下面程序中s1和s2是两个不同对象

    String s1 = newString("Hello");

    String s2 =newString("Hello");

    //String类重写了Object类的hashCode方法——改为根据字符序列计算hashCode值,

    //因为s1和s2的字符序列相同,所以它们的hashCode方法返回值相同

    System.out.println(s1.hashCode() +"----" + s2.hashCode());

    //s1和s2是不同的字符串对象,所以它们的identityHashCode值不同,

    //identityHashCode是根据对象的地址计算得到的,所以任何两个不同的对象的

    //identityHashCode值总是不相等

    System.out.println(System.identityHashCode(s1)+ "----"

    +System.identityHashCode(s2));

    //s3和s4是相同的字符串对象,所以它们的identityHashCode值相同

    String s3 ="Java";

    String s4 ="Java";

    System.out.println(System.identityHashCode(s3)+"----"

    +System.identityHashCode(s4));}

    输出:

    69609650----69609650

    13078969----3154093

    28399250----28399250

     

     ========================

    一个接口可以继承多个接口.
    interface C extends A, B {}是可以的.

    一个类可以实现多个接口:
    class D implements A,B,C{}

    但是一个类只能继承一个类,不能继承多个类
    class B extends A{}

    在继承类的同时,也可以继承接口:
    class E extends D implements A,B,C{}
    这也正是选择用接口而不是抽象类的原因

    ==========================================

    others: 两个有序的List合并 并保持有序   -------   http://blog.csdn.net/a_lele123/article/details/46982727

    =============
  • 相关阅读:
    10年学到的编程经验总结
    高效阅读源代码指南
    一行代码解决各种IE兼容问题,IE6,IE7,IE8,IE9,IE10
    如何用git将项目代码上传到github
    关于[].slice.call(arguments, 1) 的思考
    前端笔试题(一)
    2017年第一波JavaScript前端面试题
    2017前端面试题之综合篇(1)
    2017前端面试题之Js篇(1)
    前后端分离后各自职责
  • 原文地址:https://www.cnblogs.com/xingzc/p/5748970.html
Copyright © 2020-2023  润新知