Java高级工程师面试知识汇总(一)

1.String、StringBuffer与StringBuilder的区别

 解析:(1)String是字符串常量,StringBuffer是字符串变量,线程安全的,StringBuilder也是字符串变量,非线程安全的。(2)String和StringBufferd的主要性能区别在于String是不可变的对象,因此每次对String类型的值进行变更的时候就等同于生成了一个新的String对象,并将引用重新指向新的对象,因为每次生成对象都对系统性能有影响,特别是JVM的GC还要回收内存中无引用的对象。StringBuffer则不会产生新的对象,所以字符串的值经常改变的情况下推荐使用StringBuffer。

2.Java是如何实现跨平台的

 解析:通过JVM实现,jVM也是一个软件,不同的平台有不同的版本。Java程序从源文件创建到程序运行要经过两大步骤:1、源文件由编译器编译成字节码;2、字节码由JVM解释运行。不同平台下编译生成的字节码是一样的,但是由JVM翻译成的机器码却不一样。

3.Java线程有哪些状态,这些状态之间是如何转化的

 解析:(1)新建(NEW):新创建了一个线程对象。(2)可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。(3)运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。(4)阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种: 

(一)等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二)同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

(5)死亡(DEAD):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

4.List接口、Set接口和Map接口的区别

 解析:(1)List和Set接口继承自Collection接口,而Map不是继承的Collection接口;(2)List元素有放入顺序,可重复,接口有三个实现类:LinkedList:基于链表实现,增删快查找慢,ArrayList线程不安全,效率高,Vector线程安全,ArrayList和Vector适合查询。(3)Set接口元素无放入顺序,不可重复,有两个实现接口:HashSet,LinkedHashSet;(3)Map接口以键值对的方式出现,有三个实现类:HashMap,HashTable,LinkedHashMap。HashMap非线程安全,高效,支持null,如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力HashTable线程安全,键值不支持null;LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。

5.Cookie和Session的区别

 解析:(1)session 在服务器端,cookie 在客户端(浏览器)。

(2)session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie 同时 session 也会失效(但是可以通过其它方式实现,比如在 url 中传递 session_id)。(3)session 可以放在文件、数据库、或内存中都可以。(4)用户验证这种场合一般会用session 。因此,维持一个会话的核心就是客户端的唯一标识,即 sessionid。

6.HashMap与ConcurrentHashMap的区别

 解析:并发编程实践中,ConcurrentHashMap是一个经常被使用的数据结构,相比于Hashtable以及Collections.synchronizedMap(),ConcurrentHashMap在线程安全的基础上提供了更好的写并发能力,但同时降低了对读一致性的要求。ConcurrentHashMap的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响,无论对于Java并发编程的学习还是Java内存模型的理解,ConcurrentHashMap的设计以及源码都值得非常仔细的阅读与揣摩。 ConcurrentHashMap具体是怎么实现线程安全的呢,肯定不可能是每个方法加synchronized,那样就变成了HashTable。 JDK6与JDK7中的实现它引入了一个“分段锁”的概念,在ConcurrentHashMap中,就是把Map分成了N个Segment,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中。JDK8中的实现它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想(JDK7与JDK8中HashMap的实现),但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类

  详见:点击打开链接
7.Java中的equals和hashCode方法详解
 解析:Object类中有两个非常重要的方法equals、hashCode,equals()方法很明显是对两个对象的地址值进行的比较(即比较引用是否相同)。String 、Math、Integer、Double等这些封装类在使用equals()方法时,已经覆盖了object类的equals()方法,进行的内容比较,而已经不再是地址的比较。当然,基本类型是进行值的比较。
hashCode()方法给对象返回一个hash code值,如果两个对象根据equals(Object)方法是相等的,那么调用二者各自的hashCode()方法必须产生同一个integer结果。

(1)相等(相同)的对象必须具有相等的哈希码(或者散列码)。

(2)如果两个对象的hashCode相同,它们并不一定是相同的对象。

8.Java中CAS算法
       解析:悲观锁会把整个对象加锁占为自有后才去做操作,乐观锁不获取锁直接做操作,然后通过一定检测手段决定是否更新数据。Synchronized互斥锁属于悲观锁。它有一个明显的缺点,它不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时间比较长,其性能开销将会变得很大。有没有办法解决这个问题?答案是基于冲突检测的乐观锁。这种模式下,已经没有所谓的锁概念了,每条线程都直接先去执行操作,计算完成后检测是否与其他线程存在共享数据竞争,如果没有则让此操作成功,如果存在共享数据竞争则可能不断地重新执行操作和检测,直到成功为止,可叫CAS自旋。
 乐观锁的核心算法是CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。

 乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:① 乐观锁只能保证一个共享变量的原子操作。如上例子,自旋过程中只能保证value变量的原子性,这时如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。② 长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。③ ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。乐观锁是对悲观锁的改进,虽然它也有缺点,但它确实已经成为提高并发性能的主要手段,而且jdk中的并发包也大量使用基于CAS的乐观锁。  

 详见:乐观的并发策略——基于CAS的自旋

9.comparable与comparator的区别  

 解析:Comparable和Comparator都是用来实现集合中元素的比较、排序的。
Comparable是在集合内部定义的方法实现的排序,位于java.lang下。
Comparator是在集合外部实现的排序,位于java.util下。
Comparable是一个对象本身就已经支持自比较所需要实现的接口,如String、Integer自己就实现了Comparable接口,可完成比较大小操作。自定义类要在加入list容器中后能够排序,也可以实现Comparable接口,即若一个类实现了Comparable 接口,实现 Comparable 接口的类的对象的 List 列表 ( 或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序:Collections.sort(list),在用Collections类的sort方法排序时若不指定Comparator,那就以自然顺序排序。所谓自然顺序就是实现Comparable接口设定的排序方式。
Comparator是一个专用的比较器,如果我们的这个类无法修改,我们又要对其进行排序或者自比较函数不能满足要求时,可写一个比较器来完成两个对象之间大小的比较。Comparator体现了一种策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。

例子:Comparable :

package collections;

public class Person1 implements Comparable<Person1>
{
    private int age;
    private String name;

    public Person1(String name, int age)
    {
        this.name = name;
        this.age = age;
    }
    @Override
    public int compareTo(Person1 o)
    {
        return this.age-o.age;
    }
    @Override 
    public String toString()
    {
        return name+":"+age;
    }
}

测试代码:

  Person1 person1 = new Person1("zzh",18);
        Person1 person2 = new Person1("jj",17);
        Person1 person3 = new Person1("qq",19);

        List<Person1> list = new ArrayList<>();
        list.add(person1);
        list.add(person2);
        list.add(person3);

        System.out.println(list);
        Collections.sort(list);
        System.out.println(list)

输出结果:

[zzh:18, jj:17, qq:19]
[jj:17, zzh:18, qq:19]

Comparetor:

 Person2 p1 = new Person2("zzh",18);
        Person2 p2 = new Person2("jj",17);
        Person2 p3 = new Person2("qq",19);
        List<Person2> list2 = new ArrayList<Person2>();
        list2.add(p1);
        list2.add(p2);
        list2.add(p3);
        System.out.println(list2);
        Collections.sort(list2,new Comparator<Person2>(){

            @Override
            public int compare(Person2 o1, Person2 o2)
            {
                if(o1 == null || o2 == null)
                    return 0;
                return o1.getAge()-o2.getAge();
            }

        });
        System.out.println(list2);
10.单例模式(线程安全)

解析:

解法一:加同步锁时,前后两次判断实例是否存在(可行)

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }
            }

        }
        return instance;
    }
}
解法二:饿汉式(推荐使用):
public class Singleton {
    private static Singleton instance=new Singleton();
    private Singleton(){
        
    }
    public static Singleton getInstance(){
        return instance;
    }
}

注解:初试化静态的instance创建一次。如果我们在Singleton类里面写一个静态的方法不需要创建实例,它仍然会早早的创建一次实例。而降低内存的使用率。缺点:没有lazyloading的效果,从而降低内存的使用率。

解法三:静态内部内(建议使用):
public class Singleton {
    private Singleton(){
        
    }
    private static class SingletonHolder{
        private final static Singleton instance=new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}
注解:定义一个私有的内部类,在第一次用这个嵌套类时,会创建一个实例。而类型为SingletonHolder的类,只有在Singleton.getInstance()中调用,由于私有的属性,他人无法使用SingleHolder,不调用Singleton.getInstance()就不会创建实例。
11. Java线程间的通信方式

 解析:①同步这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信。 ②while轮询的方式 。③wait/notify机制。要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中,线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法)。Thread.sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的。notify()方法会唤醒一个等待当前对象的锁的线程。被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁。notify()方法应该是被拥有对象的锁的线程所调用。换句话说,和wait()方法一样,notify方法调用必须放在synchronized方法或synchronized块中。

package utils;

public class WaitNotifyTest {

    private Object object=new Object();

    class WaitThread extends Thread{
        @Override
        public void run() {
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"线程等待开始");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"线程等待结束");
            }
        }
    }

    class NotifyThread extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"唤醒线程");
                object.notify();
            }
        }
    }
    public static void main(String [] args){
        WaitNotifyTest waitNotifyTest=new WaitNotifyTest();
        WaitThread waitThread=waitNotifyTest.new WaitThread();
        waitThread.start();
        NotifyThread notifyThread=waitNotifyTest.new NotifyThread();
        notifyThread.start();

    }
}

 详见:Java 多线程(七) 线程间的通信——wait及notify方法

12.spring中ApplicationContextAware接口有什么用途

   解析:当一个类实现了这个接口(ApplicationContextAware)之后,这个类就可以方便获得ApplicationContext中的所有bean。换句话说,就是这个类可以直接获取spring配置文件中,所有有引用到的bean对象。


 详见:通过ApplicationContextAware加载Spring上下文环境

13.zookeeper怎么通知客户端

解析:

  客户端与服务器通信采用tcp长连接,客户端和服务器通过心跳来保持seesion的连接。当session失效时临时节点会被删除。

  Clients会连接到某一个ZooKeeper Server上。Client和Server保持一个TCP长连接,通过该TCP长连接,Client可以发送请求,得到response,得到watch event,还有发送心跳(客户端和服务端通过心跳来保持连接,即session)。如果和Server的TCP长连接断了,那么Client就会连接到另外一个Server上。

  在ZooKeeper中,引入了Watcher机制来实现这种分布式的通知功能。ZooKeeper允许客户端向服务端注册一个Watcher监听,当服务端的一些事件触发了这个Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。

详见: Zookeeper(十)Watcher——数据变更的通知

14.dubbo支持哪些协议

 解析:Dubbo支持dubbo、rmi、hessian、http、webservice、thrift、redis等多种协议,但是Dubbo官网是推荐我们使用Dubbo协议的。dubbo协议:(1)dubbo默认采用dubbo协议,dubbo协议采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况 ;(2)他不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。rmi协议:Java标准的远程调用协议。采用JDK标准的java.rmi.*实现,采用阻塞式短连接和JDK标准序列化方式 。

   详见:Dubbo支持的协议的详解

15.nginx负载均衡算法有哪些

 解析:nginx的负载均衡策略可以划分为两大类:内置策略和扩展策略。内置策略包含加权轮询和ip hash,在默认情况下这两种策略会编译进nginx内核,只需在nginx配置中指明参数即可。扩展策略有很多,如fair、url_hash、consistent hash等,默认不编译进nginx内核。(1)轮询(默认):每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。(2)指定权重:指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。(3)ip_hash:每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。

16.volatile型变量的特殊规则

解析:关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制,但是它并不容易完全被正确、完整的理解,以至于许多程序员都不习惯去使用它,遇到需要处理多线程的问题的时候一律使用synchronized来进行同步。了解volatile变量的语义对后面了解多线程操作的其他特性很有意义。Java内存模型对volatile专门定义了一些特殊的访问规则,当一个变量被定义成volatile之后,他将具备两种特性:

  • 保证此变量对所有线程的可见性。第一保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。另外,java里面的运算并非原子操作,会导致volatile变量的运算在并发下一样是不安全的。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
  • 禁止指令重排序。重排序:类似于这样的场景:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序与程序中的执行顺序一致,在单线程中,我们是无法感知这一点的。
17.线程中join的用法
解析:Thread中,join方法的作用是调用线程等待该线程完成后,才能继续用下运行。thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

  

main是主线程,在main中创建了thread线程,在main中调用了thread.join(),那么等thread结束后再执行main代码。在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

18.java的多态

 解析:多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

   指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。

 19.Java序列化与反序列化是什么?为什么需要序列化与反序列化?如何实现Java序列化与反序列化?

 解析:把对象转换为字节序列的过程称为对象的序列化把字节序列恢复为对象的过程称为对象的反序列化。这两个过程结合起来,可以轻松地存储和传输数据。

序列化的目的:(1)以某种存储形式使自定义对象持久化;(2)将对象通过网络进行传输。

java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
  java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
  只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。
  对象序列化包括如下步骤:
  1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
  2) 通过对象输出流的writeObject()方法写对象。
  对象反序列化的步骤如下:
  1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
  2) 通过对象输入流的readObject()方法读取对象。

20.Spring事务的传播行为

 解析:@Transactional(propagation=Propagation.REQUIRED) 
如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
@Transactional(propagation=Propagation.NOT_SUPPORTED) 
容器不为这个方法开启事务
@Transactional(propagation=Propagation.REQUIRES_NEW) 
不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
@Transactional(propagation=Propagation.MANDATORY) 
必须在一个已有的事务中执行,否则抛出异常
@Transactional(propagation=Propagation.NEVER) 
必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.SUPPORTS) 
如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务。

21.Spring AOP 实现原理

 解析:AOP技术利用一种称为横切的技术,解剖封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,这样就能减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。AOP使用代理模式,动态代理实现主要是实现InvocationHandler,并且将目标对象注入到代理对象中,利用反射机制来执行目标对象的方法。

 动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。


测试:


   生成的代理对象的方法调用都会委托到InvocationHandler.invoke()方法。

 详见:Spring aop的实现原理

22.Java NIO和IO的区别

 解析:Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

 阻塞与非阻塞IOJava IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

23.java反射机制

 解析:1.反射机制概念

 在Java中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。

     2.反射的应用场合

 在Java程序中许多对象在运行是都会出现两种类型:编译时类型和运行时类型。

编译时的类型由声明对象时实用的类型来决定,运行时的类型由实际赋值给对象的类型决定 。

除此之外,程序在运行时还可能接收到外部传入的对象,该对象的编译时类型为Object,但是程序有需要调用该对象的运行时类型的方法。为了解决这些问题,程序需要在运行时发现对象和类的真实信息。然而,如果编译时根本无法预知该对象和类属于哪些类,程序只能依靠运行时信息来发现该对象和类的真实信息,此时就必须使用到反射了。

24.如何预防Mysql注入

 解析:所谓SQL注入,就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。

  防止SQL注入,我们需要注意以下几个要点:

  1.永远不要信任用户的输入。对用户的输入进行校验,可以通过正则表达式,或限制长度;对单引号和双"-"进行转换等。

  2.永远不要使用动态拼装sql,可以使用参数化的sql或者直接使用存储过程进行数据查询存取。

  3.永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接。

  4.不要把机密信息直接存放,加密或者hash掉密码和敏感的信息。

  5.应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装

  6.sql注入的检测方法一般采取辅助软件或网站平台来检测。
25.sendRedirect, foward区别

 解析:foward是服务器端控制页面转向,在客户端的浏览器地址中不会显示转向后的地址;sendRedirect则是完全的跳转,浏览器中会显示跳转的地址并重新发送请求链接。

  原理:forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后再将这些内容返回给浏览器,浏览器根本不知道服务器发送的这些内容是从哪来的,所以地址栏还是原来的地址。redirect是服务器端根据逻辑,发送一个状态码,告诉浏览器重新去请求的那个地址,浏览器会用刚才的所有参数重新发送新的请求。

26.jvm性能调优都做了什么

 解析:JVM性能调优有很多设置,这个参考JVM参数即可。主要调优的目的:

  1. 控制GC的行为。GC是一个后台处理,但是它也是会消耗系统性能的,因此经常会根据系统运行的程序的特性来更改GC行为。

  2. 控制JVM堆栈大小。一般来说,JVM在内存分配上不需要你修改,但是当你的程序新生代对象在某个时间段产生的比较多的时候,就需要控制新生代的堆大小。同时,还要需要控制总的JVM大小避免内存溢出。

  3. 控制JVM线程的内存分配。如果是多线程程序,产生线程和线程运行所消耗的内存也是可以控制的,需要通过一定时间的观测后,配置最优结果。

27.static关键字修饰类用什么用途
 解析:java里面static一般用来修饰成员变量或函数。但有一种特殊用法是用static修饰内部类,普通类是不允许声明为静态的,只有内部类才可以。被static修饰的内部类可以直接作为一个普通类来使用,而不需实例一个外部类。非静态内部类是附属在外部类对象上的,需要先实例化一个外部类的对象,通过外部类对象才能实例化非静态内部类;而静态内部类可以看做是直接附属在外部类上的,这个静态代表附属体是外部类,而不是外部类实例。
 详见:static修饰类
28.写代码分别使得JVM的堆、栈和持久代发生内存溢出
 解析:在JAVA中,可以使用关键字new来创建Java对象。例如,ArrayList list = new ArrayList();实际上,在创建完上面的一个对象后,在JVM中,会把new出来的对象存放在堆内存中,同时,在方法栈中存放着对象的引用关系。如果想要堆溢出,比较简单,可以循环创建对象或大的对象;如果想要栈溢出,可以递归调用方法,这样随着栈深度的增加,JVM 维持着一条长长的方法调用轨迹,直到内存不够分配,产生栈溢出。
29.画出最熟悉的三个设计模式的类图
 解析:单例模式:
工厂方法模式:


代理模式:


 详见:java设计模式之代理模式

30.hashmap原理

 解析:hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,这个Entry对象就会存储在前面提到的Entry数组table中,Entry对象将会存放在具体哪个位置(在table中的精确位置)由key的hashcode()方法计算出来的hash值来决定。hash值用来计算key在Entry数组的索引。如果两个元素有相同的hashcode,即两个key有相同的hash值(也叫冲突),它们会被放在同一个索引上。问题出现了,该怎么放呢?原来它是以链表(LinkedList)的形式来存储的。

  • 如果在刚才计算出来的索引位置没有元素,直接把Entry对象放在那个索引上。
  • 如果索引上有元素,然后会进行迭代,一直到Entry->next是null。当前的Entry对象变成链表的下一个节点。
  • 如果我们再次放入同样的key会怎样呢?逻辑上,它应该替换老的value。事实上,它确实是这么做的。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)),如果这个方法返回true,它就会用当前Entry的value来替换之前的value。
  • get方法,根据key的hashcode()方法被调用,然后计算hash值,根据hash值找到要获取的Entry对象在table数组中的精确的位置(table数组的索引)之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。
HashMap有一个叫做Entry的内部类,它用来存储key-value对。 Entry对象是存储在一个叫做table的Entry数组中。 table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。 key的hashcode()方法用来找到Entry对象所在的桶。 如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。 key的equals()方法用来确保key的唯一性。

 详见:Hashmap的工作原理

31.线程池原理

 解析:最简单的服务器工作模型:服务器每当接收到一个客户端请求时就创建一个线程为其服务。这种模式理论上可以工作的很好,但实际上会存在一些缺陷,服务器应用程序中经常出现的情况是单个客户端请求处理的任务很简单但客户端的数目却是巨大的,因此服务器在创建和销毁线程所花费的时间和系统资源可能比处理客户端请求处理的任务花费的时间和资源更多。

 线程池技术就是为了解决上述问题而出现的。合理的使用线程池便可重复利用已创建的线程,以减少在创建线程和销毁线程上花费的时间和资源。除此之外,线程池在某些情况下还能动态的调整工作线程的数量,以平衡资源消耗和工作效率。同时线程池还提供了对池中工作线程进行统一的管理的相关方法。

 线程池的工作模型主要两部分组成,一部分是运行Runnable的Thread对象(工作线程),另一部分就是阻塞队列(执行任务的线程队列)。

 由线程池创建的Thread对象其内部的run方法会通过阻塞队列的take方法获取一个Runnable对象,然后执行这个Runnable对象的run方法(即,在Thread的run方法中调用Runnable对象的run方法)。当Runnable对象的run方法执行完毕以后,Thread中的run方法又循环的从阻塞队列中获取下一个Runnable对象继续执行。这样就实现了Thread对象的重复利用,也就减少了创建线程和销毁线程所消耗的资源。

 当需要向线程池提交任务时会调用阻塞队列的offer方法向队列的尾部添加任务。提交的任务实际上就是Runnable对象。

 详见:线程池原理

猜你喜欢

转载自blog.csdn.net/xiaoxiangzi520/article/details/78839131