美文网首页java基础或面试问题
阿里巴巴高级Java面试题

阿里巴巴高级Java面试题

作者: 壹点零 | 来源:发表于2017-12-08 13:45 被阅读52次

    参考资料:https://github.com/landy8530/interview

    一、java事件机制

    java事件机制包括三个部分:事件、事件监听器、事件源。

    1、事件。一般继承自java.util.EventObject类,封装了事件源对象及跟事件相关的信息。
    com.javaedu.event.CusEvent类

    package com.javaedu.event;  
      
    import java.util.EventObject;  
      
    /** 
     * 事件类,用于封装事件源及一些与事件相关的参数. 
     * @author Eric 
     */  
    public class CusEvent extends EventObject {  
        private static final long serialVersionUID = 1L;  
        private Object source;//事件源  
          
        public CusEvent(Object source){  
            super(source);  
            this.source = source;  
        }  
      
        public Object getSource() {  
            return source;  
        }  
      
        public void setSource(Object source) {  
            this.source = source;  
        }  
    }  
    

    2、事件监听器。实现java.util.EventListener接口,注册在事件源上,当事件源的属性或状态改变时,取得相应的监听器调用其内部的回调方法。
    com.javaedu.event.CusEventListener类

    package com.javaedu.event;  
     
    import java.util.EventListener;  
      
    /** 
     * 事件监听器,实现java.util.EventListener接口。定义回调方法,将你想要做的事 
     * 放到这个方法下,因为事件源发生相应的事件时会调用这个方法。 
     * @author Eric 
     */  
    public class CusEventListener implements EventListener {  
        
       //事件发生后的回调方法  
       public void fireCusEvent(CusEvent e){  
           EventSourceObject eObject = (EventSourceObject)e.getSource();  
            System.out.println("My name has been changed!");  
            System.out.println("I got a new name,named \""+eObject.getName()+"\"");    
        }  
    }  
    

    3、事件源。事件发生的地方,由于事件源的某项属性或状态发生了改变(比如BUTTON被单击、TEXTBOX的值发生改变等等)导致某项事件发生。换句话说就是生成了相应的事件对象。因为事件监听器要注册在事件源上,所以事件源类中应该要有盛装监听器的容器(List,Set等等)。
    com.javaedu.event.EventSourceObject类

    package com.javaedu.event;  
    
    import java.util.HashSet;  
    import java.util.Iterator;  
    import java.util.Set;  
      
    /** 
     * 事件源. 
    * @author Eric 
    */  
    public class EventSourceObject {  
        private String name;  
        //监听器容器  
       private Set<CusEventListener> listener;  
       public EventSourceObject(){  
            this.listener = new HashSet<CusEventListener>();  
            this.name = "defaultname";  
        }  
        //给事件源注册监听器  
       public void addCusListener(CusEventListener cel){  
            this.listener.add(cel);  
        }  
        //当事件发生时,通知注册在该事件源上的所有监听器做出相应的反应(调用回调方法)  
        protected void notifies(){  
            CusEventListener cel = null;  
            Iterator<CusEventListener> iterator = this.listener.iterator();  
            while(iterator.hasNext()){  
                cel = iterator.next();  
                cel.fireCusEvent(new CusEvent(this));  
            }  
       }  
        public String getName() {  
           return name;  
        }  
       //模拟事件触发器,当成员变量name的值发生变化时,触发事件。  
        public void setName(String name) {  
            if(!this.name.equals(name)){  
                this.name = name;  
                notifies();  
            }        
        }  
    } 
    

    下面是主方法类
    com.javaedu.event.MainTest类

    package com.javaedu.event;  
      
    public class MainTest {  
     
        /** 
         * @param args 
         */  
        public static void main(String[] args) {  
           EventSourceObject object = new EventSourceObject();  
           //注册监听器  
            object.addCusListener(new CusEventListener(){  
               @Override  
                public void fireCusEvent(CusEvent e) {  
                   super.fireCusEvent(e);  
                }  
            });  
           //触发事件  
            object.setName("eric");  
       }  
    }  
    

    二、Java线程池使用说明

    一简介
    线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在jdk1.5之后这一情况有了很大的改观。Jdk1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用。为我们在开发中处理线程的问题提供了非常大的帮助。

    二:线程池
    线程池的作用:
    线程池作用就是限制系统中执行线程的数量。
    根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

    为什么要用线程池:
    1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
    Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
    比较重要的几个类:

    说明
    ExecutorService 真正的线程池接口
    ScheduledExecutorService 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题
    ThreadPoolExecutor ExecutorService的默认实现
    ScheduledThreadPoolExecutor 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现

    要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

    1. newSingleThreadExecutor
      创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
      2.newFixedThreadPool
      创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
    2. newCachedThreadPool
      创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
      那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
      4.newScheduledThreadPool
      创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

    实例
    1:newSingleThreadExecutor
    MyThread.java

    Public class MyThread extends Thread {
        @Override
        publicvoid run() {
            System.out.println(Thread.currentThread().getName() + "正在执行。。。");
        }
    }
    

    TestSingleThreadExecutor.java

    Public class TestSingleThreadExecutor {
        publicstaticvoid main(String[] args) {
            //创建一个可重用固定线程数的线程池
            ExecutorService pool = Executors. newSingleThreadExecutor();
            //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
            Thread t1 = new MyThread();
            Thread t2 = new MyThread();
            Thread t3 = new MyThread();
            Thread t4 = new MyThread();
            Thread t5 = new MyThread();
            //将线程放入池中进行执行
            pool.execute(t1);
            pool.execute(t2);
            pool.execute(t3);
            pool.execute(t4);
            pool.execute(t5);
            //关闭线程池
            pool.shutdown();
        }
    }
    

    输出结果

    pool-1-thread-1正在执行。。。
    pool-1-thread-1正在执行。。。
    pool-1-thread-1正在执行。。。
    pool-1-thread-1正在执行。。。
    pool-1-thread-1正在执行。。。
    

    2: newFixedThreadPool
    TestFixedThreadPool.Java

    publicclass TestFixedThreadPool {
        publicstaticvoid main(String[] args) {
            //创建一个可重用固定线程数的线程池
            ExecutorService pool = Executors.newFixedThreadPool(2);
            //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
            Thread t1 = new MyThread();
            Thread t2 = new MyThread();
            Thread t3 = new MyThread();
            Thread t4 = new MyThread();
            Thread t5 = new MyThread();
            //将线程放入池中进行执行
            pool.execute(t1);
            pool.execute(t2);
            pool.execute(t3);
            pool.execute(t4);
            pool.execute(t5);
            //关闭线程池
            pool.shutdown();
        }
    }
    

    输出结果

    pool-1-thread-1正在执行。。。
    pool-1-thread-2正在执行。。。
    pool-1-thread-1正在执行。。。
    pool-1-thread-2正在执行。。。
    pool-1-thread-1正在执行。。。
    

    3: newCachedThreadPool
    TestCachedThreadPool.java

    publicclass TestCachedThreadPool {
        publicstaticvoid main(String[] args) {
            //创建一个可重用固定线程数的线程池
            ExecutorService pool = Executors.newCachedThreadPool();
            //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
            Thread t1 = new MyThread();
            Thread t2 = new MyThread();
            Thread t3 = new MyThread();
            Thread t4 = new MyThread();
            Thread t5 = new MyThread();
            //将线程放入池中进行执行
            pool.execute(t1);
            pool.execute(t2);
            pool.execute(t3);
            pool.execute(t4);
            pool.execute(t5);
            //关闭线程池
            pool.shutdown();
        }
    }
    

    输出结果:

    pool-1-thread-2正在执行。。。
    pool-1-thread-4正在执行。。。
    pool-1-thread-3正在执行。。。
    pool-1-thread-1正在执行。。。
    pool-1-thread-5正在执行。。。
    

    4: newScheduledThreadPool
    TestScheduledThreadPoolExecutor.java

    publicclass TestScheduledThreadPoolExecutor {
        publicstaticvoid main(String[] args) {
            ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(1);
            exec.scheduleAtFixedRate(new Runnable() {//每隔一段时间
                          @Override
                          publicvoid run() {
                               //throw new RuntimeException();
                               System.out.println("================");
                          }
                      }, 1000, 5000, TimeUnit.MILLISECONDS);
            exec.scheduleAtFixedRate(new Runnable() {//每隔一段时间打印系统时间,证明两者是互不影响的
                          @Override
                          publicvoid run() {
                               System.out.println(System.nanoTime());
                          }
                      }, 1000, 2000, TimeUnit.MILLISECONDS);
        }
    }
    

    输出结果

    ================
    8384644549516
    8386643829034
    8388643830710
    ================
    8390643851383
    8392643879319
    8400643939383
    

    三:ThreadPoolExecutor详解
    ThreadPoolExecutor的完整构造方法的签名是:ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) .
    corePoolSize - 池中所保存的线程数,包括空闲线程。
    maximumPoolSize-池中允许的最大线程数。
    keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
    unit - keepAliveTime 参数的时间单位。
    workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。
    threadFactory - 执行程序创建新线程时使用的工厂。
    handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
    ThreadPoolExecutor是Executors类的底层实现。
    在JDK帮助文档中,有如此一段话:
    “强烈建议程序员使用较为方便的Executors工厂方法Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池)Executors.newSingleThreadExecutor()(单个后台线程)
    它们均为大多数使用场景预定义了设置。”

    下面介绍一下几个类的源码:
    ExecutorService newFixedThreadPool (int nThreads):固定大小线程池。
    可以看到,corePoolSize和maximumPoolSize的大小是一样的(实际上,后面会介绍,如果使用无界queue的话maximumPoolSize参数是没有意义的),keepAliveTime和unit的设值表名什么?-就是该实现不想keep alive!最后的BlockingQueue选择了LinkedBlockingQueue,该queue有一个特点,他是无界的。

     public static ExecutorService newFixedThreadPool(int nThreads) {   
                return new ThreadPoolExecutor(nThreads, nThreads,   
                                               0L, TimeUnit.MILLISECONDS,   
                                              new LinkedBlockingQueue<Runnable>());   
    }
    

    ExecutorService newSingleThreadExecutor():单线程

    public static ExecutorService newSingleThreadExecutor() {   
             return new FinalizableDelegatedExecutorService   
                 (new ThreadPoolExecutor(1, 1,  0L, TimeUnit.MILLISECONDS,   
                                        new LinkedBlockingQueue<Runnable>()));   
    }
    

    ExecutorService newCachedThreadPool():无界线程池,可以进行自动线程回收
    这个实现就有意思了。首先是无界的线程池,所以我们可以发现maximumPoolSize为big big。其次BlockingQueue的选择上使用SynchronousQueue。可能对于该BlockingQueue有些陌生,简单说:该QUEUE中,每个插入操作必须等待另一个线程的对应移除操作。

    public static ExecutorService newCachedThreadPool() {   
           return new ThreadPoolExecutor(0, Integer.MAX_VALUE,   
                                          60L, TimeUnit.SECONDS,   
                                           new SynchronousQueue<Runnable>());   
    }
    

    先从BlockingQueue<Runnable> workQueue这个入参开始说起。在JDK中,其实已经说得很清楚了,一共有三种类型的queue。
    所有BlockingQueue 都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:
    如果运行的线程少于 corePoolSize,则 Executor始终首选添加新的线程,而不进行排队。(如果当前运行的线程小于corePoolSize,则任务根本不会存放,添加到queue中,而是直接抄家伙(thread)开始运行)
    如果运行的线程等于或多于 corePoolSize,则 Executor始终首选将请求加入队列,而不添加新的线程。
    如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
    queue上的三种类型。

    排队有三种通用策略:
    直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
    无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
    有界队列。当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

    BlockingQueue的选择。
    例子一:使用直接提交策略,也即SynchronousQueue。
    首先SynchronousQueue是无界的,也就是说他存数任务的能力是没有限制的,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加。在这里不是核心线程便是新创建的线程,但是我们试想一样下,下面的场景。
    我们使用一下参数构造ThreadPoolExecutor:

    new ThreadPoolExecutor(   
                2, 3, 30, TimeUnit.SECONDS,    
                 new  SynchronousQueue<Runnable>(),    
                 new RecorderThreadFactory("CookieRecorderPool"),    
            new ThreadPoolExecutor.CallerRunsPolicy());  
    
    new ThreadPoolExecutor(
      2, 3, 30, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>(),
      new RecorderThreadFactory("CookieRecorderPool"),
      new ThreadPoolExecutor.CallerRunsPolicy());
    

    当核心线程已经有2个正在运行.
    1.此时继续来了一个任务(A),根据前面介绍的“如果运行的线程等于或多于 corePoolSize,则 Executor始终首选将请求加入队列,而不添加新的线程。”,所以A被添加到queue中。
    2.又来了一个任务(B),且核心2个线程还没有忙完,OK,接下来首先尝试1中描述,但是由于使用的SynchronousQueue,所以一定无法加入进去。
    3.此时便满足了上面提到的“如果无法将请求加入队列,则创建新的线程,除非创建此线程超出maximumPoolSize,在这种情况下,任务将被拒绝。”,所以必然会新建一个线程来运行这个任务。
    4.暂时还可以,但是如果这三个任务都还没完成,连续来了两个任务,第一个添加入queue中,后一个呢?queue中无法插入,而线程数达到了maximumPoolSize,所以只好执行异常策略了。

    所以在使用SynchronousQueue通常要求maximumPoolSize是无界的,这样就可以避免上述情况发生(如果希望限制就直接使用有界队列)。对于使用SynchronousQueue的作用jdk中写的很清楚:此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。
    什么意思?如果你的任务A1,A2有内部关联,A1需要先运行,那么先提交A1,再提交A2,当使用SynchronousQueue我们可以保证,A1必定先被执行,在A1么有被执行前,A2不可能添加入queue中。

    例子二:使用无界队列策略,即LinkedBlockingQueue
    这个就拿newFixedThreadPool来说,根据前文提到的规则:
    如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。那么当任务继续增加,会发生什么呢?
    如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。OK,此时任务变加入队列之中了,那什么时候才会添加新线程呢?
    如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。这里就很有意思了,可能会出现无法加入队列吗?不像SynchronousQueue那样有其自身的特点,对于无界队列来说,总是可以加入的(资源耗尽,当然另当别论)。换句说,永远也不会触发产生新的线程!corePoolSize大小的线程数会一直运行,忙完当前的,就从队列中拿任务开始运行。所以要防止任务疯长,比如任务运行的实行比较长,而添加任务的速度远远超过处理任务的时间,而且还不断增加,不一会儿就爆了。

    例子三:有界队列,使用ArrayBlockingQueue。
    这个是最为复杂的使用,所以JDK不推荐使用也有些道理。与上面的相比,最大的特点便是可以防止资源耗尽的情况发生。
    举例来说,请看如下构造方法:

    new ThreadPoolExecutor(   
              2, 4, 30, TimeUnit.SECONDS,    
               new ArrayBlockingQueue<Runnable>(2),    
              new RecorderThreadFactory("CookieRecorderPool"),    
              new ThreadPoolExecutor.CallerRunsPolicy());  
    
    new ThreadPoolExecutor(
        2, 4, 30, TimeUnit.SECONDS,
        new ArrayBlockingQueue<Runnable>(2),
        new RecorderThreadFactory("CookieRecorderPool"),
        new ThreadPoolExecutor.CallerRunsPolicy());
    

    假设,所有的任务都永远无法执行完。
    对于首先来的A,B来说直接运行,接下来,如果来了C,D,他们会被放到queue中,如果接下来再来E,F,则增加线程运行E,F。但是如果再来任务,队列无法再接受了,线程数也到达最大的限制了,所以就会使用拒绝策略来处理。
    keepAliveTime
    jdk中的解释是:当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
    有点拗口,其实这个不难理解,在使用了“池”的应用中,大多都有类似的参数需要配置。比如数据库连接池,DBCP中的maxIdle,minIdle参数。
    什么意思?接着上面的解释,后来向老板派来的工人始终是“借来的”,俗话说“有借就有还”,但这里的问题就是什么时候还了,如果借来的工人刚完成一个任务就还回去,后来发现任务还有,那岂不是又要去借?这一来一往,老板肯定头也大死了。

    合理的策略:既然借了,那就多借一会儿。直到“某一段”时间后,发现再也用不到这些工人时,便可以还回去了。这里的某一段时间便是keepAliveTime的含义,TimeUnit为keepAliveTime值的度量。

    RejectedExecutionHandler
    另一种情况便是,即使向老板借了工人,但是任务还是继续过来,还是忙不过来,这时整个队伍只好拒绝接受了。
    RejectedExecutionHandler接口提供了对于拒绝任务的处理的自定方法的机会。在ThreadPoolExecutor中已经默认包含了4中策略,因为源码非常简单,这里直接贴出来。
    CallerRunsPolicy:线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {   
                 if (!e.isShutdown()) {   
                    r.run();   
                 }   
             }  
    
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
               if (!e.isShutdown()) {
                   r.run();
               }
           }
    

    这个策略显然不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身来执行。
    AbortPolicy:处理程序遭到拒绝将抛出运行时RejectedExecutionException

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {   
                throw new RejectedExecutionException();   
             }  
    
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
               throw new RejectedExecutionException();
           }
    

    这种策略直接抛出异常,丢弃任务。
    DiscardPolicy:不能执行的任务将被删除

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {   
           }  
    
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
           }
    

    这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常。
    DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {   
               if (!e.isShutdown()) {   
                   e.getQueue().poll();   
                   e.execute(r);   
                }   
    }  
    
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
               if (!e.isShutdown()) {
                   e.getQueue().poll();
                   e.execute(r);
               }
           }
    

    该策略就稍微复杂一些,在pool没有关闭的前提下首先丢掉缓存在队列中的最早的任务,然后重新尝试运行该任务。这个策略需要适当小心。
    设想:如果其他线程都还在运行,那么新来任务踢掉旧任务,缓存在queue中,再来一个任务又会踢掉queue中最老任务。

    总结:
    keepAliveTime和maximumPoolSize及BlockingQueue的类型均有关系。如果BlockingQueue是无界的,那么永远不会触发maximumPoolSize,自然keepAliveTime也就没有了意义。
    反之,如果核心数较小,有界BlockingQueue数值又较小,同时keepAliveTime又设的很小,如果任务频繁,那么系统就会频繁的申请回收线程。

    public static ExecutorService newFixedThreadPool(int nThreads) {
           return new ThreadPoolExecutor(nThreads, nThreads,
                                         0L, TimeUnit.MILLISECONDS,
                                         new LinkedBlockingQueue<Runnable>());
       }
    

    还有很多其他的方法:
    比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法,有兴趣的朋友可以自行查阅API。

    三、JaVA 反射机制

    一,先看一下反射的概念:
    主要是指程序可以访问,检测和修改它本身状态或行为的一种能力,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。

    反射是java中一种强大的工具,能够使我们很方便的创建灵活的代码,这些代码可以再运行时装配,无需在组件之间进行源代码链接。但是反射使用不当会成本很高!

    二,反射机制的作用:
    1,反编译:.class-->.java
    2,通过反射机制访问java对象的属性,方法,构造方法等;
    这样好像更容易理解一些,下边我们具体看怎么实现这些功能。

    三,在这里先看一下sun为我们提供了那些反射机制中的类:
    java.lang.Class;
    java.lang.reflect.Constructor;
    java.lang.reflect.Field;
    java.lang.reflect.Method;
    java.lang.reflect.Modifier;

    四,具体功能实现:
    1,反射机制获取类有三种方法,我们来获取Employee类型

    //第一种方式:  
    Classc1 = Class.forName("Employee");  
    
    //第二种方式:  
    //java中每个类型都有class 属性.  
    Classc2 = Employee.class;  
      
    //第三种方式:  
    //java语言中任何一个java对象都有getClass 方法  
    Employeee = new Employee();  
    Classc3 = e.getClass(); //c3是运行时类 (e的运行时类是Employee)
    

    2,创建对象:获取类以后我们来创建它的对象,利用newInstance:

    Class c =Class.forName("Employee");  
     
    //创建此Class 对象所表示的类的一个新实例  
    Objecto = c.newInstance(); //调用了Employee的无参数构造方法. 
    

    3,获取属性:分为所有的属性和指定的属性:
    a,先看获取所有的属性的写法:

    /获取整个类  
    Class c = Class.forName("java.lang.Integer");  
    //获取所有的属性?  
    Field[] fs = c.getDeclaredFields();  
    
    //定义可变长的字符串,用来存储属性  
    StringBuffer sb = new StringBuffer();  
    //通过追加的方法,将每个属性拼接到此字符串中  
    //最外边的public定义  
    sb.append(Modifier.toString(c.getModifiers()) + " class " + c.getSimpleName() +"{\n");  
    //里边的每一个属性  
    for(Field field:fs){  
        sb.append("\t");//空格  
        sb.append(Modifier.toString(field.getModifiers())+" ");//获得属性的修饰符,例如public,static等等  
        sb.append(field.getType().getSimpleName() + " ");//属性的类型的名字  
        sb.append(field.getName()+";\n");//属性的名字+回车  
    }  
    
    sb.append("}");  
    
    System.out.println(sb);  
    

    b,获取特定的属性,对比着传统的方法来学习:

    public static void main(String[] args) throws Exception{  
                
        //以前的方式:  
        /* 
        User u = new User(); 
        u.age = 12; //set 
        System.out.println(u.age); //get 
        */  
              
        //获取类  
        Class c = Class.forName("User");  
        //获取id属性  
        Field idF = c.getDeclaredField("id");  
        //实例化这个类赋给o  
        Object o = c.newInstance();  
        //打破封装  
        idF.setAccessible(true); //使用反射机制可以打破封装性,导致了java对象的属性不安全。  
        //给o对象的id属性赋值"110"  
        idF.set(o, "110"); //set  
        //get  
        System.out.println(idF.get(o));  
    }  
    

    4,获取方法,和构造方法,不再详细描述,只来看一下关键字:

    方法关键字 含义
    getDeclaredMethods() 获取所有的方法
    getReturnType() 获得方法的放回类型
    getParameterTypes() 获得方法的传入参数类型
    getDeclaredMethod("方法名",参数类型.class,……) 获得特定的方法
    构造方法关键字 含义
    getDeclaredConstructors() 获取所有的构造方法
    getDeclaredConstructor(参数类型.class,……) 获取特定的构造方法
    父类和父接口 含义
    getSuperclass() 获取某类的父类
    getInterfaces() 获取某类实现的接口

    这样我们就可以获得类的各种内容,进行了反编译。对于JAVA这种先编译再运行的语言来说,反射机制可以使代码更加灵活,更加容易实现面向对象。

    五,反射加配置文件,使我们的程序更加灵活:
    当然了,JAVA中其实也是一样,只不过这里的配置文件为.properties,称作属性文件。通过反射读取里边的内容。这样代码是固定的,但是配置文件的内容我们可以改,这样使我们的代码灵活了很多!

    四、http协议

    HTTP是Hyper Text Transfer Protocol(超文本传输协议)的缩写。
    HTTP协议(HyperText Transfer Protocol,超文本传输协议)是用于从WWW服务器传输超文本到本地浏览器的传送协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本先于图形)等。
    HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器模型。HTTP是一个无状态的协议。

    1.2 在TCP/IP协议栈中的位置
    HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。如下图所示:

    image.png

    默认HTTP的端口号为80,HTTPS的端口号为443。

    1.3 HTTP的请求响应模型
    HTTP协议永远都是客户端发起请求,服务器回送响应。见下图:

    image.png

    这样就限制了使用HTTP协议,无法实现在客户端没有发起请求的时候,服务器将消息推送给客户端。
    HTTP协议是一个无状态的协议,同一个客户端的这次请求和上次请求是没有对应关系。

    1.4 工作流程
    一次HTTP操作称为一个事务,其工作过程可分为四步:
    1)首先客户机与服务器需要建立连接。只要单击某个超级链接,HTTP的工作开始。
    2)建立连接后,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(URL)、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和可能的内容。
    3)服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或错误的代码,后边是MIME信息包括服务器信息、实体信息和可能的内容。
    4)客户端接收服务器所返回的信息通过浏览器显示在用户的显示屏上,然后客户机与服务器断开连接。
    如果在以上过程中的某一步出现错误,那么产生错误的信息将返回到客户端,有显示屏输出。对于用户来说,这些过程是由HTTP自己完成的,用户只要用鼠标点击,等待信息显示就可以了。

    HTTP/1.0 每次请求都需要建立新的TCP连接,连接不能复用。HTTP/1.1 新的请求可以在上次请求建立的TCP连接之上发送,连接可以复用。优点是减少重复进行TCP三次握手的开销,提高效率。

    注意:在同一个TCP连接中,新的请求需要等上次请求收到响应后,才能发送。

    2.1.2 Host域
    HTTP1.1在Request消息头里头多了一个Host域, HTTP1.0则没有这个域。

    Eg:
        GET /pub/WWW/TheProject.html HTTP/1.1
        Host: www.w3.org
    

    可能HTTP1.0的时候认为,建立TCP连接的时候已经指定了IP地址,这个IP地址上只有一个host。

    2.1.3日期时间戳
    (接收方向)
    无论是HTTP1.0还是HTTP1.1,都要能解析下面三种date/time stamp:
    Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
    Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
    Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
    (发送方向)
    HTTP1.0要求不能生成第三种asctime格式的date/time stamp;
    HTTP1.1则要求只生成RFC 1123(第一种)格式的date/time stamp。

    2.1.4状态响应码
    状态响应码100 (Continue) 状态代码的使用,允许客户端在发request消息body之前先用request header试探一下server,看server要不要接收request body,再决定要不要发request body。
    客户端在Request头部中包含
    Expect: 100-continue
    Server看到之后呢如果回100 (Continue) 这个状态代码,客户端就继续发request body。这个是HTTP1.1才有的。
    另外在HTTP/1.1中还增加了101、203、205等等性状态响应码

    2.1.5请求方式
    HTTP1.1增加了OPTIONS, PUT, DELETE, TRACE, CONNECT这些Request方法.

           Method         = "OPTIONS"                ; Section 9.2
                          | "GET"                    ; Section 9.3
                          | "HEAD"                   ; Section 9.4
                          | "POST"                   ; Section 9.5
                          | "PUT"                    ; Section 9.6
                          | "DELETE"                 ; Section 9.7
                          | "TRACE"                  ; Section 9.8
                          | "CONNECT"                ; Section 9.9
                          | extension-method
           extension-method = token
    

    请求消息格式如下所示:
    请求行
    通用信息头|请求头|实体头
    CRLF(回车换行)
    实体内容
    其中“请求行”为:请求行 = 方法 [空格] 请求URI [空格] 版本号 [回车换行]

    请求行实例:
    Eg1:
    GET /index.html HTTP/1.1
    Eg2:
    POST http://192.168.2.217:8080/index.jsp HTTP/1.1
    HTTP请求消息实例:
    GET /hello.htm HTTP/1.1
    Accept: */*
    Accept-Language: zh-cn
    Accept-Encoding: gzip, deflate
    If-Modified-Since: Wed, 17 Oct 2007 02:15:55 GMT
    If-None-Match: W/"158-1192587355000"
    User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)
    Host: 192.168.2.162:8080
    Connection: Keep-Alive
    

    2.3.2.1 1:请求收到,继续处理
    100——客户必须继续发出请求
    101——客户要求服务器根据请求转换HTTP协议版本

    2.3.2.2 2:操作成功收到,分析、接受
    200——交易成功
    201——提示知道新文件的URL
    202——接受和处理、但处理未完成
    203——返回信息不确定或不完整
    204——请求收到,但返回信息为空
    205——服务器完成了请求,用户代理必须复位当前已经浏览过的文件
    206——服务器已经完成了部分用户的GET请求

    2.3.2.3 3:完成此请求必须进一步处理
    300——请求的资源可在多处得到
    301——删除请求数据
    302——在其他地址发现了请求数据
    303——建议客户访问其他URL或访问方式
    304——客户端已经执行了GET,但文件未变化
    305——请求的资源必须从服务器指定的地址得到
    306——前一版本HTTP中使用的代码,现行版本中不再使用
    307——申明请求的资源临时性删除

    2.3.2.4 4:请求包含一个错误语法或不能完成
    400——错误请求,如语法错误
    401——未授权
    HTTP 401.1 - 未授权:登录失败
      HTTP 401.2 - 未授权:服务器配置问题导致登录失败
      HTTP 401.3 - ACL 禁止访问资源
      HTTP 401.4 - 未授权:授权被筛选器拒绝
    HTTP 401.5 - 未授权:ISAPI 或 CGI 授权失败
    402——保留有效ChargeTo头响应
    403——禁止访问
    HTTP 403.1 禁止访问:禁止可执行访问
      HTTP 403.2 - 禁止访问:禁止读访问
      HTTP 403.3 - 禁止访问:禁止写访问
      HTTP 403.4 - 禁止访问:要求 SSL
      HTTP 403.5 - 禁止访问:要求 SSL 128
      HTTP 403.6 - 禁止访问:IP 地址被拒绝
      HTTP 403.7 - 禁止访问:要求客户证书
      HTTP 403.8 - 禁止访问:禁止站点访问
      HTTP 403.9 - 禁止访问:连接的用户过多
      HTTP 403.10 - 禁止访问:配置无效
      HTTP 403.11 - 禁止访问:密码更改
      HTTP 403.12 - 禁止访问:映射器拒绝访问
      HTTP 403.13 - 禁止访问:客户证书已被吊销
      HTTP 403.15 - 禁止访问:客户访问许可过多
      HTTP 403.16 - 禁止访问:客户证书不可信或者无效
    HTTP 403.17 - 禁止访问:客户证书已经到期或者尚未生效
    404——没有发现文件、查询或URl
    405——用户在Request-Line字段定义的方法不允许
    406——根据用户发送的Accept拖,请求资源不可访问
    407——类似401,用户必须首先在代理服务器上得到授权
    408——客户端没有在用户指定的饿时间内完成请求
    409——对当前资源状态,请求不能完成
    410——服务器上不再有此资源且无进一步的参考地址
    411——服务器拒绝用户定义的Content-Length属性请求
    412——一个或多个请求头字段在当前请求中错误
    413——请求的资源大于服务器允许的大小
    414——请求的资源URL长于服务器允许的长度
    415——请求资源不支持请求项目格式
    416——请求中包含Range请求头字段,在当前请求资源范围内没有range指示值,请求也不包含If-Range请求头字段
    417——服务器不满足请求Expect头字段指定的期望值,如果是代理服务器,可能是下一级服务器不能满足请求长。

    2.3.2.5 5:服务器执行一个完全有效请求失败
      HTTP 500 - 内部服务器错误
      HTTP 500.100 - 内部服务器错误 - ASP 错误
      HTTP 500-11 服务器关闭
      HTTP 500-12 应用程序重新启动
      HTTP 500-13 - 服务器太忙
      HTTP 500-14 - 应用程序无效
      HTTP 500-15 - 不允许请求 global.asa
      Error 501 - 未实现
    HTTP 502 - 网关错误
    可看到,当采用HTTP/1.1时,连接不是在请求结束后就断开的。若采用HTTP1.0,在命令窗口键入:
    GET /index.html HTTP/1.0
    此时可以看到请求结束之后马上断开。
    读者还可以尝试在使用GET或POST等时,带上头域信息,例如键入如下信息:
    GET /index.html HTTP/1.1
    connection: close
    Host: www.baidu.com

    2.5 常用的请求方式
    常用的请求方式是GET和POST.
    l GET方式:是以实体的方式得到由请求URI所指定资源的信息,如果请求URI只是一个数据产生过程,那么最终要在响应实体中返回的是处理过程的结果所指向的资源,而不是处理过程的描述。
    l POST方式:用来向目的服务器发出请求,要求它接受被附在请求后的实体,并把它当作请求队列中请求URI所指定资源的附加新子项,Post被设计成用统一的方法实现下列功能:
    1:对现有资源的解释;
    2:向电子公告栏、新闻组、邮件列表或类似讨论组发信息;
    3:提交数据块;
    4:通过附加操作来扩展数据库 。
    从上面描述可以看出,Get是向服务器发索取数据的一种请求;而Post是向服务器提交数据的一种请求,要提交的数据位于信息头后面的实体中。
    GET与POST方法有以下区别:
    (1) 在客户端,Get方式在通过URL提交数据,数据在URL中可以看到;POST方式,数据放置在HTML HEADER内提交。
    (2) GET方式提交的数据最多只能有1024字节,而POST则没有此限制。
    (3) 安全性问题。正如在(1)中提到,使用 Get 的时候,参数会显示在地址栏上,而 Post 不会。所以,如果这些数据是中文数据而且是非敏感数据,那么使用 get;如果用户输入的数据不是中文字符而且包含敏感数据,那么还是使用 post为好。
    (4) 安全的和幂等的。所谓安全的意味着该操作用于获取信息而非修改信息。幂等的意味着对同一 URL 的多个请求应该返回同样的结果。完整的定义并不像看起来那样严格。换句话说,GET 请求一般不应产生副作用。从根本上讲,其目标是当用户打开一个链接时,她可以确信从自身的角度来看没有改变资源。比如,新闻站点的头版不断更新。虽然第二次请求会返回不同的一批新闻,该操作仍然被认为是安全的和幂等的,因为它总是返回当前的新闻。反之亦然。POST 请求就不那么轻松了。POST 表示可能改变服务器上的资源的请求。仍然以新闻站点为例,读者对文章的注解应该通过 POST 请求实现,因为在注解提交之后站点已经不同了(比方说文章下面出现一条注解)。

    3.1 Cookie和Session
    Cookie和Session都为了用来保存状态信息,都是保存客户端状态的机制,它们都是为了解决HTTP无状态的问题而所做的努力。
    Session可以用Cookie来实现,也可以用URL回写的机制来实现。用Cookie来实现的Session可以认为是对Cookie更高级的应用。

    3.1.1两者比较
    Cookie和Session有以下明显的不同点:
    1)Cookie将状态保存在客户端,Session将状态保存在服务器端;
    2)Cookies是服务器在本地机器上存储的小段文本并随每一个请求发送至同一个服务器。在客户终端,浏览器解析这些cookies并将它们保存为一个本地文件,它会自动将同一服务器的任何请求缚上这些cookies。Session并没有在HTTP的协议中定义;
    3)Session是针对每一个用户的,变量的值保存在服务器上,用一个sessionID来区分是哪个用户session变量,这个值是通过用户的浏览器在访问的时候返回给服务器,当客户禁用cookie时,这个值也可能设置为由get来返回给服务器;
    4)就安全性来说:当你访问一个使用session 的站点,同时在自己机子上建立一个cookie,建议在服务器端的SESSION机制更安全些.因为它不会任意读取客户存储的信息。

    3.2 缓存的实现原理
    3.2.1什么是Web缓存
    WEB缓存(cache)位于Web服务器和客户端之间。
    缓存会根据请求保存输出内容的副本,例如html页面,图片,文件,当下一个请求来到的时候:如果是相同的URL,缓存直接使用副本响应访问请求,而不是向源服务器再次发送请求。
    HTTP协议定义了相关的消息头来使WEB缓存尽可能好的工作。

    3.2.2缓存的优点
    减少相应延迟:因为请求从缓存服务器(离客户端更近)而不是源服务器被相应,这个过程耗时更少,让web服务器看上去相应更快。
    减少网络带宽消耗:当副本被重用时会减低客户端的带宽消耗;客户可以节省带宽费用,控制带宽的需求的增长并更易于管理。
    3.4 https通信过程
    3.4.1什么是https
    HTTPS(全称:Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容请看SSL。
    见下图:

    image.png

    https所用的端口号是443。

    3.4.2 https的实现原理
    有两种基本的加解密算法类型:
    1)对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有DES、AES等;
    2)非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有RSA、DSA等。
    下面看一下https的通信过程:

    image.png

    https通信的优点:
    1)客户端产生的密钥只有客户端和服务器端能得到;
    2)加密的数据只有客户端和服务器端才能得到明文;
    3)客户端到服务端的通信是安全的。

    3.5 http代理
    3.5.1 http代理服务器
    代理服务器英文全称是Proxy Server,其功能就是代理网络用户去取得网络信息。形象的说:它是网络信息的中转站。
    代理服务器是介于浏览器和Web服务器之间的一台服务器,有了它之后,浏览器不是直接到Web服务器去取回网页而是向代理服务器发出请求,Request信号会先送到代理服务器,由代理服务器来取回浏览器所需要的信息并传送给你的浏览器。
    而且,大部分代理服务器都具有缓冲的功能,就好象一个大的Cache,它有很大的存储空间,它不断将新取得数据储存到它本机的存储器上,如果浏览器所请求的数据在它本机的存储器上已经存在而且是最新的,那么它就不重新从Web服务器取数据,而直接将存储器上的数据传送给用户的浏览器,这样就能显著提高浏览速度和效率。
    更重要的是:Proxy Server(代理服务器)是Internet链路级网关所提供的一种重要的安全功能,它的工作主要在开放系统互联(OSI)模型的对话层。

    3.5.2 http代理服务器的主要功能
    主要功能如下:
    1)突破自身IP访问限制,访问国外站点。如:教育网、169网等网络用户可以通过代理访问国外网站;
    2)访问一些单位或团体内部资源,如某大学FTP(前提是该代理地址在该资源的允许访问范围之内),使用教育网内地址段免费代理服务器,就可以用于对教育 网开放的各类FTP下载上传,以及各类资料查询共享等服务;
    3)突破中国电信的IP封锁:中国电信用户有很多网站是被限制访问的,这种限制是人为的,不同Serve对地址的封锁是不同的。所以不能访问时可以换一个国 外的代理服务器试试;
    4)提高访问速度:通常代理服务器都设置一个较大的硬盘缓冲区,当有外界的信息通过时,同时也将其保存到缓冲区中,当其他用户再访问相同的信息时, 则直接由缓冲区中取出信息,传给用户,以提高访问速度;
    5)隐藏真实IP:上网者也可以通过这种方法隐藏自己的IP,免受攻击。

    3.5.3 http代理图示
    http代理的图示见下图:

    image.png

    对于客户端浏览器而言,http代理服务器相当于服务器。
    而对于Web服务器而言,http代理服务器又担当了客户端的角色。

    3.6 虚拟主机的实现
    3.6.1什么是虚拟主机
    虚拟主机:是在网络服务器上划分出一定的磁盘空间供用户放置站点、应用组件等,提供必要的站点功能与数据存放、传输功能。
    所谓虚拟主机,也叫“网站空间”就是把一台运行在互联网上的服务器划分成多个“虚拟”的服务器,每一个虚拟主机都具有独立的域名和完整的Internet服务器(支持WWW、FTP、E-mail等)功能。一台服务器上的不同虚拟主机是各自独立的,并由用户自行管理。但一台服务器主机只能够支持一定数量的虚拟主机,当超过这个数量时,用户将会感到性能急剧下降。

    3.6.2虚拟主机的实现原理
    虚拟主机是用同一个WEB服务器,为不同域名网站提供服务的技术。Apache、Tomcat等均可通过配置实现这个功能。
    相关的HTTP消息头:Host。
    例如:Host: www.baidu.com
    客户端发送HTTP请求的时候,会携带Host头,Host头记录的是客户端输入的域名。这样服务器可以根据Host头确认客户要访问的是哪一个域名。

    相关文章

      网友评论

        本文标题:阿里巴巴高级Java面试题

        本文链接:https://www.haomeiwen.com/subject/pikkixtx.html