近5年常考Java面试题及答案整理(二)

上一篇:近5年常考Java面试题及答案整理(一)

3一、String s = new String("xyz");建立了几个字符串对象?html

答:两个对象,一个是静态区的"xyz",一个是用new建立在堆上的对象。java

3二、接口是否可继承(extends)接口?抽象类是否可实现(implements)接口?抽象类是否可继承具体类(concrete class)?程序员

答:接口能够继承接口,并且支持多重继承。抽象类能够实现(implements)接口,抽象类可继承具体类也能够继承抽象类。面试

举一个多继承的例子,咱们定义一个动物(类)既是狗(父类1)也是猫(父类2),两个父类都有“叫”这个方法。那么当咱们调用“叫”这个方法时,它就不知道是狗叫仍是猫叫了,这就是多重继承的冲突。
而接口没有具体的方法实现,因此多继承接口也不会出现这种冲突。算法

3三、一个".java"源文件中是否能够包含多个类(不是内部类)?有什么限制?数据库

答:能够,但一个源文件中最多只能有一个公开类(public class)并且文件名必须和公开类的类名彻底保持一致。编程

3四、Anonymous Inner Class(匿名内部类)是否能够继承其它类?是否能够实现接口?数组

答:能够继承其余类或实现其余接口,在Swing编程和Android开发中经常使用此方式来实现事件监听和回调。浏览器

3五、内部类能够引用它的包含类(外部类)的成员吗?有没有什么限制?缓存

答:一个内部类对象能够访问建立它的外部类对象的成员,包括私有成员。

3六、Java 中的final关键字有哪些用法?

答:(1)修饰类:表示该类不能被继承;(2)修饰方法:表示方法不能被重写;(3)修饰变量:表示变量只能一次赋值之后值不能被修改(常量)。

3七、指出下面程序的运行结果。

 1 class A {  2  
 3     static {  4         System.out.print("1");  5  }  6  
 7     public A() {  8         System.out.print("2");  9  } 10 } 11  
12 class B extends A{ 13  
14     static { 15         System.out.print("a"); 16  } 17  
18     public B() { 19         System.out.print("b"); 20  } 21 } 22  
23 public class Hello { 24  
25     public static void main(String[] args) { 26         A ab = new B(); 27         ab = new B(); 28  } 29  
30 }

答:执行结果:1a2b2b。建立对象时构造器的调用顺序是:先初始化静态成员,而后调用父类构造器,再初始化非静态成员,最后调用自身构造器。

提示:若是不能给出此题的正确答案,说明以前第21题Java类加载机制尚未彻底理解,赶忙再看看吧。

3八、数据类型之间的转换:
如何将字符串转换为基本数据类型?
如何将基本数据类型转换为字符串?

答:

  • 调用基本数据类型对应的包装类中的方法parseXXX(String)或valueOf(String)便可返回相应基本类型;
  • 一种方法是将基本数据类型与空字符串("")链接(+)便可得到其所对应的字符串;另外一种方法是调用String 类中的valueOf()方法返回相应字符串

3九、如何实现字符串的反转及替换?

答:方法不少,能够本身写实现也可使用String或StringBuffer/StringBuilder中的方法。有一道很常见的面试题是用递归实现字符串反转,代码以下所示:

1 public static String reverse(String originStr) { 2     if(originStr == null || originStr.length() <= 1) 3         return originStr; 4     return reverse(originStr.substring(1)) + originStr.charAt(0); 5 }

40、怎样将GB2312编码的字符串转换为ISO-8859-1编码的字符串?

答:代码以下所示:

1 String s1 = "你好"; 2 String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");

4一、日期和时间:
如何取得年月日、小时分钟秒?
如何取得从1970年1月1日0时0分0秒到如今的毫秒数?
如何取得某月的最后一天?
如何格式化日期?

答:
问题1:建立java.util.Calendar 实例,调用其get()方法传入不一样的参数便可得到参数所对应的值。Java 8中可使用java.time.LocalDateTimel来获取,代码以下所示。

 1 public class DateTimeTest {  2     public static void main(String[] args) {  3         Calendar cal = Calendar.getInstance();  4  System.out.println(cal.get(Calendar.YEAR));  5         System.out.println(cal.get(Calendar.MONTH));    // 0 - 11
 6  System.out.println(cal.get(Calendar.DATE));  7  System.out.println(cal.get(Calendar.HOUR_OF_DAY));  8  System.out.println(cal.get(Calendar.MINUTE));  9  System.out.println(cal.get(Calendar.SECOND)); 10  
11         // Java 8
12         LocalDateTime dt = LocalDateTime.now(); 13  System.out.println(dt.getYear()); 14         System.out.println(dt.getMonthValue());     // 1 - 12
15  System.out.println(dt.getDayOfMonth()); 16  System.out.println(dt.getHour()); 17  System.out.println(dt.getMinute()); 18  System.out.println(dt.getSecond()); 19  } 20 }

问题2:如下方法都可得到该毫秒数。

1 Calendar.getInstance().getTimeInMillis(); 2 System.currentTimeMillis(); 3 Clock.systemDefaultZone().millis(); // Java 8

问题3:代码以下所示。

1 Calendar time = Calendar.getInstance(); 2 time.getActualMaximum(Calendar.DAY_OF_MONTH);

问题4:利用java.text.DataFormat 的子类(如SimpleDateFormat类)中的format(Date)方法可将日期格式化。Java 8中能够用java.time.format.DateTimeFormatter来格式化时间日期,代码以下所示。

 1 import java.text.SimpleDateFormat;  2 import java.time.LocalDate;  3 import java.time.format.DateTimeFormatter;  4 import java.util.Date;  5  
 6 class DateFormatTest {  7  
 8     public static void main(String[] args) {  9         SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd"); 10         Date date1 = new Date(); 11  System.out.println(oldFormatter.format(date1)); 12  
13         // Java 8
14         DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd"); 15         LocalDate date2 = LocalDate.now(); 16  System.out.println(date2.format(newFormatter)); 17  } 18 }

补充:Java的时间日期API一直以来都是被诟病的东西,为了解决这一问题,Java 8中引入了新的时间日期API,其中包括LocalDate、LocalTime、LocalDateTime、Clock、Instant等类,这些的类的设计都使用了不变模式,所以是线程安全的设计。

4二、打印昨天的当前时刻。

1 import java.util.Calendar; 2  
3 class YesterdayCurrent { 4     public static void main(String[] args){ 5         Calendar cal = Calendar.getInstance(); 6         cal.add(Calendar.DATE, -1); 7  System.out.println(cal.getTime()); 8  } 9 }

在Java 8中,能够用下面的代码实现相同的功能。

 1 import java.time.LocalDateTime;  2  
 3 class YesterdayCurrent {  4  
 5     public static void main(String[] args) {  6         LocalDateTime today = LocalDateTime.now();  7         LocalDateTime yesterday = today.minusDays(1);  8  
 9  System.out.println(yesterday); 10  } 11 }

4三、比较一下Java和JavaSciprt。

答:JavaScript 与Java是两个公司开发的不一样的两个产品。Java 是原Sun Microsystems公司推出的面向对象的程序设计语言,特别适合于互联网应用程序开发;而JavaScript是Netscape公司的产品,为了扩展Netscape浏览器的功能而开发的一种能够嵌入Web页面中运行的基于对象和事件驱动的解释性语言。JavaScript的前身是LiveScript;而Java的前身是Oak语言。
下面对两种语言间的异同做以下比较:

  • 基于对象和面向对象:Java是一种真正的面向对象的语言,即便是开发简单的程序,必须设计对象;JavaScript是种脚本语言,它能够用来制做与网络无关的,与用户交互做用的复杂软件。它是一种基于对象(Object-Based)和事件驱动(Event-Driven)的编程语言,于是它自己提供了很是丰富的内部对象供设计人员使用。
  • 解释和编译:Java的源代码在执行以前,必须通过编译。JavaScript是一种解释性编程语言,其源代码不需通过编译,由浏览器解释执行。(目前的浏览器几乎都使用了JIT(即时编译)技术来提高JavaScript的运行效率)
  • 强类型变量和弱类型变量:Java采用强类型变量检查,即全部变量在编译以前必须做声明;JavaScript中变量是弱类型的,甚至在使用变量前能够不做声明,JavaScript的解释器在运行时检查推断其数据类型。
  • 代码格式不同。

补充:上面列出的四点是网上流传的所谓的标准答案。其实Java和JavaScript最重要的区别是一个是静态语言,一个是动态语言。目前的编程语言的发展趋势是函数式语言和动态语言。在Java中类(class)是一等公民,而JavaScript中函数(function)是一等公民,所以JavaScript支持函数式编程,可使用Lambda函数和闭包(closure),固然Java 8也开始支持函数式编程,提供了对Lambda表达式以及函数式接口的支持。对于这类问题,在面试的时候最好仍是用本身的语言回答会更加靠谱,不要背网上所谓的标准答案。

4四、何时用断言(assert)?

答:断言在软件开发中是一种经常使用的调试方式,不少开发语言中都支持这种机制。通常来讲,断言用于保证程序最基本、关键的正确性。断言检查一般在开发和测试时开启。为了保证程序的执行效率,在软件发布后断言检查一般是关闭的。断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为true;若是表达式的值为false,那么系统会报告一个AssertionError。断言的使用以下面的代码所示:

1 assert(a > 0); // throws an AssertionError if a <= 0

断言能够有两种形式:
assert Expression1;
assert Expression1 : Expression2 ;
Expression1 应该老是产生一个布尔值。
Expression2 能够是得出一个值的任意表达式;这个值用于生成显示更多调试信息的字符串消息。

要在运行时启用断言,能够在启动JVM时使用-enableassertions或者-ea标记。要在运行时选择禁用断言,能够在启动JVM时使用-da或者-disableassertions标记。要在系统类中启用或禁用断言,可以使用-esa或-dsa标记。还能够在包的基础上启用或者禁用断言。

注意:断言不该该以任何方式改变程序的状态。简单的说,若是但愿在不知足某些条件时阻止代码的执行,就能够考虑用断言来阻止它。

4五、Error和Exception有什么区别?

答:Error表示系统级的错误和程序没必要处理的异常,是恢复不是不可能但很困难的状况下的一种严重问题;好比内存溢出,不可能期望程序能处理这样的状况;Exception表示须要捕捉或者须要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示若是程序运行正常,从不会发生的状况。

面试题:2005年摩托罗拉的面试中曾经问过这么一个问题“If a process reports a stack overflow run-time error, what’s the most possible cause?”,给了四个选项a. lack of memory; b. write on an invalid memory space; c. recursive function calling; d. array index out of boundary. Java程序在运行时也可能会遭遇StackOverflowError,这是一个没法恢复的错误,只能从新修改代码了,这个面试题的答案是c。若是写了不能迅速收敛的递归,则颇有可能引起栈溢出的错误,以下所示:

1 class StackOverflowErrorTest { 2  
3     public static void main(String[] args) { 4         main(null); 5  } 6 }

提示:用递归编写程序时必定要牢记两点:1. 递归公式;2. 收敛条件(何时就再也不继续递归)。

4六、try{}里有一个return语句,那么紧跟在这个try后的finally{}里的代码会不会被执行,何时被执行,在return前仍是后?

答:会执行,在方法返回前执行。

注意:在finally中改变返回值的作法是很差的,由于若是存在finally代码块,try中的return语句不会立马返回调用者,而是记录下返回值待finally代码块执行完毕以后再向调用者返回其值,而后若是在finally中修改了返回值,就会返回修改后的值。显然,在finally中返回或者修改返回值会对程序形成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java中也能够经过提高编译器的语法检查级别来产生警告或错误,Eclipse中能够在如图所示的地方进行设置,强烈建议将此项设置为编译错误。

4七、Java语言如何进行异常处理,关键字:throws、throw、try、catch、finally分别如何使用?

答:Java经过面向对象的方法进行异常处理,把各类不一样的异常进行分类,并提供了良好的接口。在Java中,每一个异常都是一个对象,它是Throwable类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法能够捕获到这个异常并能够对其进行处理。Java的异常处理是经过5个关键词来实现的:try、catch、throw、throws和finally。通常状况下是用try来执行一段程序,若是系统会抛出(throw)一个异常对象,能够经过它的类型来捕获(catch)它,或经过老是执行代码块(finally)来处理;try用来指定一块预防全部异常的程序;catch子句紧跟在try块后面,用来指定你想要捕获的异常的类型;throw语句用来明确地抛出一个异常;throws用来声明一个方法可能抛出的各类异常(固然声明异常时容许无病呻吟);finally为确保一段代码无论发生什么异常情况都要被执行;try语句能够嵌套,每当遇到一个try语句,异常的结构就会被放入异常栈中,直到全部的try语句都完成。若是下一级的try语句没有对某种异常进行处理,异常栈就会执行出栈操做,直到遇到有处理这种异常的try语句或者最终将异常抛给JVM。

4八、运行时异常与受检异常有何异同?

答:异常表示程序运行过程当中可能出现的非正常状态,运行时异常表示虚拟机的一般操做中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题一般就不会发生。受检异常跟程序运行的上下文环境有关,即便程序设计无误,仍然可能因使用的问题而引起。Java编译器要求方法必须声明抛出可能发生的受检异常,可是并不要求必须声明抛出未被捕获的运行时异常。异常和继承同样,是面向对象程序设计中常常被滥用的东西,在Effective Java中对异常的使用给出了如下指导原则:

  • 不要将异常处理用于正常的控制流(设计良好的API不该该强迫它的调用者为了正常的控制流而使用异常)
  • 对能够恢复的状况使用受检异常,对编程错误使用运行时异常
  • 避免没必要要的使用受检异常(能够经过一些状态检测手段来避免异常的发生)
  • 优先使用标准的异常
  • 每一个方法抛出的异常都要有文档
  • 保持异常的原子性
  • 不要在catch中忽略掉捕获到的异常

4九、列出一些你常见的运行时异常?

答:

  • ArithmeticException(算术异常)
  • ClassCastException (类转换异常)
  • IllegalArgumentException (非法参数异常)
  • IndexOutOfBoundsException (下标越界异常)
  • NullPointerException (空指针异常)
  • SecurityException (安全异常)

50、阐述final、finally、finalize的区别。

答:

  • final:修饰符(关键字)有三种用法:若是一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,所以它和abstract是反义词。将变量声明为final,能够保证它们在使用中不被改变,被声明为final的变量必须在声明时给定初值,而在之后的引用中只能读取不可修改。被声明为final的方法也一样只能使用,不能在子类中被重写。
  • finally:一般放在try…catch…的后面构造老是执行代码块,这就意味着程序不管正常执行仍是发生异常,这里的代码只要JVM不关闭都能执行,能够将释放外部资源的代码写在finally块中。
  • finalize:Object类中定义的方法,Java中容许使用finalize()方法在垃圾收集器将对象从内存中清除出去以前作必要的清理工做。这个方法是由垃圾收集器在销毁对象时调用的,经过重写finalize()方法能够整理系统资源或者执行其余清理工做。

5一、类ExampleA继承Exception,类ExampleB继承ExampleA。
有以下代码片段:

1 try { 2     throw new ExampleB("b") 3 } catch(ExampleA e){ 4     System.out.println("ExampleA"); 5 } catch(Exception e){ 6     System.out.println("Exception"); 7 }

请问执行此段代码的输出是什么?

答:输出:ExampleA。(根据里氏代换原则[能使用父类型的地方必定能使用子类型],抓取ExampleA类型异常的catch块可以抓住try块中抛出的ExampleB类型的异常)

面试题 - 说出下面代码的运行结果。(此题的出处是《Java编程思想》一书)

 1 class Annoyance extends Exception {}  2 class Sneeze extends Annoyance {}  3  
 4 class Human {  5  
 6     public static void main(String[] args)  7         throws Exception {  8         try {  9             try { 10                 throw new Sneeze(); 11  } 12             catch ( Annoyance a ) { 13                 System.out.println("Caught Annoyance"); 14                 throw a; 15  } 16  } 17         catch ( Sneeze s ) { 18             System.out.println("Caught Sneeze"); 19             return ; 20  } 21         finally { 22             System.out.println("Hello World!"); 23  } 24  } 25 }

5二、List、Set、Map是否继承自Collection接口?

答:List、Set 是,Map 不是。Map是键值对映射容器,与List和Set有明显的区别,而Set存储的零散的元素且不容许有重复元素(数学中的集合也是如此),List是线性结构的容器,适用于按数值索引访问元素的情形。

5三、阐述ArrayList、Vector、LinkedList的存储性能和特性。

答:ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增长和插入元素,它们都容许直接按序号索引元素,可是插入元素要涉及数组元素移动等内存操做,因此索引数据快而插入数据慢,Vector中的方法因为添加了synchronized修饰,所以Vector是线程安全的容器,但性能上较ArrayList差,所以已是Java中的遗留容器。LinkedList使用双向链表实现存储(将内存中零散的内存单元经过附加的引用关联起来,造成一个能够按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据须要进行前向或后向遍历,可是插入数据时只须要记录本项的先后项便可,因此插入速度较快。Vector属于遗留容器(Java早期的版本中提供的容器,除此以外,Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),已经不推荐使用,可是因为ArrayList和LinkedListed都是非线程安全的,若是遇到多个线程操做同一个容器的场景,则能够经过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另外一个类的构造器中建立新的对象来加强实现)。

补充:遗留容器中的Properties类和Stack类在设计上有严重的问题,Properties是一个键和值都是字符串的特殊的键值对映射,在设计上应该是关联一个Hashtable并将其两个泛型参数设置为String类型,可是Java API中的Properties直接继承了Hashtable,这很明显是对继承的滥用。这里复用代码的方式应该是Has-A关系而不是Is-A关系,另外一方面容器都属于工具类,继承工具类自己就是一个错误的作法,使用工具类最好的方式是Has-A关系(关联)或Use-A关系(依赖)。同理,Stack类继承Vector也是不正确的。Sun公司的工程师们也会犯这种低级错误,让人唏嘘不已。

5四、Collection和Collections的区别?

答:Collection是一个接口,它是Set、List等容器的父接口;Collections是个一个工具类,提供了一系列的静态方法来辅助容器操做,这些方法包括对容器的搜索、排序、线程安全化等等。

5五、List、Map、Set三个接口存取元素时,各有什么特色?

答:List以特定索引来存取元素,能够有重复元素。Set不能存放重复元素(用对象的equals()方法来区分元素是否重复)。Map保存键值对(key-value pair)映射,映射关系能够是一对一或多对一。Set和Map容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为O(1),而基于排序树版本的实如今插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。

5六、TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?

答:TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象必须实现Comparable接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较,可是要求传入第二个参数,参数是Comparator接口的子类型(须要重写compare方法实现元素的比较),至关于一个临时定义的排序规则,其实就是经过接口注入比较元素大小的算法,也是对回调模式的应用(Java中对函数式编程的支持)。
例子1:

 1 public class Student implements Comparable<Student> {  2     private String name;        // 姓名
 3     private int age;            // 年龄
 4  
 5     public Student(String name, int age) {  6         this.name = name;  7         this.age = age;  8  }  9  
10  @Override 11     public String toString() { 12         return "Student [name=" + name + ", age=" + age + "]"; 13  } 14  
15  @Override 16     public int compareTo(Student o) { 17         return this.age - o.age; // 比较年龄(年龄的升序)
18  } 19  
20 }

 

 1 import java.util.Set;  2 import java.util.TreeSet;  3  
 4 class Test01 {  5  
 6     public static void main(String[] args) {  7         Set<Student> set = new TreeSet<>();     // Java 7的钻石语法(构造器后面的尖括号中不须要写类型)
 8         set.add(new Student("Hao LUO", 33));  9         set.add(new Student("XJ WANG", 32)); 10         set.add(new Student("Bruce LEE", 60)); 11         set.add(new Student("Bob YANG", 22)); 12  
13         for(Student stu : set) { 14  System.out.println(stu); 15  } 16 // 输出结果: 17 // Student [name=Bob YANG, age=22] 18 // Student [name=XJ WANG, age=32] 19 // Student [name=Hao LUO, age=33] 20 // Student [name=Bruce LEE, age=60]
21  } 22 }

例子2:

 1 public class Student {  2     private String name;    // 姓名
 3     private int age;        // 年龄
 4  
 5     public Student(String name, int age) {  6         this.name = name;  7         this.age = age;  8  }  9  
10     /**
11  * 获取学生姓名 12      */
13     public String getName() { 14         return name; 15  } 16  
17     /**
18  * 获取学生年龄 19      */
20     public int getAge() { 21         return age; 22  } 23  
24  @Override 25     public String toString() { 26         return "Student [name=" + name + ", age=" + age + "]"; 27  } 28  
29 }

 

 1 import java.util.ArrayList;  2 import java.util.Collections;  3 import java.util.Comparator;  4 import java.util.List;  5  
 6 class Test02 {  7  
 8     public static void main(String[] args) {  9         List<Student> list = new ArrayList<>();     // Java 7的钻石语法(构造器后面的尖括号中不须要写类型)
10         list.add(new Student("Hao LUO", 33)); 11         list.add(new Student("XJ WANG", 32)); 12         list.add(new Student("Bruce LEE", 60)); 13         list.add(new Student("Bob YANG", 22)); 14  
15         // 经过sort方法的第二个参数传入一个Comparator接口对象 16         // 至关因而传入一个比较对象大小的算法到sort方法中 17         // 因为Java中没有函数指针、仿函数、委托这样的概念 18         // 所以要将一个算法传入一个方法中惟一的选择就是经过接口回调
19         Collections.sort(list, new Comparator<Student> () { 20  
21  @Override 22             public int compare(Student o1, Student o2) { 23                 return o1.getName().compareTo(o2.getName());    // 比较学生姓名
24  } 25  }); 26  
27         for(Student stu : list) { 28  System.out.println(stu); 29  } 30 // 输出结果: 31 // Student [name=Bob YANG, age=22] 32 // Student [name=Bruce LEE, age=60] 33 // Student [name=Hao LUO, age=33] 34 // Student [name=XJ WANG, age=32]
35  } 36 }

5七、Thread类的sleep()方法和对象的wait()方法均可以让线程暂停执行,它们有什么区别?

答:sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其余线程,可是对象的锁依然保持,所以休眠时间结束后会自动恢复(线程回到就绪状态,请参考第66题中的线程状态转换图)。wait()是Object类的方法,调用对象的wait()方法致使当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),若是线程从新得到对象的锁就能够进入就绪状态。

补充:可能很多人对什么是进程,什么是线程还比较模糊,对于为何须要多线程编程也不是特别理解。简单的说:进程是具备必定独立功能的程序关于某个数据集合上的一次运行活动,是操做系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时一般拥有独立的内存单元,而线程之间能够共享内存。使用多线程的编程一般可以带来更好的性能和用户体验,可是多线程的程序对于其余程序是不友好的,由于它可能占用了更多的CPU资源。固然,也不是线程越多,程序的性能就越好,由于线程之间的调度和切换也会浪费CPU时间。时下很时髦的Node.js就采用了单线程异步I/O的工做模式。

5八、线程的sleep()方法和yield()方法有什么区别?

答:
① sleep()方法给其余线程运行机会时不考虑线程的优先级,所以会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操做系统CPU调度相关)具备更好的可移植性。

5九、当一个线程进入一个对象的synchronized方法A以后,其它线程是否可进入此对象的synchronized方法B?

答:不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。由于非静态方法上的synchronized修饰符要求执行方法时要得到对象的锁,若是已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。

60、请说出与线程同步以及线程调度相关的方法。

答:

  • wait():使一个线程处于等待(阻塞)状态,而且释放所持有的对象的锁;
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
  • notify():唤醒一个处于等待状态的线程,固然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM肯定唤醒哪一个线程,并且与优先级无关;
  • notityAll():唤醒全部处于等待状态的线程,该方法并非将对象的锁给全部线程,而是让它们竞争,只有得到锁的线程才能进入就绪状态;

补充:Java 5经过Lock接口提供了显式的锁机制(explicit lock),加强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通讯的Condition对象;此外,Java 5还提供了信号量机制(semaphore),信号量能够用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问以前,线程必须获得信号量的许可(调用Semaphore对象的acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。

下面的例子演示了100个线程同时向一个银行帐户中存入1元钱,在没有使用同步机制和使用同步机制状况下的执行状况。

银行帐户类:

 1 /**
 2  * 银行帐户  3  * @author nnngu  4  *  5  */
 6 public class Account {  7     private double balance;     // 帐户余额
 8  
 9     /**
10  * 存款 11  * @param money 存入金额 12      */
13     public void deposit(double money) { 14         double newBalance = balance + money; 15         try { 16             Thread.sleep(10);   // 模拟此业务须要一段处理时间
17  } 18         catch(InterruptedException ex) { 19  ex.printStackTrace(); 20  } 21         balance = newBalance; 22  } 23  
24     /**
25  * 得到帐户余额 26      */
27     public double getBalance() { 28         return balance; 29  } 30 }

存钱线程类:

 1 /**
 2  * 存钱线程  3  * @author nnngu  4  *  5  */
 6 public class AddMoneyThread implements Runnable {  7     private Account account;    // 存入帐户
 8     private double money;       // 存入金额
 9  
10     public AddMoneyThread(Account account, double money) { 11         this.account = account; 12         this.money = money; 13  } 14  
15  @Override 16     public void run() { 17  account.deposit(money); 18  } 19  
20 }

测试类:

 1 import java.util.concurrent.ExecutorService;  2 import java.util.concurrent.Executors;  3  
 4 public class Test01 {  5  
 6     public static void main(String[] args) {  7         Account account = new Account();  8         ExecutorService service = Executors.newFixedThreadPool(100);  9  
10         for(int i = 1; i <= 100; i++) { 11             service.execute(new AddMoneyThread(account, 1)); 12  } 13  
14  service.shutdown(); 15  
16         while(!service.isTerminated()) {} 17  
18         System.out.println("帐户余额: " + account.getBalance()); 19  } 20 }

在没有同步的状况下,执行结果一般是显示帐户余额在10元如下,出现这种情况的缘由是,当一个线程A试图存入1元的时候,另一个线程B也可以进入存款的方法中,线程B读取到的帐户余额仍然是线程A存入1元钱以前的帐户余额,所以也是在原来的余额0上面作了加1元的操做,同理线程C也会作相似的事情,因此最后100个线程执行结束时,原本指望帐户余额为100元,但实际获得的一般在10元如下(极可能是1元哦)。解决这个问题的办法就是同步,当一个线程对银行帐户存钱时,须要将此帐户锁定,待其操做完成后才容许其余的线程进行操做,代码有以下几种调整方案:

在银行帐户的存款(deposit)方法上加同步(synchronized)关键字

 1 /**
 2  * 银行帐户  3  * @author 张凯  4  *  5  */
 6 public class Account {  7     private double balance;     // 帐户余额
 8  
 9     /**
10  * 存款 11  * @param money 存入金额 12      */
13     public synchronized void deposit(double money) { 14         double newBalance = balance + money; 15         try { 16             Thread.sleep(10);   // 模拟此业务须要一段处理时间
17  } 18         catch(InterruptedException ex) { 19  ex.printStackTrace(); 20  } 21         balance = newBalance; 22  } 23  
24     /**
25  * 得到帐户余额 26      */
27     public double getBalance() { 28         return balance; 29  } 30 }

在线程调用存款方法时对银行帐户进行同步

 1 /**
 2  * 存钱线程  3  * @author 张凯  4  *  5  */
 6 public class AddMoneyThread implements Runnable {  7     private Account account;    // 存入帐户
 8     private double money;       // 存入金额
 9  
10     public AddMoneyThread(Account account, double money) { 11         this.account = account; 12         this.money = money; 13  } 14  
15  @Override 16     public void run() { 17         synchronized (account) { 18  account.deposit(money); 19  } 20  } 21  
22 }

经过Java 5显示的锁机制,为每一个银行帐户建立一个锁对象,在存款操做进行加锁和解锁的操做

 1 import java.util.concurrent.locks.Lock;  2 import java.util.concurrent.locks.ReentrantLock;  3  
 4 /**
 5  * 银行帐户  6  *  7  * @author 张凯  8  *  9  */
10 public class Account { 11     private Lock accountLock = new ReentrantLock(); 12     private double balance; // 帐户余额
13  
14     /**
15  * 存款 16  * 17  * @param money 18  * 存入金额 19      */
20     public void deposit(double money) { 21  accountLock.lock(); 22         try { 23             double newBalance = balance + money; 24             try { 25                 Thread.sleep(10); // 模拟此业务须要一段处理时间
26  } 27             catch (InterruptedException ex) { 28  ex.printStackTrace(); 29  } 30             balance = newBalance; 31  } 32         finally { 33  accountLock.unlock(); 34  } 35  } 36  
37     /**
38  * 得到帐户余额 39      */
40     public double getBalance() { 41         return balance; 42  } 43 }

按照上述三种方式对代码进行修改后,重写执行测试代码Test01,将看到最终的帐户余额为100元。固然也可使用Semaphore或CountdownLatch来实现同步。

6一、编写多线程程序有几种实现方式?

答:Java 5之前实现多线程有两种实现方法:一种是继承Thread类;另外一种是实现Runnable接口。两种方式都要经过重写run()方法来定义线程的行为,推荐使用后者,由于Java中的继承是单继承,一个类有一个父类,若是继承了Thread类就没法再继承其余类了,显然使用Runnable接口更为灵活。

补充:Java 5之后建立线程还有第三种方式:实现Callable接口,该接口中的call方法能够在线程执行结束时产生一个返回值,代码以下所示:

import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; class MyTask implements Callable<Integer> { private int upperBounds; public MyTask(int upperBounds) { this.upperBounds = upperBounds; } @Override public Integer call() throws Exception { int sum = 0; for(int i = 1; i <= upperBounds; i++) { sum += i; } return sum; } } class Test { public static void main(String[] args) throws Exception { List<Future<Integer>> list = new ArrayList<>(); ExecutorService service = Executors.newFixedThreadPool(10); for(int i = 0; i < 10; i++) { list.add(service.submit(new MyTask((int) (Math.random() * 100)))); } int sum = 0; for(Future<Integer> future : list) { // while(!future.isDone()) ;
            sum += future.get(); } System.out.println(sum); } }

6二、synchronized关键字的用法?

答:synchronized关键字能够将对象或者方法标记为同步,以实现对对象和方法的互斥访问,能够用synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized做为方法的修饰符。在第60题的例子中已经展现了synchronized关键字的用法。

6三、举例说明同步和异步。

答:若是系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据之后可能被另外一个线程读到,或者正在读的数据可能已经被另外一个线程写过了,那么这些数据就必须进行同步存取(数据库操做中的排他锁就是最好的例子)。当应用程序在对象上调用了一个须要花费很长时间来执行的方法,而且不但愿让程序等待方法的返回时,就应该使用异步编程,在不少状况下采用异步途径每每更有效率。事实上,所谓的同步就是指阻塞式操做,而异步就是非阻塞式操做。

6四、启动一个线程是调用run()仍是start()方法?

答:启动一个线程是调用start()方法,使线程所表明的虚拟处理机处于可运行状态,这意味着它能够由JVM 调度并执行,这并不意味着线程就会当即运行。run()方法是线程启动后要进行回调(callback)的方法。

6五、什么是线程池(thread pool)?

答:在面向对象编程中,建立和销毁对象是很费时间的,由于建立一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每个对象,以便可以在对象销毁后进行垃圾回收。因此提升服务程序效率的一个手段就是尽量减小建立和销毁对象的次数,特别是一些很耗资源的对象建立和销毁,这就是”池化资源”技术产生的缘由。线程池顾名思义就是事先建立若干个可执行的线程放入一个池(容器)中,须要的时候从池中获取线程不用自行建立,使用完毕不须要销毁线程而是放回池中,从而减小建立和销毁线程对象的开销。
Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤为是对于线程池的原理不是很清楚的状况下,所以在工具类Executors里面提供了一些静态工厂方法,生成一些经常使用的线程池,以下所示:

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

第60题的例子中演示了经过Executors工具类建立线程池并使用线程池执行线程的代码。若是但愿在服务器上使用线程池,强烈建议使用newFixedThreadPool方法来建立线程池,这样能得到更好的性能。

6六、线程的基本状态以及状态之间的关系?

答:

说明:其中Running表示运行状态,Runnable表示就绪状态(万事俱备,只欠CPU),Blocked表示阻塞状态,阻塞状态又有多种状况,多是由于调用wait()方法进入等待池,也多是执行同步方法或同步代码块进入等锁池,或者是调用了sleep()方法或join()方法等待休眠或其余线程结束,或是由于发生了I/O中断。

6七、简述synchronized 和java.util.concurrent.locks.Lock的异同?

答:Lock是Java 5之后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的全部功能;主要不一样点:Lock有比synchronized更精确的线程语义和更好的性能,并且不强制性的要求必定要得到锁。synchronized会自动释放锁,而Lock必定要求程序员手工释放,而且最好在finally 块中释放(这是释放外部资源的最好的地方)。

相关文章
相关标签/搜索