[转]JAVA工程师面试题精选(三)

61.Java中有几种方法可以实现一个线程?用什么关键字修饰同步方法?stop()和suspend()方法为何不推荐使用? 

  Java中有三种方法来实现一个线程,分别是继承Thread类、实现Runnable接口、使用ExecutorService,Callable,Future。其中,前两种线程执行完之后没有返回值,最后一种是带返回值的。 
  继承Thread:自己的线程类继承Thread,然后创建一个自己线程类的实例,通过实例调用start方法来执行run方法。

Public class MyThread extends Thread {Public void run() {...}}

  然后在合适的地方启动线程:

MyThread t1 = new MyThread();MyThread t2 = new MyThread();t1.start();t2.start();

  实现Runnable接口:自己的线程类实现Runnable接口,然后创建一个自己线程类的实例,将实例作为参数构造一个Thread对象,最后通过该Thread对象来调用start方法运行run方法。

Public class MyThread extends Otherclass implements Runnable{Public void run() {...}}

  然后在合适的地方启动线程:

MyThread t1 = new MyThread();Thread thread = new Thread(t1);Thread.start();

  使用ExecutorService,Callable,Future来实现:可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。 
  参考:http://blog.csdn.net/aboy123/article/details/38307539 
  用synchronized关键字来修饰同步方法,表示该方法只能同时被一个线程访问。 
  stop方法不安全,suspend方法容易发生死锁。

62.sleep()和join()和yield()和wait()之间的区别 
  首先说下sleep()和yield()的区别: 
  1)sleep()必须带参数,yield()不带参数。 
  2)sleep()是让当前线程进入sleep状态,时间到了,然后进入就绪态;yield()是直接让当前线程进入就绪态。 
  3)sleep()后,所有优先级的其它线程都有机会运行;yield()后,只有相同优先级的线程才有机会执行。 
  Tips:sleep()和yield()都不会释放对象锁。

  join()方法是让调用该方法的Thread在完成了run中的代码之后才能执行join后面的代码。比如:

thread1.start();thread2.start();

  这样的话,thread1和thread2会同时运行,但是这样:

thread1.start();thread1.join();thread2.start();

  那么只有当线程thread1完成执行之后才能执行thread2。所以通过join方法可以确保多个线程按照顺序执行。

  wait()方法通常和notify()和notifyAll()方法同时使用。 
  这三个方法主要用于协调多个线程对共享数据的存取。必须在synchronized语句块中使用。 
  synchronized关键字用于保护共享数据,阻止其他线程对共享数据的存取,但是这样程序的流程就很不灵活了,如何才能在当前线程还没退出synchronized数据块时让其他线程也有机会访问共享数据呢?此时就用这三个方法来灵活控制。 
  wait()方法使当前线程暂停执行并释放对象锁,让其他线程可以进入synchronized数据块,当前线程被放入对象等待池中。当调用notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志。如果锁标志等待池中没有线程,则notify()不起作用。注意,在调用此方法时,并不能确切地唤醒某一个等待状态的线程,而是由JVM确定唤醒哪一个,而且不是按照优先级。notifyAll()则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中,让他们竞争。 
  Tips:sleep,join,yield都是Thread类中的方法,wait,notify,notifyAll都是Object类中的方法。 
  Synchronized关键字:该关键字用于保护共享数据,当然前提条件是要分清哪些数据是共享数据。每个对象都有一个锁标志(对象锁),当一个线程访问该对象时,其中被Synchronized修饰的数据将被”上锁”,阻止其他线程访问。当前线程访问完这部分数据后释放锁标志,其他线程就可以访问了。其内部机制为,当一个线程获取锁并且执行到synchronized代码之时,该线程首先从主堆内存空间中读取该锁定对象的所有变化,确保其在开始执行之前拥有最新的信息。在synchronized部分执行完毕后,线程准备释放锁的时候,所有针对被锁定对象的修改都将被写入主堆内存中。这样,其它线程在请求锁的时候就可以获取该对象的最新信息。这就是线程安全的实现机制。

63.当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法? 
  Java中每个对象都会有一个对象锁,当一个线程访问某个对象的synchronized方法时,表示对该对象进行上锁,那么,在该线程执行完这个synchronized方法之前,其它任何线程都不能再访问该对象的任何synchronized方法,但是却可以访问非synchronized方法。 
  另外,如果synchronized方法中调用了wait方法,则其它线程是可以进入synchronized方法的。 
  如果synchronized方法同时是static的,那么,一个线程进入该方法后,它锁定的不再是对象锁,而是将整个类都锁定了,其它线程暂时不能访问这个类(无论其中是否有其它synchronized方法)。只有当前线程结束对synchronized static访问后,才会解锁该类。 
  线程基本状态图 
  首先可以看出,线程的基本状态有新建、就绪、运行、阻塞和死亡五个。 
  当新建的线程调用start方法就可以使线程进入就绪状态。 
  当就绪的线程获取了CPU调度就可以进入运行状态。 
  运行中的线程因为CPU时间片用完就会重新进入就绪状态。 
  运行中的线程如果调用了sleep,wait等方法来等待某一条件达成就会进入阻塞状态,这其中又分为几种情况: 
  1)调用sleep的话,线程不会释放对象锁,只是阻塞等到时间结束就会进入就绪状态。 
  2)进入synchronized的话,线程会进入对象锁池,只有获得了对象锁才能重新进入就绪状态。 
  3)调用wait的话,线程会进入对象等待池,只有再次调用notify或者notifyAll的时候,线程才会进入对象锁池,此时,再获得对象锁就可以重新进入就绪状态。 
  运行中的线程运行结束就会进入死亡状态。 
 http://www.cnblogs.com/mengdd/archive/2013/02/20/2917966.html

64.简述synchronized和java.util.concurrent.locks.Lock的异同。 
  相同点:Lock能完成synchronized所实现的所有功能。 
  不同点:Lock比synchronized有更精确的线程语义和更好的性能。Synchronized会自动释放锁,而Lock需要程序员手工释放锁,且必须在finally从句中释放。

65.Collection框架中实现比较需呀实现什么接口? 
  可以通过实现Comparable和Comparator来实现排序。 
  Comparable一般用于需要比较操作的类上,在类内部定义比较的规则,即重写compareTo方法。 
  Comparator用于独立实现一个比较器,往往用在事先没有定义比较规则的类进行比较的时候。新建的比较器中需要重写compare方法。 
  参考: 
  http://blog.csdn.net/u012050416/article/details/50765240

66.简述HashMap的内部实现原理(HashMap怎么保证键的唯一性) 
  HashMap是基于哈希表的Map接口的非同步实现(HashTable是同步实现)。允许key和value为null,但是null的key只能有一个。不能保证HashMap中的元素顺序为插入时的顺序,因为key-value对都是按照哈希值来存放的。 
  在Java中,最为基本的存储方式有两种,分别是顺序存储(数组)和链式存储(引用),所有的数据结构都是采用这两种基本的存储方式。HashMap结合了这两种存储方式,表现为: 
  Hash表存储示意图 
  从上图可以看出,底层实现是一个顺序存储的数组,数组中的每一个项又都是一个链表。当新建一个hashMap的时候,就会自动初始化这样一个数组。 
  当我们向HashMap中put键值对的时候,会根据key的hashcode来散列生成一个hash值,这个值表示存放在数组中的位置。如果该位置上没有键值对元素,就直接将键值对放到此位置,如果已经存在了键值对,那么就存放在以这个键值对为头结点的链表中。 
  同样的,当我们从HashMap中get键值对的时候,会根据key的hashcode来散列出hash值,找到数组中对应的位置,然后在这个位置对应的链表中通过key的equals方法来比对最终找到想要的键值对。 
  参考:http://zhangshixi.iteye.com/blog/672697

67.如何实现文件的断点下载? 
  请求端通过告知服务器端某个文件的开始下载位置,让服务器端从该位置开始发送数据,然后接收端将数据保存下来存放到临时文件,然后在原先下载未完成的文件中定位到需要继续下载的地方,将临时文件的内容读进来,最终形成一个完整的文件。 
  在Java中可以使用HttpURLConnection的setRequestProperty方法来设置需要继续下载的位置,然后使用HttpURLConnection的getInputStream方法来获取输入流。最后通过RandomAccessFile的seek方法先定位到未下载完文件的最后位置,然后将输入流中的数据write进去,最终形成一个完整的文件。 
  参考:http://blog.csdn.net/it_man/article/details/6602697

68.Java中的线程池实现原理。 
  我们知道,一个线程完成一个任务的时间分为三个部分: 
  T1:创建线程的时间 
  T2:使用线程执行任务的时间 
  T3:销毁线程的时间 
  我们希望,在服务器处理用户请求的时候,尽可能地减少T1和T3的时间,因此,需要使用线程池来存放一些可以长期存在,能处理多个任务的线程。 
  一个线程池至少应该包括四个部分: 
  1)线程池管理器:创建,销毁线程池,向任务队列添加任务等。 
  2)工作线程:可以循环执行任务的线程,任务队列中没有任务时,等待。 
  3)任务接口:为所有任务提供统一的接口,供工作线程调度任务。 
  4)任务队列:存放没有处理的任务。 
  其中,工作线程和任务的存放数据结构都是Vector。简单的线程池实现就是这样,但是高级的线程池还应该作如下考虑: 
  动态增减工作线程数目。当用户请求突然大量增加,工作线程来不及处理任务时,线程池需要适量地增加一些工作线程;当用户请求量降低了,也可以逐步减少工作线程的数量。同时,应该设定线程池中工作线程数量的上限和下限,保证提高系统的性能。 
  创建多个线程池,对不同优先级的任务分别进行处理。 
  线程池的适用范围。需要创建大量线程来完成的任务,而且每个线程处理任务的时间比较短。比如,Web服务器响应网页请求这样的任务,原先服务器需要为每个请求创建一个线程,但是线程完成网页请求的时间较短,总体上花在线程创建和销毁上的时间却很多,而且不会限制线程创建的数量,严重占用了服务器的资源,并最终会影响网页请求的速度。所以,此时使用线程池是比较合适的。 
  总结,线程池的创建就是为了限制线程的数目,使已经创建的线程可以重复使用,保护系统的资源。 
  参考:http://www.ibm.com/developerworks/cn/java/l-threadPool/ 
  在Java中使用线程池示例: 
  Java并发包:java.util.concurrent,其中主要的有以下内容。 
  1)Executor:线程池的顶级接口,用来执行一个线程任务。 
  2)ExecutorService:真正的线程池接口。 
  3)Executors:提供生成各种线程池的静态工厂。 
  4)newSingleThreadExecutor:单线程线程池。 
  5)newFixedThreadPool:固定大小线程池。 
  6)newCachedThreadPool:可缓存线程池,能动态增减的线程池。 
  7)newScheduledThreadPool:无限大小可定时执行任务的线程池。

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

Public class TestSingleThreadExecutor { public static void 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正在执行。。。

参考:http://www.oschina.net/question/565065_86540

69.Java中的数据库连接池实现原理。 
  数据库连接池的思想是事先建立若干条数据库的连接,将它们放到连接池中,当有线程需要连接数据库的时候,按照一定的策略取得连接,使用完以后,再将连接返回给连接池,避免了连接的频繁建立和关闭,达到数据库连接高效,安全,复用的效果。 
  数据库连接池的实现主要分为三个部分:建立、连接管理、关闭。 
  连接池的建立:一般在系统初始化的时候,数据库连接池就会按照配置文件进行数据库连接的创建,供程序在使用时可以从连接池中获取。这些连接不能被随意的创建和关闭,避免额外的系统开销。连接池往往使用Vector来实现。 
  连接池的管理:当客户请求数据库连接时,首先查看连接池中是否有空闲连接,如果存在空闲连接,则将连接分配给客户使用;如果没有空闲连接,则查看当前所开的连接数是否已经达到最大连接数,例如如果没有达到就重新创建一个;如果达到,就按设定的最大等待时间进行等待,如果超出最大等待时间还没有空闲的连接,则抛出异常给客户。 
  连接池的关闭:当系统退出时,就关闭连接池中的所有连接,释放其它连接池资源,该过程和连接池的创建过程相反。 
  http://www.cnblogs.com/newpanderking/p/3875749.html

70.关于继承中的静态代码块,构造代码块和构造函数的执行先后顺序

public class A { public A() { System.out.println("A constructor method!"); } static { System.out.println("A static code!"); } { System.out.println("A code!"); }}

public class B extends A { public B(){ System.out.println("B constructor method!"); } static { System.out.println("B static code!"); } { System.out.println("B Code!"); }}

public class Test { public static void main(String[] args) { B b = new B(); }}

  执行结果:

A static code!B static code!A code!A constructor method!B Code!B constructor method!

  结果分析: 
  静态代码块是在类加载的过程中执行的,是给类变量进行初始化,只执行一次。 
  构造代码块是给所有对象进行统一初始化,构造代码块中包含所有对象共有的实例变量,Java会把构造代码块放到每种构造方法的前面部分,这样每次调用构造函数都会先调用构造代码块,减少了代码量。 
  构造函数是给指定对象进行初始化。

71.下面程序的执行结果是

public static void main(String[] args) { try { int i = 100 / 0; System.out.println(1); } catch (Exception e) { System.out.println(2); //throw new RuntimeException(); } finally { System.out.println(3); } System.out.println(4); }

  结果为:

234

public static void main(String[] args) { try { int i = 100 / 0; System.out.println(1); } catch (Exception e) { System.out.println(2); throw new RuntimeException(); } finally { System.out.println(3); } System.out.println(4); }

  结果是:

23

  throw异常之后就不会再执行后续的代码! 
  参考: 
  http://blog.csdn.net/u012050416/article/details/50781426

72.sleep()和wait()的区别。 
  1)这两个方法来自不同的类分别是Thread和Object。 
  2)sleep方法没有释放对象锁,而wait方法释放了锁,使得其它线程可以使用同步控制块或者方法。 
  3)wait,notify和notifyAll只能在同步方法或者同步代码块里面使用,而sleep可以在任何地方使用。 
  4)sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。

73.关于java.util.concurrent的知识点 
  concurrent包是JDK1.5开始增加的内容,它的引入大大简化了多线程程序的开发。主要分成三个部分: 
  java.util.concurrent 
  java.util.concurrent.atomic 
  java.util.concurrent.lock 
  这些主要包含了“并发集合类”,“线程池机制”,“同步互斥机制”,“线程安全变量更新工具类”和“锁”等常用的工具。 
  Executors 
  通过这个类可以获取多种类型的线程池实例:

ExecutorService service = Executors.newFixedThreadPool(2);ExecutorService service = Executors.newSingleThreadExecutor();ExecutorService service = Executors.newCachedThreadPool();ExecutorService service = Executors.newScheduledThreadPool(10);

  有了ExecutorService对象以后,就可以调用execute,submit,invokeAll, invokeAny等方法来启动实现了Runnable或者Callable接口的对象线程。而通过Thread.start()或者service.execute()是不会有返回值的,service.invokeAll()和service.invokeAny()是有返回值的。 
  Lock 
  多线程编程中需要锁定某个对象,一般都会使用synchronized,现在可以使用java.util.concurrent.locks来实现。 
  使用lock需要自己在合适的地方获取锁然后用完以后手动释放锁,这一点synchronized是自动释放锁的。通常使用方式如下:

//一个需要类型的锁Lock lock = new ReentrantLock()//锁定当前对象lock.lock();try{...} finally {//手工解锁lock.unlock();}

  并发集合类 
  concurrent包中提供了几个并发集合类:ConcurrentHashMap,ConcurrentLinkedQueue,CopyOnWriteArrayList等等。我们可以根据不同的使用场景,用其中的集合类代替java.util包中相应的集合类。 
  AtomicInteger 
  在多线程编程中对某些变量的操作要求具有原子性,即某个变量的修改过程必须不能被打断。如:i++操作包含的过程有,获取i的原始值,自增操作,写回i,返回原始值等步骤。如果在i++的过程中不进行同步控制,某一个步骤被打断,那么会引起一些问题。而java.concurrent.atomic为我们提供了很多工具,可以保证以原子方式更新变量。

74.ThreadLocal是什么? 
  当使用ThreadLocal维护局部变量的时候,它为每个使用该变量的线程提供独立的变量副本,所以每个线程都可以独立地改变自己的变量副本而不会影响其它线程所对应的变量副本。 
  它的内部实现机制是使用了一个map来存储每一个线程的变量副本,其中map元素的键为各个线程对象,值为对应线程的变量副本。

75.Callable和Runnable的区别 
  Callable类似与Runnable接口,实现Callable接口的类表示是可以被其它线程执行的任务。 
  区别: 
  1)Callable定义的方法是call,Runnable定义的方法是run。 
  2)Callable的call方法可以有返回值,Runnable的run方法没有返回值。 
  3)Callable的call方法可以抛出异常,Runnable的run方法不能抛出异常。 
  Tips:顺便讲一下Future。 
  Future表示异步计算的结果,它提供检查计算是否完成的方法,并检索计算的结果。Future的cancel()可以取消任务的执行,get()是等待计算完成并获取计算结果,isDone()在任务完成情况下返回true。 
 http://www.cnblogs.com/whgw/archive/2011/09/28/2194760.html

76.java中如何保护父类的方法不会被子类重写? 
  1)使用final修饰符。 
  2)将该方法声明为private,这样,即使子类中有相同的方法,那也是子类自己定义的,和父类中的这个方法,没有关系。

77.Java的内存模型 
参考:http://blog.csdn.net/ccit0519/article/details/11241403

78.Java中volatile的使用 
  首先,只有成员变量才能使用volatile进行修饰。未使用volatile修饰的变量在多线程情况下对其它线程都是透明的,而使用了volatile关键字的变量对其它线程是可见的。我们知道,每个线程都会有自己的栈用来存放局部变量,每次线程运行前,都会从主内存区加载变量值到自己的线程栈中形成一个变量副本,那么如果是没有使用volatile的透明情况,各个线程不知道自己在使用副本变量时主内存的变量值也许被其它线程修改了,各个线程只是使用自己的本地变量副本,从而造成不同步的情况。然而,使用volatile后,主内存区的变量修改对于各个线程是可见的,那么线程在使用自己的本地变量副本前会去读取修改后的变量值进行操作。 
  然而,使用volatile关键字并不能解决并发的问题,比如有这样的情况,线程A和线程B发现主内存的变量已经更改,于是都加载这个最新的值,线程A将变量进行修改,先放到本地副本中,此时挂起,未来得及写回主内存,线程B以为当前副本值是最新值,也修改变量,然后写回主内存,此时线程A再将修改后的变量写回主内存。 
  所以,可以说,volatile关键字只是保证了可见性,并不能保证原子性。如果想要实现变量修改的原子性,可以使用synchronized或者使用java.util.concurrent包中的AtomicInteger类。 
 http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html

79.Java中如何停止一个线程? 
  原先Java中是有stop,suspend等方法来停止线程的,但是都存在死锁的威胁,所以都被弃用了。 
  一般,当一个线程的run()或者call()方法执行完了线程会自动终止。 
  或者,如果想终止线程中的循环,可以定义一个volatile的boolean值,想要结束线程的话,设置这个boolean值为ture,然后利用循环的判断条件使循环继续的条件为false。 
  还可以调用Thread的interrupt方法。 
  http://blog.csdn.net/anhuidelinger/article/details/11746365

80.interrupted()和isInterrupted()有什么区别? 
  通过调用Thread.interrupt()可以用来中断当前的线程,然后使用这两方法判断某线程是否中断。区别在于: 
  interrupted()是静态方法,在判断某个线程是否中断以后会消除该线程的中断状态。 
  isInterrupted()不是静态方法,不会改变该线程的中断状态。

posted @ 2016-11-02 14:21  黄小易  阅读(244)  评论(0编辑  收藏  举报