Java面试知识点

Java面试知识点

花写完这篇文章,我感触很深,以前学Java认为会用就行了,而不会刨根问底,导致很多知识都停留在表面,思考得不透彻。经过这次较全面的整理,可以知道以前知道的还是太少了。学习就要一步一个脚印,才会扎实。

1、Java运行机制

Java程序的执行过程,必须经过先编译,后解释两个步骤。Java代码使用javac编译生成.class文件(字节码文件),交给JVM进行解析执行。

2、面向对象三大特征

  • 封装:写一个类就是对数据和方法的封装,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。
  • 继承:面向对象实现软件复用的手段,当子类继承父类后,将获得父类的属性和方法
  • 多态:简单的说就是用同样的对象调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。

3、实现多态主要有以下三种方式

  • 接口实现
  • 方法重载(实现的是编译时的多态性)
  • 方法重写(实现的是运行时的多态性)

4、面向对象与基于对象区别

面向对象和基于对象都实现了“封装”的概念,但面向对象实现了“继承”和“多态”,而基于对象没有。JavaScript是基于对象的,它使用已封装好的对象,调用对象的方法,但它无法让开发者派生新的类,只能使用现有的方法和属性。

5、虚拟机是如何实现多态的

动态绑定技术(dynamic binding),执行期间判断所引用对象的实际类型,根据实际类型调用对应的方法

6、Java数据类型分类

7、访问控制符

以下修饰符只能用于修饰成员变量,不能修饰局部变量;private与protected不能用来修饰类(只有public、abstract或final能用来修饰类)。

  • private(当前类访问权限):只能在当前类内部被访问
  • default(包访问权限):当不使用任何访问控制符修饰类或成员时,系统默认使用该控制符,可被同一包中其他类访问
  • protected(子类访问权限):既可以被同一包中其他类访问,也可以被不同包中的子类访问
  • public(公共访问权限):整个工程中都可被访问

8、变量引用

  • void:无返回值
  • this:指向本类
  • super:指向该类的父类

9、类、方法、变量修饰符(列出重要的)

  • static:用于区分成员变量、方法、内部类和初始化块这4种成员是属于类本身还是属于实例。所修饰的方法和成员变量,既可以通过类来调用,也可以通过实例来调用。
  • final:所修饰的变量获得初始值后不可被修改;所修饰的方法不可被重写;所修饰的类不可被继承。
  • abstract只能修饰类和方法(抽象类不能实例化,抽象方法没有方法体),有抽象方法的类只能被定义成抽象类,抽象类可以没有抽象方法;抽象方法必须被子类重写,因此private、final都不能与abstract同时使用。
  • interface:一个接口可继承多个接口,一个类可实现多个接口;可定义静态变量(默认public static final,且必须赋初始值)、抽象方法(默认public abstract,且只能用这两个修饰符),Java8开始增加了类方法(static)、默认方法(default)(都必须有方法体)(接口及其成员只能用public修饰)
  • volatile:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又强迫线程将变化值回写到主内存。这样在任何时刻,不同线程总能看到该变量的最新值。(只能保证变量安全,不能保证线程安全)
  • transient:表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中。

10、接口和抽象类区别

相同:都不能实例化、都可定义抽象方法、静态方法、默认方法(Java8开始)

不同:

  • 接口只能定义静态变量,不能定义普通变量,而抽象类都可以
  • 接口不能定义普通方法,而抽象类可以
  • 接口没有构造器、初始化块,而抽象类有
  • 接口可以多继承,而抽象类不行

11、不可变类

对象一旦被创建,状态就不能再改变,如 String、Integer及其它包装类。

12、java创建对象的方法

  • 采用new
  • 通过反射机制
  • 采用clone
  • 通过序列化机制

13、Object中有哪些公共方法

  • equals()、hashCode()
  • clone()
  • getClass()
  • notify()、notifyAll()、wait()
  • finalize()

14、方法重载和方法重写

方法重载:方法名相同,但形参列表不同。

方法重写:子类包含与父类同名的方法,有相同返回类型。

区别:重载主要发生在同一个类的多个同名方法之间,而重写发生在子类和父类的同名方法之间。

15、工厂模式

工厂模式是最常用的实例化对象模式,是用工厂方法代替new操作的一种模式。

作用:如果有多处需要生成A的对象,那么你需要写很多A  a=new A(),需要修改时,就会很麻烦!但是如果用工厂模式,只需要修改工厂代码。

public interface Shape {
   void draw();
}
public class Rectangle implements Shape {
   public void draw() {
      System.out.println("Inside Rectangle::draw() method.");
   }
}
public class Square implements Shape {
   public void draw() {
      System.out.println("Inside Square::draw() method.");
   }
}
public class ShapeFactory {
   //使用 getShape 方法获取形状类型的对象
   public Shape getShape(String shapeType){
      if(shapeType == null){
         return null;
      }		
	if(shapeType.equalsIgnoreCase("RECTANGLE")){
         return new Rectangle();
      } else if(shapeType.equalsIgnoreCase("SQUARE")){
         return new Square();
      }
      return null;
   }
}

16、内部类的作用

  • 提供更好的封装,不允许同一包类的其他类访问

  •  可直接访问外部类私有数据

  • 匿名内部类适用于创建那些仅需要使用一次的类

 

17、对象在内存中的状态

当JVM执行可恢复对象的finalize()方法时,可变成可达状态,但什么时候调用该方法并不确定。

18、强制垃圾回收的两种方法

  • 调用System类的gc()静态方法:System.gc()(只通知GC进行回收,但什么时候回收不确定)

  • 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()

19、Java四种引用:强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)

  • 强引用:具有强引用的对象不会被垃圾回收器回收,即使当前内存空间不足,JVM也不会回收它。显式地将引用赋值为null,可中断强引用和某个对象之间关联,JVM在合适的时间就会回收该对象。
  • 软引用:只有在内存不足时,软引用才会被垃圾回收器回收,可用于图片缓存。
  • 弱引用:无论当前内存空间是否充足,JVM进行回收时一旦发现弱引用,都会将它回收。不过由于垃圾回收器是一个优先级较低的线程,并不一定能迅速发现弱引用对象,同样可用于图片缓存。
  • 虚引用:如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。它存在的唯一作用就是当它指向的对象回收时,它本身会被加入到引用队列中,这样我们可以知道它指向的对象何时被销毁。

在Java中有时候我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型。

20、如何判断对象是否可以被回收

我们知道不可达的对象就可以被回收,问题就转化为:如何判断对象是可达还是不可达?

可达性算法(引用链法)

从GC roots对象(根对象)开始向下搜寻,如果从根对象开始无法引用到该对象,则该对象就是不可达的。

以下三类对象在jvm中作为GC roots:虚拟机栈(JVM stack)中引用的对象、方法区中类静态属性引用的对象、本地方法栈(Native Stack)引用的对象

21、你知道哪些垃圾回收算法

  • 标记-清除
  • 标记-复制
  • 标记-整理
  • 分代回收

22、Java中==与equals()的区别

==:用于比较两个变量是否相等(基本数据类型)或比较两个引用变量是否指向同一个对象

equals():是Object类的方法,同样用于比较两个引用变量是否指向同一个对象(其中String已经重写了该方法,比较的是两字符串的值)

23、equals()和hashcode()的联系

hashCode()是Object类的一个方法,返回一个哈希值。如果两个对象根据equal()方法比较相等,那么它们调用hashCode()方法返回的哈希值相同。但如果不相等,返回的哈希值不一定不等(冲突情况下还是会相等的——两个不相等的对象有可能有相同的哈希值)。

24、3*0.1==0.3返回值是什么

false,因为有些浮点数不能完全精确的表示出来。

25、floatf=3.4;是否正确?

不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。

26、shorts1 = 1;s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?

对于short s1 = 1; s1 = s1 + 1;由于1是int类型,因此s1+1运算结果也是int 型,需要强制转换类型才能赋值给short型。而short s1 = 1; s1 += 1;可以正确编译,因为s1+= 1;相当于s1 = (short)(s1 + 1);其中有隐含的强制类型转换

27、int和Integer的区别

Integer是int的包装类,int是基本类型,i=5;直接在栈内存分配空间;而Integer是类,Integer i = new Integr(5);,对象的数据是存在堆内存中,而i(引用变量)是在栈内存中。

(JVM管理的是堆内存,在堆内存中分配空间所需的时间远大于从栈中分配存储空间,所以JAVA速度比C 慢。)

28、Java基本数据类型、包装类与String类之间的转换

29、String a1 = “Hello”; String a2 = newString(“Hello”);区别

String实现了常量池!!

 

  • 第一种方法:String是先检测常量池中有没有对应字符串,如果有,则取出来;如果没有,则把当前的添加进去。
  • 第二种方法:同样是先检测常量池中有没有对应字符串,如果有,则只创建一个对象放在堆内存中,如果没有,则创建两个对象(常量池一个,堆内存一个)。

 

第二种方法直接调用构造函数来创建字符串,如果所创建的字符串在字符串常量池中不存在则调用构造函数创建全新的字符串,如果所创建的字符串在字符串常量池中已有则再拷贝一份到 Java 堆中。

30、Java内存分配

(1)栈内存:用于保存基本数据类型的值、引用变量、局部变量

  • 栈内存是线程私有的,其生命周期和线程相同
  • 栈内数据共享
  • 存放对象的引用,但对象本身不存放在栈中,而是存放在堆内存或者常量池中

(2)堆内存:用于保存new产生的对象,数组(对象只包含成员变量,不包括成员方法。同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法)

  • 堆内存是被所有线程共享的一块内存区域,在JVM启动的时就被创建
  • 堆内存是垃圾回收的主要区域

(3)方法区:方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量,被所有线程共享

常量池:存在于方法区中,存放字符串常量和基本类型(指包装类)变量。(含有[-128,127]的值供实现的常量池的基本类型引用)

  • 8种基本类型(Byte,Short,Integer,Long,Character,Boolean,Float,Double),除Float和Double以外,其它六种都实现了常量池,但是值在[-128,127]范围内才能使用常量池,超过该范围就会直接在堆内存中创建对象。
  • String也实现了常量池,String型是先检测常量池中有没有对应字符串,如果有,则取出来;如果没有,则把当前的添加进去。
package Controller;
public class test {
	public static void main(String[] args) {
		String aa="ab";//放在常量池中
		String bb="ab";//在常量池中查找
		String a=new String("ab");
		String b=new String("ab");//a,b分别位于堆中不同的内存空间 
		System.out.println(a.equals(b));//true
		System.out.println(aa==bb);//true
		System.out.println(a==b);//false
		System.out.println(a==bb);//false
		System.out.println(a.equals(bb));//true
		
		int c=128;					//放在栈中,指向128的内存地址
		float d=128.0f;				//放在栈中,指向128的内存地址   
		double e=128.0;				//放在栈中,指向128的内存地址 ,即栈内数据是共享的
		Integer cc=new Integer(128);//cc放在栈中,指向堆中的对象
		System.out.println(c==d);//true
		System.out.println(c==e);//true
		System.out.println(c==cc);//true,/这里实际上是:c == cc.intValue()(自动拆箱)
		System.out.println(cc.equals(c));//true
		System.out.println(cc.equals(e));//false

		Integer i1=new Integer(1);  
		Integer i2=new Integer(1);  //i1,i2分别位于堆中不同的内存空间  
		Integer i12=new Integer(2);
		System.out.println(i1==i2);//输出false
		System.out.println(i12==i1+i2);//输出true,Java的数学运算都是在栈中进行的,Java会自动对i1、i2进行拆箱操作转化成整型 
		Integer i3=1;  
		Integer i4=1;  //i3,i4指向常量池中同一个内存空间 ,Integer在常量池的范围为-128~127
		System.out.println(i3==i4);//输出true 
		Integer i5=129;  
		Integer i6=129;  //i5,i6指向堆中不同的内存空间
		System.out.println(i5==i6);//输出false
	}
}

31、i++与++i的区别

i++:在程序执行完后进行自增;++i:在程序开始执行前进行自增

	public static void main(String[] aegs){
		int i=1;
		System.out.println(i++);		//1
		System.out.println(++i);		//3
		int j=1;
		System.out.println(j+++j++);	//3
		System.out.println(j);			//3
		System.out.println(j+++ ++j);	//8
		System.out.println(j);			//5
		System.out.println(j+++j+++j++);//18
		System.out.println(j);			//8
	}

32、String、StringBuffer与StringBuilder区别

String是字符串常量,默认final修饰,是一个对象,而不是基本数据类型;StringBuffer字符串变量(线程安全);StringBuilder 字符串变量(线程不安全)。

 

(1)String和StringBuffer

String和StringBuffer主要区别是性能。String是不可变对象,每次对String类型进行操作都等同于产生了一个新的String对象,然后指向新的String对象;StringBuffer是对对象本身操作,而不是产生新的对象。

 

  • 在拼接静态字符串时,尽量用 +,JVM会对String拼接做一定的优化:String s=“a ”+”b”会被JVM优化成Strings=“a b”,此时就不存在拼接过程。
  • 在拼接动态字符串时,尽量用 StringBuffer 或 StringBuilder的 append,这样可以减少构造过多的临时 String 对象。

(2)StringBuffer和StringBuilder

 

 

 

StringBuffer是线程安全的可变字符串,其内部实现是可变数组。StringBuilder是Java 5.0新增的,其功能和StringBuffer类似,但是非线程安全。因此,在没有多线程问题的前提下,使用StringBuilder会取得更好的性能。

33、什么是编译期常量

公共静态不可变(public static final )变量就是编译期常量,这里的 public 可选的。它在编译期就可以确定,调用编译期常量不会初始化类

 

package testPage;

class InitalizedClass {
    static {
        System.out.println("You have initalized InitalizedClass!");
    }
    public static int inititalize_varible = 1;
}

public class TestInitializeClass {
    public static void main(String[] args) {
        System.out.println(InitalizedClass.inititalize_varible);
    }
    /**
     * 输出结果为:
     * You have initalized InitalizedClass!
     * 1
     */
}
package testPage;

class InitalizedClass {
    static {
        System.out.println("You have initalized InitalizedClass!");
    }
    public final static int INITIALIZED_VARIBLE = 1;//编译器常量

}

public class TestInitializeClass {
    public static void main(String[] args) {
        System.out.println(InitalizedClass.INITIALIZED_VARIBLE);
    }
    /**
     * 输出结果为:
     * 1
     */
}

34、继承与组合的区别

继承和组合都是实现类复用的重要手段,不同的是继承会破坏封装(重写改变方法实现),而组合能提供更好的封装性。继承表达的是“is a”的关系,而组合表达的是“has a”的关系。

遵循这样一个原则:能使用组合的时候尽量不要使用继承。除非两个类之间是“is-a”的关系,否则不要轻易地使用继承,因为过多地使用继承会破坏代码的可维护性,当父类被修改的时候,会影响到所有继承自它的子类,从而增加程序的维护难度与成本。

35、为什么不能根据返回类型来区分重载

如两个方法 void a();和 int a(),如果这样调用int result=a();,系统是可以识别调用了返回值类型为int的方法,但java里允许调用一个有返回值的方法的时候不必将返回值赋给变量,这样JVM就不知道你调用的是有返回值的还是没返回值的。

36、向上转型和向下转型

public class Father {
	public String name="父亲属性";
	public void method() {
		System.out.println("父类方法,对象类型:" + this.getClass());
	}
	public void method1(){
		System.out.println("父类方法1,对象类型:" + this.getClass());
	}
}
public class Son extends Father{
	public String name="儿子属性";
	public void method() {
		System.out.println("子类方法,对象类型:" + this.getClass());
	}
	public static void main(String[] args) {
		Father s1 = new Son();//向上转型
		System.out.println("调用的成员:"+s1.name);
		s1.method();
		s1.method1();
		
		Son s2=(Son) s1;//向下转型  ,而Son s2=(Son) new Farther();会抛出java.lang.ClassCastException异常
		System.out.println("调用的成员:"+s2.name);
		s2.method();
		s2.method1();//this总是指向调用该方法的对象
	}
}

结果

调用的成员:父亲属性
子类方法,对象类型:class Controller.Son
父类方法1,对象类型:class Controller.Son
调用的成员:儿子属性
子类方法,对象类型:class Controller.Son
父类方法1,对象类型:class Controller.Son

37、集合中能放基本类型的值吗

List a=new ArrayList();
a.add(6); //java支持自动装箱,实际放进去的是int的包装类Integer

不能,但Java支持基本类型的自动装箱。

 

38、Java集合框架的基础接口有哪些

java集合类主要由两个根接口派生而出:Collection和Map,Collection下还有以下三个接口

  • Set是一个无序,元素不能重复的集合,其实现类都是非线程安全的
  • Queue是队列
  • List是一个有序,元素可重复的集合

 

39、Iterator是什么

Iterator接口提供遍历任何Collection的接口,Iterrator对象也被称为迭代器。Iterator是安全的,可通过Iterator的remove()方法删除元素,但不允许在迭代过程中其他线程修改集合结构,它采用的是快速失败(fail-fast)机制,一旦检测到其他线程修改,就会触发ConcurrentModificationException异常。

40、Iterater和ListIterator之间有什么区别?

  • Iterator可遍历任何实现了Collection接口的集合,而ListIterator只能遍历List。
  • Iterator只可以向前遍历,而LIstIterator可以双向遍历。
  • ListIterator继承了Iterator,并添加了额外的功能,比如添加、替换、获取前面或后面元素的索引位置。

41、Enumeration和Iterator的区别

  • Enumeration中没有删除方法,只有遍历
  • Iterator支持fail-fast机制,而Enumeration不支持
  • Enumeration的速度是Iterator的两倍,也使用更少的内存

42、什么是 fail-fast 机制?

fail-fast机制在遍历一个集合时,当集合结构被修改,会抛出Concurrent Modification Exception

Map<String, String> premiumPhone = new HashMap<String, String>();
premiumPhone.put("Apple", "iPhone");
premiumPhone.put("HTC", "HTC one");
premiumPhone.put("Samsung", "S5");

Iterator<String> iterator = premiumPhone.keySet().iterator();
while (iterator.hasNext()) {
	System.out.println(premiumPhone.get(iterator.next()));
	premiumPhone.put("Sony", "Xperia Z");
}

如果改成如下

premiumPhone.put("HTC", "Xperia Z");

则不会抛出ConcurrentModification 异常。因为只修改了值,没有改变集合的结构。

43、什么是fail-safe机制

Java.util包中的所有集合类都被设计为fail-fast的,而java.util.concurrent中的并发集合类都为fail-safe的。

fail-safe原理是:任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException异常。
fail-safe机制有两个问题:引入额外的空间开销、无法保证读取的数据是原始数据

44、HashSet特点

HashSet是基于HashMap实现的,集合元素实际由HashMap的Key来保存,HashSet访问集合元素时根据元素的hashCode值来快速定位

  • 不能保证元素的排列顺序
  • HashSet不是同步的,会出现多线程并发访问集合时的线程安全问题,则必须通过代码保证其同步
  • 集合元素值可以是null

HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。因此,放进HashSet的对象必须重写equals()和hashCode()方法,并且保证两对象equals相等时,它们返回的哈希值也相等。以维护常规原则——相等的对象必须具有相等的哈希值。

 

将对象放入到HashSet中时,根据对象的hashcode值快速定位,找到该对象在数组储存的位置,接着找到该位置保存的对象,并调用equals方法比较这两个对象是否相等,如果相等则不添加,如果不等,则添加到该数组索引对应的链表中。

45、TreeSet特点

TreeSet是SortedSet接口的实现类,是基于TreeMap(红黑树)实现的,元素处于排序状态,支持两种排序方式:

  • 自然排序(默认):添加到TreeSet的对象必须实现了Comparable接口,TreeSet会调用元素的compareTo(Object obj)方法来比较元素间的大小关系,然后按升序排序。(对象同时也要重写equals()方法,以维护原则——两对象通过equals比较相等时,它们通过compareTo比较也应返回0
  • 定制排序:添加到TreeSet的对象可无需实现任何接口,也可无需重写任何方法,排序是由TreeSet关联的Comparator对象完成
public class M {
	public int age;
	public M(int age) {
		this.age=age;
	}
	public String toString(){
		return "M[age:"+age+"]";
	}
}
public class Test {
	public static void main(String[] args) {
		TreeSet<M> t=new TreeSet<M>(new Comparator<M>() {
			@Override
			public int compare(M o1, M o2) {
				return o1.age>o2.age ? -1 : o1.age<o2.age ? 1:0;
			}
		});
		t.add(new M(5));
		t.add(new M(-3));
		t.add(new M(9));
		System.out.println(t);
	}
}

注:HashSet和TreeSet集合中推荐只放入不可变对象。

46、Comparable和Comparator的区别

Comparable可以认为是一个内比较器,实现了Comparable接口的类可以和自己比较,依赖compareTo方法,一般实现自然排序。(a.compareTo(b)=0,则a=b;a.compareTo(b)>0,则a>b;a.compareTo(b)<0,则a<b)

Comparator可以认为是一个外比较器,实现了Comparator接口的类通过compare方法定制自己想要的比较方式,可实现定制排序。(compare(a,b)=0,则a=b;compare(a,b)>0,则a>b;compare(a,b)<0,则a<b)

47、历遍一个List集合有哪些方法

  • 使用for循环(有安全问题,Set集合不能用for循环)
  • 使用foreach循环(集合结构发生改变时,会抛出ConcurrentModificationException异常)
  • 使用Iterator迭代器(集合结构发生改变时,会抛出ConcurrentModificationException异常)
  • 使用ListIterator迭代器(集合结构发生改变时,会抛出ConcurrentModificationException异常)

48、ArrayList与Vector的异同

ArrayList与Vector都是List的实现类,一般推荐使用ArrayList。

  • Vector是线程安全的,而ArrayList不是,使用Collections工具类可以将ArrayList变成线程安全,或使用对应的并发集合类CopyOnWriteArrayList
  • Vector支持多线程操作,所以性能上比ArrayList低
  • 两者维护插入的顺序
  • ArrayList和Vector的迭代器实现都是fail-fast的

49、ArrayList与Array(数组)的区别,什么时候更适合用Array?

  • Array可以包含基本类型和对象类型,ArrayList只能包含对象类型(添加基本类型时自动装箱)
  • Array大小是固定的,ArrayList的大小是动态变化的
  • ArrayList提供了更多的方法和特性

尽管ArrayList明显是更好的选择,但也有些时候Array比较好用。

  • 列表的大小已经指定,并且插入、查询和历遍操作比较多
  • 保存大量基本类型数据(ArrayList的自动装箱会有消耗)

50、ArrayList与LinkedList的区别

两者都实现的是List接口,不同之处在于:

  • ArrayList是基于动态数组实现的,LinkedList是基于链表的数据结构
  • 对于新增和删除操作LinkedList要优于ArrayList,因为ArrayList要移动数据
  • 访问任意元素时,ArrayList要优于LinkedList,因为LinkedList要同过历遍来查找
  • LinkedList比ArrayList消耗更多的内存,因为LinkedList中的每个节点存储了前后节点的引用

此外,LinkedList实现了Deque接口,是双向链表的,可被用作栈(stack),队列(queue)或双向队列(deque)。注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是使用Collections工具类:List list = Collections.synchronizedList(new LinkedList(…));

51、遍历ArrayList时如何正确移除一个元素

使用Iterator的remove()方法

52、ArrayDeque(双端队列)和LinkedList的区别

  • 都实现了Deque的接口,都可用作栈(stack),队列(queue)或双向队列(deque)
  • ArrayDeque是基于动态数组实现的,而LinkedList是基于链表实现的
  • 访问任意元素时,ArrayDeque要优于LinkedList
  • 对于新增和删除操作LinkedList要优于ArrayDeque

53、基于数组的线性表与基于链表的线性表

前者:随机访问时性能更好

后者:执行删除、插入操作时性能更好,而且迭代操作的性能也更好

54、解决hash冲突的方法

  • 开放地址法
  • 再哈希法
  • 链地址法(HasnMap就是采用该方法)

55、基于链地址法实现的哈希表

我们知道数组寻址容易,插入和删除困难;链表寻址困难,插入和删除容易。而哈希表综合了两者的优点,既满足了数据的查找方便,同时不占用太多的内容空间。其中一种基于链地址法的哈希表如下:

56、HashMap实现原理(重点)

HashMap用于储存键值对,它内部是通过一个数组实现的,只是这个数组比较特殊,数组里存储的元素是一个Entry对象,这个Entry对象包含了key、value以及一个Entry对象next(用于指向下一个Entry对象)。HashMap是基于hashing实现的:

  • 插入Entry对象A:首先A的key值通过hashcode()方法得到hashcode值,利用该值找到A要存储在数组的位置。如果该位置是空的,直接将A插进去;如果存在一个Entry对象,就比较它们的hashcode值和key值,如果都相同就用A将它替换掉,如果不同,就继续比较下一个Entry对象;如果链表中找不到有相同的对象,就将A插进链表头,原Entry对象赋值给A的next对象(也就是说数组中存储的是最后插入的元素)。
  • 根据key值获取value值:首先根据key的hashcode值定位到所在的数组位置,同样通过比较hashcode值与key值的方式,分别与链表中每个Entry对象比较,找到就返回该Entry的value值,找不到就返回null。

(1)当两个对象的hashcode相同会发生什么

定位到同一个bucket,发生冲突

(2)如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办 

HashMap默认的负载因子是0.75,会创建一个是原数组大小两倍的数组,并将原来的对象放入新的数组中,这个过程叫rehashing。

(3)我们可以使用自定义的对象作为键吗?

所定义的对象必须重写equals方法和hashcode方法,并且满足相等的对象必须具有相等的哈希值,为了防止放进HashMap的对象被修改(修改后,通过get方法可能会找不回到),可将对象定义成不可变对象。

57、ArrayList和HashMap默认大小?

ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素

58、HashMap与Hashtable区别

都是Map接口的典型实现类,使用Collections工具类可以将HashMap变成线程安全,或使用对应的并发集合类CocurrentHashMap

  • Hashtable是一个线程安全的,而HashMap不是,所以HashMap比Hashtable性能高,
  • Hashtable不允许使用null作为key和value,而HashMap可以,但key值最多只有一个为null

59、CocurrentHashMap与Hashtable区别

相同点: Hashtable 和 ConcurrentHashMap都是线程安全的,可以在多线程环境中运行; key跟value都不能是null

区别: 两者主要是性能上的差异,Hashtable的所有操作都会锁住整个hash表,虽然能够保证线程安全,但是性能较差; ConcurrentHashMap锁的方式是对hash表局部锁定,不影响其他线程对hash表其他地方的访问,默认支持16个线程并发写入。并且ConcurrentHashMap的迭代器采用了fail-safe机制,因此不会抛出ConcurrentModificationException异常。

 

试想,原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。

60、哪些集合类提供对元素的随机访问?

ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。

61、TreeMap特点

TreeMap是SortedMap接口的实现类,它就是一个红黑树数据结构,TreeMap保证所有的key-value对处于有序状态。跟TreeSet一样支持两种排序:

  • 自然排序(默认):TreeMap所有key值必须实现了Comparable接口,TreeMap会调用key值的compareTo(Object obj)方法来比较key-value对的大小关系,然后按升序排序。(对象key值同时也要重写equals()方法,以维护原则——两对象通过equals比较相等时,它们通过compareTo比较也应返回0
  • 定制排序:添加到TreeMap的对象的key值可无需实现任何接口,也可无需重写任何方法,排序是由TreeMap关联的Comparator对象完成

62、如何决定选用HashMap还是TreeMap?

对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。

63、并发集合类是什么

并发集合类解决了传统集合类线程不安全的问题,提供大量支持高效并发访问的集合接口,这些线程安全的集合类可分为两类:

(1)以Concurrent开头的集合类可以支持多个线程并发写入访问,并且所有操作不会锁住整个集合,如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue和ConcurrentLinkedDeque.

(2)以CopyOnWrite开头的集合类,如CopyOnWriteArrayList、CopyOnWriteArraySet.。CopyOnWriteArraySet.的底层封装了CopyOnWriteArrayList,所以两者的实现机制是完全一样的。它们采用复制底层数组的方式来实现写操作。

  • 当线程执行读取操作时,线程会直接读取集合本身,无需加锁和阻塞;
  • 当线程执行写入操作时,集合会复制一份新的数组,线程会对新的数组进行写入操作,而原数组没改变,因此是线程安全的。(由于要频繁复制数组,性能比较差,它们使用于读取操作远大于写入操作的场景,如缓存

64、BlockingQueue是什么?

BlockingQueue(处于conurrent包下)——阻塞队列,用于实现多个线程间数据的共享,比如经典的“”生产者和消费者“模型”,BlockingQueue解决了生产者线程和消费者线程数据处理速度不匹配的问题。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)

Java提供了多个其实现类:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue和DelayQueue。

 

下面两幅图演示了BlockingQueue的两个常见阻塞场景:

  • 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
  • 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

                           

65、Collections类是什么?

Collections是操作集合的工具类,可提供大量方法对集合元素进行排序、查询和修改等操作,还提供将集合对象设置为不可变、对集合对象实现同步控制等方法。

66、大写的O是什么?举几个例子?

大写的O描述的是一个算法的性。

  • ArrayList的get(index i)是一个常量时间操作,它的性能是O(1)。
  • 数组或列表的线性搜索的性能是O(n),因为我们需要遍历所有的元素来查找需要的元素。

67、poll()方法和remove()方法区别?

poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。

68、如何对一组对象进行排序

对象数组:Arrays.sort()方法

对象列表:Collection.sort()方法

69、当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它?

在作为参数传递之前,使用Collections.unmodifiableCollection(Collection c)方法创建一个只读集合,这将确保改变集合的任何操作都会抛出UnsupportedOperationException。

70、如何从给定集合那里创建一个synchronized的集合?

可以使用Collections.synchronizedCollection(Collection c)根据指定集合来获取一个synchronized(线程安全的)集合。

71、进程和线程的关系

线程是系统运行调度的最小单位,是进程的子集,同一进程中的多个线程之间可以并发执行。不同的进程使用不同的内存空间,而同一进程的所有的线程共享该进程的内存空间。

72、创建线程的方式?他们有什么区别?

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

实现Runnable接口比继承Thread类更优:(缺点是:访问当前线程必须使用Thread.currentThread()方法,而继承Thread类直接使用this即可访问)

  • Java不支持多重继承,但可以实现多个接口,实现Runnable接口后还可以继承另一个类
  • 实现Runnable接口的线程可以共享同一个target对象

Runnable与Callable的主要区别:

  • Callable的call()方法可以有返回值和抛出异常,而Runnable的run方法没有
  • Callable接口能和Future、FutureTask配合,可方便获取多线程运行的结果

73、什么是FutureTask?

FutureTask表示一个异步运算的任务,可以将一个任务交给后台线程执行,并通过get方法返回执行结果。可以与Callable接口配合使用,方便获取多线程运行的结果。

74、线程的生命周期

新建就绪运行阻塞死亡

线程创建后,不会立即执行线程执行体,调用start()方法后处于就绪状态,当获得CPU开始执行run()方法,处于运行状态,当另一个线程抢占了CPU后,处于阻塞状态然后在合适的时候重新进入就绪状态,知道run()方法运行完进入死亡。

75、Thread 类中的start()和 run()方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法;而直接调用run()方法,只会是在原来的线程中调用,没有启动新的线程。

76、什么是多线程上下文切换

CPU控制权由一个正运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

77、CountDownLatch和CyclicBarrier的区别(都是Concurrent包下的类)

CountDownLatch是一次性的,而CyclicBarrier能够重复使用。

  • CountDownLatch能够使一个线程等待其他线程执行后再执行。它是通过一个计数器来实现的,计数器的初始值为线程的数量,每执行完一个线程,计数器就减1。当计数器值到达0时,释放所有等待的线程。
public class Test {  
    public static void main(String[] args) throws InterruptedException {  
        CountDownLatch countDownLatch = new CountDownLatch(5);  
        for(int i=0;i<5;i++){  
            new Thread(new readNum(i,countDownLatch)).start();  		
        }
        countDownLatch.await();
        System.out.println("线程执行结束。。。。");
    }

    static class readNum  implements Runnable{
        private int id;
        private CountDownLatch latch;
        public readNum(int id,CountDownLatch latch){
            this.id = id;
            this.latch = latch;
        }
        @Override
        public void run() {  
            synchronized (this){  
                System.out.println("id:"+id);  
                System.out.println("线程组任务"+id+"结束,其他任务继续");
                latch.countDown();    
            }  
        }  
    }  
} 

运行结果:
id:0
id:2
id:3
线程组任务3结束,其他任务继续
id:4
线程组任务4结束,其他任务继续
id:1
线程组任务1结束,其他任务继续
线程组任务2结束,其他任务继续
线程组任务0结束,其他任务继续
线程执行结束。。。。

  • CyclicBarrier能够使一组线程互相等待,直到都到达某个公共屏障点后,再一起执行。当一个线程到达屏障时(通过调用 CyclicBarrier.await()),它会被阻塞,每到达一个线程,计数器加1,当所有线程都到达屏障时,然后在该点释放所有阻塞线程。
public class Test {  
    public static void main(String[] args) throws InterruptedException {  
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {  
            @Override  
            public void run() {  
                System.out.println("线程组执行结束");  
            }
        });
        for (int i = 0; i < 5; i++) {
            new Thread(new readNum(i,cyclicBarrier)).start();
        }
    }
    static class readNum  implements Runnable{
        private int id;
        private CyclicBarrier cyc;
        public readNum(int id,CyclicBarrier cyc){
            this.id = id;
            this.cyc = cyc;  
        }  
        @Override  
        public void run() {  
            synchronized (this){  
                System.out.println("id:"+id);  
                try {  
                    cyc.await();  
                    System.out.println("线程组任务" + id + "结束,其他任务继续");  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    }  
} 

运行结果:

id:0
id:4
id:3
id:1
id:2
线程组执行结束
线程组任务2结束,其他任务继续
线程组任务0结束,其他任务继续
线程组任务1结束,其他任务继续
线程组任务3结束,其他任务继续
线程组任务4结束,其他任务继续

78、CountDownLatch的应用场景

  • 实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数为1的CountDownLatch,并让所有线程都在这个锁上等待,那么我们可以很轻松地完成测试。我们只需调用 一次countDown()方法就可以让所有的等待线程同时恢复执行。、
  • 开始执行前等待n个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统已经启动和运行了。
  • 死锁检测:使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。

79、什么是线程安全?

如果多线程同时运行某段代码,如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

80、什么是竞态条件?举个例子说明。

多个线程同时访问相同的资源并进行读写操作时,就有可能产生竞态条件。通俗一点,就是线程A 需要判断一个变量的状态,然后根据这个变量的状态来执行某个操作。而在执行这个操作之前,这个变量的状态可能会被其他线程修改。

最典型的就是单例

public class Singleton {    
    private static Singleton instance;    
        
    public static Singleton getInstance(){    
        if(instance == null)  
            instance = new Singleton();  
        return instance;  
    }    
}  

线程a和线程b同时执行getInstance(),线程a看到instance为空,创建了一个新的Obj对象,此时线程b也需要判断instance是否为空,此时的instance是否为空取决于不可预测的时序:包括线程a创建Obj对象需要多长时间以及线程的调度方式,如果b检测时,instance为空,那么b也会创建一个instance对象。

线程B 在执行红色部分代码(判断instance是否为空)时,线程A 还没来得执行完绿色部分的代码。

81、什么是后台线程

在后台运行的线程,它的任务是为其他线程提供服务,如果前台线程都死亡,后台线程也会自动死亡。JVM垃圾回收线程就是典型的后台线程。

82、控制线程

  • join线程:让一个线程等待另一个线程完成的方法,调用该方法的线程先执行完
  • 线程睡眠sleep:调用该sleep()方法的线程进入阻塞状态,睡眠期间内不能返回运行状态(在同步代码块或同步方法上使用,不会释放同步监视器)

  • 线程让步yield:调用该yield()方法的线程暂停,进入就绪状态,但会随时返回运行状态(在同步代码块或同步方法上使用,不会释放同步监视器)

  • 改变线程优先级:每个线程执行时都具有一定的优先级,优先级高的比优先级低的获得更多的执行机会。每个线程的优先级都与创建它的父线程优先级相同,在默认情况下,main线程具有普通优先级,而main线程创建的子线程也具有普通优先级。

83、如何停止一个线程

  • run()或call()方法执行完,线程正常结束
  • 线程抛出一个未捕获的异常
  • 使用interrupt方法中断线程

84、什么情况会导致线程阻塞

  • 线程调用sleep()方法
  • 线程调用了阻塞式的IO方法
  • 线程在等待某个通知(notify)
  • 调用suspend()方法(该方法已废弃)

85、解决线程安全的方法

  • 同步代码块(同步监视器):任何时刻只能有一个线程可获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。

    synchronized(obj){

           … //此处的代码就是同步代码块

    }

    obj就是同步监视器,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。Java允许使用任何对象作为同步监视器,同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问。

        public class ThreadTest extends Thread {
        	private Account account;
        	private double money;
        	public void run() { 
        		synchronized(account){
        			…
        		}
        	}
        }

     

  • 同步方法:使用synchronized关键词修饰某个方法,该方法称为同步方法,同步监视器是this。

        public class Account {
        	private String account;
        	private double balance;
        	public synchronized void draw(double drawAmount) {
        		if(account>= drawAmount){
        			… 
        		}
        	}
        }

     

    线程执行同步代码块或同步方法时,程序调用sleep()、yield()方法会暂停该线程,但不会释放同步监视器。

  • 同步锁:Lock是控制多个线程对共享资源进行访问的工具。在实现线程安全控制中,比较常用的是ReentrantLock(可重加锁)

    public class Account {
    	private final ReentrantLock lock=new ReentrantLock();
    	private String account;
    	private double balance;
    
    	public void draw(double drawAmount) {
    		lock.lock();
    		try{
    			if(account>= drawAmount)
    			{ … }
    		}finally{
    			Lock.unlock();
    		}
    	}
    }

     

86、怎么检测一个线程是否拥有锁

Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着”某条线程”指的是当前线程。

87、wait()、notify()和notifyAll()这些方法为什么不在Thread类里面?

由于wait,notify和notifyAll都是锁级别的操作,而每个对象都有锁,所以把他们定义在Object类中。

 

这些方法只能在同步方法或者同步代码块里面调用,wait()导致当前线程暂时释放锁,进入阻塞状态(而sleep()、yield()方法不会释放锁,好让其他使用同一把锁的线程有机会执行,锁借出去了,那怎么把锁收回来呢?(线程A调用wait()方法,把锁让线程B运行)

  • 在wait()里设置参数,限定借出去的时间,时间一到自动收回
  • 线程B使用该锁的notify()方法或notifyAll()方法通知线程A用完了(线程A并不是马上运行,还要等到线程B运行完,或调用wait()方法,线程B才会释放锁

notify()方法唤醒在此同步监视器上等待的单个线程。如果有多个线程在等待,则随机选一个;而notifyAll()唤醒在此同步监视器上等待的所有线程。

public class Business {
	private boolean bShouldSub=true;
	public synchronized void sub(int i){//子线程 
		while(!bShouldSub){
			try {
				this.wait();
			}catch(InterruptedException e){ 
				e.printStackTrace(); 
			} 
		} 
		for (int j = 1; j <= 2; j++)
			System.out.println("sub thread sequece of "+j+",loop of"+i); 
		bShouldSub=false; 
		this.notify();//唤醒主线程 
	} 
	
	public synchronized void main(int i){//主线程 
		while(bShouldSub){ 
			try { 
				this.wait();
			} catch (InterruptedException e) { 
				e.printStackTrace(); 
			} 
		} 
		for (int j = 1; j <= 4; j++)
			System.out.println("main thread sequece of "+j+",loop of"+i); 
		bShouldSub=true; 
		this.notify();//唤醒子线程 
	} 
	
	public static void main(String[] args) {
		final Business business=new Business(); 
		new Thread(
				new Runnable() { 
					@Override 
					public void run() {
						for(int i=1;i<=2;i++){
							System.out.println("sub");
							business.sub(i);
						}
					} 
				}).start(); 
		for(int i=1;i<=4;i++){
			System.out.println("main");
			business.main(i); 
		}
	}
}

运行结果:

main
sub
sub thread sequece of 1,loop of1
sub thread sequece of 2,loop of1
sub
main thread sequece of 1,loop of1
main thread sequece of 2,loop of1
main thread sequece of 3,loop of1
main thread sequece of 4,loop of1
main
sub thread sequece of 1,loop of2
sub thread sequece of 2,loop of2
main thread sequece of 1,loop of2
main thread sequece of 2,loop of2
main thread sequece of 3,loop of2
main thread sequece of 4,loop of2
main

88、如何避免死锁

死锁是多个线程因争夺资源而造成的一种互相等待的现象,若无外力作用,这些进程都将无法向前推进。(一般是等待对方释放同步监视器)

一个简单的死锁例子:

public class DeadLock implements Runnable {    
	public int flag = 1;    
	//静态对象是类的所有对象共享的    
	private static Object o1 = new Object(), o2 = new Object();    
	@Override    
	public void run() {  
		if (flag == 1) {    
			synchronized (o1) {    
				try {    
					Thread.sleep(500);    
				} catch (Exception e) {    
					e.printStackTrace();    
				}    
				synchronized (o2) {    
					System.out.println("1");    
				}    
			}    
		}    
		if (flag == 0) {    
			synchronized (o2) {    
				try {    
					Thread.sleep(500);    
				} catch (Exception e) {    
					e.printStackTrace();    
				}    
				synchronized (o1) {    
					System.out.println("0");    
				}    
			}    
		}    
	}    

	public static void main(String[] args) {    
		DeadLock td1 = new DeadLock();    
		DeadLock td2 = new DeadLock();    
		td1.flag = 1;    
		td2.flag = 0;    
		new Thread(td1).start();    
		new Thread(td2).start();    
	}    
}  

死锁的发生必须同时满足以下四个条件:

  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

3种用于避免死锁的技术:

  • 加锁顺序(线程按照一定的顺序加锁)
  • 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  • 死锁检测

89、死锁、活锁和饥饿区别

与死锁不同,处于活锁的线程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。

死锁:两人在狭窄的胡同相遇,两人都不歉让,等待着对方给自己让路

活锁:两人在狭窄的胡同相遇,两人都试着避让对方,但避让的方向都一样,最后谁都不能通过

饥饿:A、B在狭窄的胡同相遇,A让B先通过,这时来了C,A又让C先通过,如此来了一个又一个,A一直处于等待状态

90、为什么wait()方法和notify()、notifyAll()方法要在同步代码块中被调用

这是JDK强制的,wait()、notify()、notifyAll()方法都是锁级别的操作,在调用前都必须先获得对象的锁。

91、wait()方法和notify()、notifyAll()方法在放弃对象监视器时有什么区别

wait()方法立即释放对象监视器,notify()、notifyAll()方法则会等待线程执行完,或主动调用wait()方法,才会放弃对象监视器。

92、wait()与sleep()的区别

  • sleep()属于Thread类,而wait()属于Object类
  • 调用sleep()方法,线程不会释放锁,而wait()会
  • sleep()可单独使用,而wait()需要配合notify()或notifyAll()使用
  • wait()必须在同步代码块(同步方法)中使用,而sleep()不需要

93、synchronized 和 ReentrantLock 有什么不同

  • synchronized 是关键字,而ReentrantLock是类
  • ReentrantLock用起来更灵活,可以被继承,可以有方法
  • ReentrantLock可以获取各种锁的信息
  • 实现线程通信时,synchronized使用的是wait()、notify()和notifyAll(),而ReentrantLock使用的是await()、signal()、signalAll()

94、有三个线程T1,T2,T3,怎么确保它们按顺序执行?

可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行,位确保顺序,T3调用T2,T2调用T1。

95、如何在两个线程间共享数据

通过在线程之间共享对象就可以了,然后通过wait()、notify()、notifyAll(),await()、signal()、signalAll()进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。

96、什么是ThreadLocal(线程局部变量)

ThreadLocal主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每个线程中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。(ThreadLocal只能使用Object类型,不能使用基本类型

 

但是在如 web 服务器使用要特别小心,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,就存在内存泄露的风险。

public class ThreadLocalDemo implements Runnable {  

	private final static ThreadLocal studentLocal = new ThreadLocal();  
	public static void main(String[] agrs) {  
		ThreadLocalDemo td = new ThreadLocalDemo();  
		Thread t1 = new Thread(td, "a");  
		Thread t2 = new Thread(td, "b");  
		t1.start();  
		t2.start();  
	}  

	public void run() {  
		accessStudent();  
	}  

	public void accessStudent() {   
		String currentThreadName = Thread.currentThread().getName();  
		System.out.println(currentThreadName + " is running!");  
		Random random = new Random();  
		int age = random.nextInt(100);  
		System.out.println("thread " + currentThreadName + " set age to:" + age);  
		Student student = getStudent();  
		student.setAge(age);  
		System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());  
		try {  
			Thread.sleep(500);  
		}  
		catch (InterruptedException ex) {  
			ex.printStackTrace();  
		}  
		System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());  
	}  

	protected Student getStudent() {  
		Student student = (Student) studentLocal.get();  
		//线程首次执行此方法的时候,studentLocal.get()肯定为null  
		if (student == null) {  
			student = new Student();  
			studentLocal.set(student);  
		}  
		return student;  
	} 
}  

97、ThreadLocal与Synchronized的区别

ThreadLocal和Synchonized都用于解决多线程并发访问,区别:

  • Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离
  • synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

98、Java中的同步集合与并发集合有什么区别?(Synchronized vs Concurrent)

同步集合和并发集合都支持线程安全,主要区别体现在性能和可扩展性。

  • 同步集合相对于并发集合要慢得多,主要原因是锁,同步集合会把整个集合锁起来,而并发不会。(该点针对Concurrent,可看第59点)
  • 并发集合允许多线程以非同步的方式读,无需加锁和阻塞,因此在读取操作远大于写入操作的场景,更具有可扩展性(该点针对CopyOnWrite)

99、为什么应该在循环中检查等待条件?(用if还是while)

wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:

synchronized (obj) {
	while (condition does not hold)
	obj.wait(); // (Releases lock, and reacquires on wakeup)
	... // Perform action appropriate to condition
}

100、Java中堆和栈有什么区别?

栈是一块和线程紧密相关的内存区域,每个线程都有自己的栈内存,用于存储本地变量,方法参数,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。

101、线程池的作用?

创建线程需要花费昂贵的资源,大量创建线程有可能导致系统内存消耗完,以及“过度切换”。而线程池有效限制并发线程的数量,使得运行效果达到最佳。线程池在系统启动时即创建大量空闲的线程,一个runnable对象传给线程池,线程池就会启动一个线程执行它的run()方法,执行完后,线程不会死亡,而是再次返回线程池成为空闲线程。

作用:

  • 实现对线程的复用,避免了反复创建线程的开销
  • 有效控制并发线程的数量,而过多的线程会导致内存消耗完,以及“过度切换”

102、生产者-消费者模型

(1)作用:

  • 主要作用:通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率
  • 附带作用:解耦,生产者和消费者并不直接相互调用,而是生产者和缓冲区、消费者和缓冲区之间的弱耦合

(2)通过notify()和wait()实现

public class ShareDataTest{

	public static void main(String[] args){
		Factory f = new Factory();
		Producer p = new Producer(f);
		Consumer c = new Consumer(f);
		new Thread(p).start();
		new Thread(c).start();
	}
}

class Producer implements Runnable{
	private Factory f;
	public Producer(Factory f){
		this.f = f;
	}
	@Override
	public void run(){
		f.produce();
	}
}

class Consumer implements Runnable{
	private Factory f;
	public Consumer(Factory f){
		this.f = f;
	}
	@Override
	public void run(){
		f.consume();
	}
}

class Factory {
	private final int MAX_SIZE = 10;//最大储存量
	private LinkedList<Object> list = new LinkedList<Object>();
	public void consume(){
		synchronized(list){
			while(true){
				if (list.size()== 0){
					try{
						System.out.println("消费完了,需要生产者生产。。。。。。。。。。。。。。。。。。");
						list.wait();
					} catch(InterruptedException e){
						e.printStackTrace();
					}
				}else{
					try {
						Thread.sleep(1000);//模拟耗时
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("正在消费"+list.remove()+",还剩下 " + list.size());
					list.notify();
				}
			}
		}
	}

	public void produce(){
		synchronized(list){
			while(true){
				if(list.size()>=MAX_SIZE){
					try {
						list.wait();
					}catch (InterruptedException e) {
						e.printStackTrace();
					}
				}else{
					try {
						Thread.sleep(2000);//模拟耗时
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					for(int i=1;i<=MAX_SIZE;i++)
						list.add("num"+i);
					System.out.println("生产者正在生产,总共生产了 "+MAX_SIZE+"个");
					list.notify();
				}
			}
		}
	}
}

(3)通过BlockingQueue实现

public class ShareDataTest{

	public static void main(String[] args){
		Factory f = new Factory();
		Producer a = new Producer(f);
		Consumer b = new Consumer(f);
		Consumer c = new Consumer(f);
		new Thread(a).start();
		new Thread(b).start();
		new Thread(c).start();
	}
}

class Producer implements Runnable{
	private Factory f;
	public Producer(Factory f){
		this.f = f;
	}
	@Override
	public void run(){
		f.produce();
	}
}

class Consumer implements Runnable{
	private Factory f;
	public Consumer(Factory f){
		this.f = f;
	}
	@Override
	public void run(){
		f.consume();
	}
}

class Factory {
	private final int MAX_SIZE = 10;  //最大储存量
	private BlockingQueue<Object> queue=new ArrayBlockingQueue<Object>(MAX_SIZE);
	public void consume(){
		while(true){
			try {
				Thread.sleep(1000);//模拟耗时
				System.out.println(Thread.currentThread().getName()+"正在消费数字"+queue.take());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public void produce(){
		while(true){
			try {
				Thread.sleep(400);//模拟耗时
				int n=new Random().nextInt(1000);
				queue.put(n);
				System.out.println(Thread.currentThread().getName()+"正在生产数字"+n);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

103、如果你提交任务时,线程池队列已满,这时会发生什么

分有界队列和无界队列进行讨论

  • 无界队列:如LinkedBlockingQueue,可无限存放任务,直到内存溢出
  • 有界队列:如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。

104、java中用到的线程调度算法是什么

抢占式的动态优先级调度算法,线程调度程序按线程的优先级进行调度,高优先级的线程先被调度。

105、Java线程池中submit() 和 execute()方法有什么区别?

两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中;而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口。

106、什么是阻塞式方法

阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。

107、Thread.sleep(0)的作用是什么

由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

108、原子性和可见性

原子性:指操作具有不可分割性,如对基本变量赋值(除long、double外),非原子操作都有安全性问题

可见性:指线程之间的可见性,一个线程修改的状态对另一个线程是可见的,如volatile修饰的变量就具有可见性

例如用volatile int count=1具有可见性和原子性,但 count++ 操作就不是原子性的。

109、可以创建Volatile数组吗?

可以创建,不过只是一个指向数组的引用,而不是整个数组,volatile修饰的变量如果是对象或数组之类的,其含义是对象或数组的地址具有可见性,但是数组或对象内部的成员改变不具备可见性。

110、volatile能使得一个非原子操作变成原子操作吗?

能,一个典型的例子是读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子操作。

111、如果同步块内的线程抛出异常会发生什么?

无论你的同步块是正常还是异常退出的,里面的线程都会释放锁。

112、单例模式的双检锁是什么?(可联系到80题)

单例模式:多线程下会产生竞态条件,可能会创建两个实例对象。

public class Singleton {  
    private static Singleton instance;  
      
    public static Singleton getInstance(){  
        if(instance == null)//1
            instance = new Singleton();//2
        return instance;//3
    }  
} 

假设两线程同时调用getInstance()

1、线程1先调用getInstance(),在执行//2时被线程2抢占了
2、此时instance=null,线程2进入//1,创建了Singleton对象,并在//2出将变量instance指向该对象,并在//3处返回
3、线程1抢回cpu,在停止的地方继续执行,在//2处创建了另一个Singleton对象
结果是两个线程都创建了对象

getInstance():使用同步方法

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

不可否认,synchronized关键字是可以保证单例,但是程序的性能却不容乐观,原因在于getInstance()整个方法体都是同步的,这就限定了访问速度。其实我们需要的仅仅是在首次初始化对象的时候需要同步,对于之后的获取不需要同步锁。

 

双检锁(模式1):使用同步代码块

public class Singleton {  
    private static Singleton instance;  
      
    public static Singleton getInstance(){
        if(instance == null){//1
        	synchronized (Singleton.class) {//2
        		instance = new Singleton();//3
		}  
        }  
        return instance;  
    }  
} 

该方法同在多线程下同样会创建两个实例对象,假设两线程同时调用getInstance()

1、线程1先获得cpu进人//1后,被线程2抢占了
2、线程2进入//2获得锁,线程1被阻塞,线程2创建了对象实例
3、线程2退出synchronized块,线程1继续执行,获得锁,并创建了对象实例
结果是两个线程都创建了对象

 

双检锁(模式2):使用同步代码块

针对以上问题,需要对instance进行第二次检查,这就是“双检锁”。

public class Singleton {  
	private static volatile Singleton instance;  

	public static Singleton getInstance(){
		if(instance == null){
			synchronized (Singleton.class) {//1
				if(instance == null)//2
					instance = new Singleton();//3
			}  
		}  
		return instance;  
	}  
} 

该方法既可以实现线程安全,又可以保证性能不受影响,解决了上述的问题,可以说是完美的。但理论是完美的,现实是残酷的。该方法在很多优化编译器上是也不完全安全,原因在于//3的代码在不同编译器上的行为是无法预知的。一个优化编译器可以合法地如下实现 instance=new Singleton():

  • 分配内存空间
  • 初始化对象
  • instance指向分配的内存空间

JVM并不保证后两个操作的先后顺序。假设两线程同时调用getInstance(),线程A先进入,在执行到//3时JVM可能先为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就有可能出错了。线程A离开synchronized块后,线程B进入,B看到的是instance已经不是null了(内存已经分配),于是它开始放心地使用instance,但这个是错误的,因为A还没有来得及完成instance的初始化,而线程B就返回了未被初始化的instance实例。

解决方法是:变量instance使用volatile修饰,禁止指令重排优化(jdk1.5版本后有效)

 

Initialization on Demand Holder模式:使用内部类维护单例的实现

该方法使用内部类来做到延迟加载对象,而且其加载过程是线程安全的。这种写法完全使用了JVM的机制,它能保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。内部类SingletonHolder只有在getInstance()方法第一次调用的时候才会被加载。

public class Singleton
{      
    private static class SingletonHolder{      
        public final static Singleton instance = new Singleton();      
    }      
     
    public static Singleton getInstance(){      
        return SingletonHolder.instance;      
    }      
}  

113、你有哪些多线程开发良好的实践?

  • 给线程命名
  • 最小化同步范围
  • 线程通信时,多使用BlockingQueue,而非wait()、notify()
  • 优先使用并发集合类,而不是同步集合类
  • 考虑使用线程池

114、SimpleDateFormat是线程安全的吗?

DateFormat的所有实现都不是线程安全的,可以使用ThreadLocal,使得每个线程都将拥有自己的SimpleDateFormat对象副本。

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>();  
    
    public static Date parse(String str) throws Exception {  
        SimpleDateFormat sdf = local.get();  
        if (sdf == null) {  
            sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);  
            local.set(sdf);  
        }  
        return sdf.parse(str);  
    }  
      
    public static String format(Date date) throws Exception {  
        SimpleDateFormat sdf = local.get();  
        if (sdf == null) {  
            sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);  
            local.set(sdf);  
        }  
        return sdf.format(date);  
    }  
}

115、Serializable 与 Externalizable 的区别

序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘中,或通过网络传输,Java对象可实现Serializable接口或Externalizable接口。Externalizable是Serializable的子类,可以使部分属性实现序列化,一般我们可以用transient代替它。

注:static、transient修饰的数据成员是不能够被序列化的。

116、流的分类

(1)输入流和输出流:是从程序运行所在内存的角度来划分的

 

  • 输入流:把文件数据输出并写入到内存
  • 输出流:把内存数据输出并写入到文件

 

Java的输入流主要由InputStream和Reader作为基类,而输出流主要由OutputStream和Writer作为基类。它们都是抽象类,无法直接创建实例。

(2)字节流和字符流

用法几乎一样,区别在于字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符;字节流主要由InputStream和OutputStream作为基类,字符流主要由Reader和Writer作为基类。

(3)节点流和处理流

  • 节点流:可以从/向一个特定的IO设备读/写数据的流
  • 处理流:用于对已存在的节点流进行连接或封装,封装后的流实现数据的读写功能

 

 如果进行输入/输出的内容是文本内容,则应该考虑使用字符流;若是二进制内容,则应该考虑使用字节流。

 

分类

字节输入流

字节输出流

字符输入流

字符输出流

抽象基类

InputStream

OutputStream

Reader

Writer

访问文件

FileInputStream

FileOutputStream

FileReader

FileWriter

访问数组

ByteArrayInputStream

ByteArrayOutputStream

CharArrayReader

CharArrayWriter

访问管道

PipedInputStream

PipedOutputStream

PipedReader

PipedWriter

访问字符串

 

 

StringReader

StringWriter

缓冲流

BufferedInputStream

BufferedOutputStream

BufferedReader

BufferedWriter

转换流

 

 

InputStreamReader

OutputStreamWriter

对象流

ObjectInputStream

ObjectOutputStream

 

 

抽象基类

FilterInputStream

FilterOutputStream

FilterReader

FilterWriter

打印流

 

PrintStream

 

PrintStream

推回输入流

PushbackInputStream

 

PushbackReader

 

特殊流

DataInputStream

OutputStream

 

 

注:粗体字为节点流,其他为处理流

117、Java IO与NIO区别

  • IO是阻塞IO,读取期间,线程被阻塞,不能干其他事情;而NIO是非阻塞IO,读取数据时可不需要等待它完全写入,该线程同时可干其他事情。
  • IO是面向流的,意味着每次从流中读一个或多个字节,直至读取所有字节,而且流中的数据不能前后移动;而NIO是面向缓冲区的,会把数据读取到一个缓冲区中,然后可对缓冲区中的数据进行处理。
  • NIO中存在选择器,它允许你把多个通道注册到一个选择器上,然后使用一个线程来监视这些通道:若某个通道准备进行读或写操作了,则开始对该通道进行读写。而在其他时间,请求对通道进行读写操作的线程可以去干别的事情。

118、JVM内存模型

Java内存由栈内存、堆内存、方法区、本地方法栈和程序计数器组成。

(1)程序计数器(线程私有):是一块较小的内存空间,可看做是当前线程所执行的字节码的行号指示器。

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,该内存为线程所私有。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

(2)Java栈(线程私有)描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

在Java 虚拟机规范中,对这个区域规定了两种异常状况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;
  • 如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常。

(3)本地方法栈:使用native关键字修饰的方法(原生函数),该方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。

本地方法栈与Java栈的作用类似,其区别是Java栈为JVM执行Java 方法(也就是字节码)服务,而本地方法栈则是为JVM使用到的Native 方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。

 

(4)Java堆(线程共享):用于保存new产生的对象,数组(对象只包含成员变量,不包括成员方法。同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法)

  • 堆内存是被所有线程共享的一块内存区域,在JVM启动的时就被创建
  • 堆内存是垃圾回收的主要区域

(5)方法区(线程共享:方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量

常量池:存在于方法区中,存放字符串常量和基本类型(指包装类)。

119、Java垃圾回收之分代回收

由于不同的对象,生命周期是不一样的,所以分代回收采用分而治之的思想,对不同的对象采用最适合的垃圾回收方式进行回收。

JVM把不同的对象分为三个代:年轻代年老代持久代。其中持久代主要存放的是Java类的类信息,与垃圾收集关系不大。由于Java堆是垃圾回收的主要区域,Java堆分为年轻代和年老代,而年轻代又分为Eden区、Survivor  from区和Survivor to区。

  • 年轻代:所有新生成的对象首先都放在年轻代,一般是放在Eden区,当新对象无法分配空间时(Eden区满时),就会触发Minor GC,把仍存活的对象复制到Survivor from区,当该Survivor区满时,就会把该区域的对象复制到Survivor to区中,当该Survivor区也满时,就会把从Survivor from区复制过来并且还存活的对象复制到年老区。(采用标记-复制算法)
  • 年老区:存放的是经过多次GC后仍存活的对象,可以认为年老区存放的是一些周期较长的对象。(采用标记-整理算法)
  • 持久代:主要存放类定义、字节码和常量等很少会变更的信息。

120、什么时候触发垃圾回收

GC有两种类型:Minor GC和Full GC(Major GC)

  • Minor GC:当新对象在Eden区无法分配空间时(Eden区满时),就会触发Minor GC,清除·不·存活的对象,并把存活的对象移动到Survivor区。(Minor GC只在Eden区进行,不会影响年老区)
  • Full GC:对整个堆进行整理,包括Young、Tenured和Perm,所以速率比Minor GC慢,有如下原因可能导致Full GC:年老代写满、持久代写满、显示调用System.gc()

121、Java中垃圾回收的方法有哪些

  • 标记-清除:标记出所有需要回收的对象,然后统一回收被标记的对象。但是会有两个主要问题:1.效率不高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC。
  • 标记-复制:内存分成相等的AB两块,每次只使用其中的一块。比如当A内存使用完后,就把A中还存活着的对象复制到另外一块内存中去(B),然后再把已经使用过的内存清理掉。优点:这样就不用考虑内存碎片的问题了。缺点:内存减半,代价略高。实际中,JVM将堆内存的年轻代分成较大的Eden区和2个较小的Survivor空间,默认Eden:Survivor=8:1,这样只有较小的内存被浪费掉。         
  • 标记-整理:复制算法当存活的对象多时,就需要大量复制,效率不高,不适合年老代这种。那么就可以采用标记-整理算法。也就是先标记,然后对存活的对象进行移动,全部移动到一端,然后再对其它的内存进行清理。                
  • 分代回收:目前普遍采用的就是分代收集法。把JVM堆内存有细分成很多部分(如年轻代,年老代,持久代),不同部分采用不同的算法,这样来提高效率。

122、Java内存模型

Java内存模型描述了线程共享变量的访问规则,以及在JVM中将变量储存到内存和内存中读取出变量这样的底层细节。

线程间的通信机制有两种:共享内存消息传递

在共享内存的并发模型中,线程间共享程序的公共状态,线程间通过写-读内存中的公共状态来隐式进行通信。

在消息传递的并发模型中,线程间没有公共状态,线程间必须通过明确的发送消息来显式进行通信。

 

Java线程间通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:

线程A与线程B之间如要通信(可见性实现)的话,必须要经历下面2个步骤:

  • 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  • 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

123、共享变量可见性的实现方式有哪些?

补充概念:

  • 重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化。多线程交错执行前,重排序可能会造成内存可见性问题,而单线程不会。
  • as-if-serial:无论如何重排序,程序执行的效果应该与代码顺序执行的效果一致

(1)synchronized:除了原子性(同步)外,还有可见性

线程解锁前对共享变量的修改在下次加锁时对其他线程可见。

  • 线程加锁时,将清空本地内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值
  • 线程解锁前,必须把共享变量的最新值刷新到主内存中

(2)volatile:实现可见性是通过加入内存屏障和禁止重排序优化来实现的。但volatile没有原子性,因此只能保证变量安全,不能保证线程安全

volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又强迫线程将变化值回写到主内存。这样在任何时刻,不同线程总能看到该变量的最新值。

  • 线程写volatile变量时,先改变本地内存的变量副本值,再刷新到主内存中
  • 线程读volatile变量时,先从主内存读取最新值到本地内存中,再读取变量副本值

volatile要在多线程中安全使用,必须满足

  • 对变量的写入操作不依赖于其当前值,如a++就不满足了
  • 该变量没有包含在具有其他变量的不变式中,如a=b+1,a,b都是volatile变量

因此在实际中,volatile的适用范围是比较窄的。

124、GC收集器有哪些

  • Serial收集器:一个采用标记-复制算法的单线程收集器,在垃圾收集时,必须暂停其他所有的工作线程直到它收集结束。特点:CPU利用率最高,停顿时间即用户等待时间比较长,是Client模式下虚拟机首选的新生代收集器。
  • ParNew收集器:Serial收集器的多线程版本,同样采用采用标记-复制算法,除了Serial收集器外,只有它能与CMS收集器配合使用,因此它是Server模式下虚拟机首选的新生代收集器。
  • Parallel收集器:也是一个采用标记-复制算法的新生代收集器,Parallel收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总运行100分钟,垃圾收集1分钟,那吞吐量就是99%。另外,Parallel收集器是虚拟机运行在Server模式下的默认垃圾收集器。高吞吐量可以高效利用CPU时间,适合于在后台运行而不需要太多交互的任务。
  • Serial Old收集器:Serial收集器的老年代版本,一个采用标记-整理算法的单线程收集器,也是给Client模式下虚拟机使用
  • Parallel Old收集器:Parallel收集器的老年代版本,一个采用标记-整理算法的多线程收集器
  • CMS收集器:一种采用标记-清除算法,以获取最短回收停顿时间为目标的老年代收集器。在互联网站或者B/S系统的服务端上,尤其注重服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS收集器非常符合该需求。
  • G1收集器:采用标记-整理算法的收集器,它不再局限于整个年轻代或者年老代,而是把堆划分成多个连续的独立区域(region),在后台维护一个区域的优先列表,优先回收垃圾最多的区域。

125、类的加载

类的加载是指将类的.class文件的二进制数据读入内存,并为之创建一个Java.lang.Class对象

类的加载包括包括以下5个阶段:加载、验证、准备、解析、初始化

类的生命周期:加载、验证、准备、解析、初始化、使用、卸载

主要有一下四种类加载器:

  • 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
  • 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

详细可参考:http://www.cnblogs.com/ityouknow/p/5603287.html

 

126、Java程序的初始化顺序是怎样的

执行顺序:父类静态变量 > 父类静态代码块 > 子类静态变量 > 子类静态代码块 > 父类非静态变量 父类非静态代码块> 父类构造函数> 子类非静态变量> 子类非静态代码块> 子类构造函数 。

  • 静态对象(变量)优先于非静态对象(变量),其中静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次
  • 父类优先于子类进行初始化
  • 按成员变量的定义顺序进行初始化

127、浅复制与深复制的区别(浅克隆和深克隆)

浅复制仅仅复制所考虑的对象,而不复制它所引用的对象;而深复制把复制的对象所引用的对象都复制了一遍。

若存在以下类:

public class BubbleSort {  
	private int i;
	private StringBuffer s;
}

128、什么是反射机制

反射机制主要功能:

  • 在运行时,得到一个对象所属的类,并可获取类的所有成员变量和方法
  • 在运行时创建对象,并可调用对象的方法

反射机制获取class对象有三种方法:

  • Classc1 = Class.forName("Employee"); 
  • Classc2 = Employee.class;//java中每个类型都有class 属性. 
  • Employee e = new Employee(); Classc3 = e.getClass();//java语言中任何一个java对象都有getClass 方法 

129、使用反射生成并操作对象

(1)创建对象

在很多JavaEE框架中都需要根据配置文件来创建Java对象,从配置文件读取的只是字符串的类名,根据该字符串创建对应的实例,就必须使用映射。

Class c =Class.forName("Employee");  
Object o = c.newInstance(); //调用了Employee的默认构造器.

(2)调用方法

通过Class对象的getMethod()或getMethods()方法获得Method对象或Method数组,然后通过Method的invoke()方法来调用对应的方法,相对于执行了目标对象的setter方法。

 

  • getMethods()方法:获取的是所有public的函数,包含父类继承而来的
  • getDeclaredMethods()方法:获取的是所有该类自己声明的方法,不问访问权限

 

(Spring框架就是通过这种方式将成员变量以及依赖的对象等都放在配置文件中进行管理的,这也是Spring的IoC的秘密)

(3)访问成员变量

通过Class对象的getFields()或getField()方法可以获取该类所包含的全部Field或指定Field。

130、内部类有哪些

(1)静态内部类(可有修饰符)

静态内部类可不依赖于外部类实例而被实例化,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态方法

class outerClass{
	static class innerClass{	//静态内部类
	}
}

(2)成员内部类(可有修饰符)

成员内部类需依赖于外部类实例而被实例化,可访问外部类的任何属性和方法,但成员内部类中不能定义静态成员

class outerClass{
	class innerClass{	//静态内部类
	}
}

(3)局部内部类(不能有修饰符)局部内部类跟局部变量一样,不能有修饰符

class outerClass{
	public void method1(){
		class innerClass{}	//局部内部类
	}
	public static void method2(){
		class innerClass{}	//局部静态内部类
	}
}

(4)匿名内部类(不能有修饰符)

  • 匿名内部类不能有构造函数
  • 匿名内部类一定是在new的后面,且必须继承一个父类或实现一个接口
  • 匿名内部类不能定义用static修饰的变量、方法和类
  • 匿名内部类不能有修饰符
interface Product {
	public double getPrice();
	public String getName();
}
public class AnonymousTest {
	public void test(Product p) {
		System.out.println(p.getName()+p.getPrice());
	}
	public static void main(String[] aegs){
		AnonymousTest ta=new AnonymousTest();
		ta.test(new Product(){			//匿名内部类
			public double getPrice(){
				return 42.65;
			}
			public String getName(){
				return "hjy";
			}
		});
	}
}

131、变量命名规则

变量名、函数名、数组名统称为标识符,Java规定标识符只能由字母(a~z,A~Z)、数字(0~9)、下划线(_)、和$组成,并且标识符的第一个·字符必须是字母、下划线或$,此外不能包含空格。

132、switch的注意事项

使用switch(expr)时,expr只能是枚举常量(内部由整型或字符类型实现)或一个整型表达式。

133、instanceof有什么用

instanceof是一个二元运算符,用来判断一个引用类型的变量所指向的对象是否是一个类(接口、抽象类、父类)的实例,即它左边的对象是否是它右边的类的实例。

134、strictfp有什么用

strictfp用来确保浮点数运算的准确性,没有该关键字,浮点数运算会不精准,并且在不同平台输出结果可能不同。一旦用strictfp声明一个类、接口、方法,在声明的范围内浮点数运算都是精准的。当修饰一个类时,所有方法都会隐式地被strictfp修饰。

135、值传递和引用传递区别(call by value/call by reference)

值传递(基本数据类型):方法调用中,实参会把值传递给形参,形参与实参有相同的值,但有着不同的存储单元,因此对形参的改变不会影响实参的值。

引用传递(其它所有类型):方法调用中,传递的是对象的地址,这是形参与实参的对象指向同一块存储单元,因此对形参的改变会影响实参的值。

注:基本数据类型的包装类都是不可变量,因此要注意!

136、基本数据类型的转换

不同类型数据在运算时会自动隐式转换,从低精度向高精度转换,即byte < short < char < int < long < float < double;而从高精度向低精度转换时需要强制转换。

注意一下几点:

  • char类型转化为高级类型(如int、long等),会转化为对应的ASCII码
  • byte、char、short类型在参与运算时会自动转化为int型,但使用“+=”运算时就不会,如short s1 = 1;s1 = s1 + 1;有错,而short s1 = 1;s1 += 1;没错
  • 基本类型与boolean不能相互转换

总之,多种数据混合运算时,系统会先自动将所有数据转换成容量最大的那一种数据类型后,才进行计算。

137、左右移操作

<<:左移n位表示原值乘以2的n次方,如(4<<3) = ( 4*2^3)

>>(有符号右移运算符):若为正数,则高位补0;若为负数,则高位补1

>>>(无符号右移运算符):不管正负,都会在高位补0

注:byte、char、short类型移位时会自动转化为int型,右移位数超过32bit时,采用取余操作,即a>>n等价于a>>(n%32)

负数以补码的形式存储,i=-5的二进制表示:

原码:00000000 00000000 00000000 00000101 (即绝对值)

反码:11111111 11111111 11111111 11111010 (原码取反)

补码:11111111 11111111 11111111 11111011 (反码+1)

则 i >> 1后的值为-3 

—— 11111111 11111111 11111111 11111101(补码)> 11111111 11111111 11111111 11111100 (反码)>  00000000 00000000 00000000 00000011 (原码)

138、Java动态绑定机制

Java的动态绑定又称为运行时绑定,即程序会在运行时自动选择调用哪个方法,是多态实现的一种——方法重写,子类的实例赋值给父类的引用,当执行重写的方法时,执行的是子类的方法。

139、锁的分类

(1)自旋锁使线程在没有获得锁时不被挂起,而转去执行一个空循环,若干空循环后,线程如果可以获得锁,则继续执行;若线程依然不能获得锁,才会被挂起。

使用自旋锁的线程被挂起的几率相对减少,执行的连贯性相对加强。适用于锁竞争不是很激烈,锁占用时间很短的并发线程。在锁竞争激烈的情况下,使用自旋锁的线程长时间无法得到对应的锁,不仅白白浪费了CPU时间,最终还免不了被挂起。

(2)阻塞锁使线程进入阻塞状态,当获得相应的信号(唤醒,时间) 时,才可以进入就绪状态,然后通过竞争,进入运行状态。

JAVA中,能够进入/退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字、ReentrantLock、Object.wait()\notify()。

(3)可重入锁也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁,它最大的作用是避免死锁

(4)悲观锁和乐观锁

  • 悲观锁:一段执行逻辑加上悲观锁,同一时间只能有一个线程执行,其他的线程在入口处等待,直到锁被释放
  • 乐观锁:一段执行逻辑加上乐观锁,多个线程可同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作(适用于读多写少的场景)

(5)读写锁:如ReentrantReadWriteLock

  • 多线程下只有写操作时互斥;
  • 多线程下有读又有写操作时互斥;
  • 多线程下只有读操作时不互斥,可一定程度上提高执行效率

140、Concurrent包下面有哪些类

  • 并发集合类
  • Semaphore:计数信号量,用于控制某个资源的并发访问量;而线程池是控制线程的数量
  • ReentrantLock:可重互斥锁
  • BlockingQueue:阻塞队列
  • CountDownLatch:能够使一个线程等待其他线程执行后再执行
  • CyclicBarrier:能够使一组线程互相等待,直到都到达某个公共屏障点后,再一起执行
  • ....

141、异常分类

142、抽象类可以继承实体类吗

可以,前提是实体类必须有明确的构造函数,即抽象类的构造函数是有(无)参的,实体类也必须有有(无)参的构造函数。

143、静态加载类与动态加载类

new创建对象的方式称作为静态加载,而使用Class.forName("XXX")称作为动态加载,它们俩本质的区别在于静态加载的类的源程序在编译时期加载(必须存在),而动态加载的类在编译时期可以缺席(源程序不必存在)。

静态加载类

如果不定义A类和B类,则会出现编译错误。假想如下场景,Test中有100个功能,你只想使用A功能,如果你使用的是静态加载的机制,你必须定义其余的99种功能才能使用A功能。

public class Test{  
    public static void main(String[] args){  
        if("A".equals(args[0])){  
            new A().print();  
        }  
        if("B".equals(args[0])){  
            new B().print();  
        }  
    }  
}  

动态加载类

如果使用动态加载机制,不仅不用定义99中功能,通过实现某种标准(继承某个接口),将大大方便了代码的编写。

public class Test{  
	public static void main(String[] args){  
		try{  
			Class c = Class.forName(args[0]);  
			MyStandard me =  (MyStandard)c.newInstance();  
			me.print();  
		}catch(Exception e){  
			e.printStackTrace();  
		}  
	}  
}  
interface MyStandard{  
	public void print();  
}  
public class A implements MyStandard{  
    public void print(){  
        System.out.println("A");  
    }  
} 
public class B implements MyStandard{  
    public void print(){  
        System.out.println("B");  
    }  
}  

144、JDBC编程步骤

  • 加载驱动程序:Class.forName("com.mysql.jdbc.Driver");
  • 获得数据库连接:DriverManager.getConnection(url,user,password);
  • 通过Connection对象创建Statement对象,执行sql,返回结果集
  • 处理结果集
  • 关闭连接,回收数据库资源

145、JDK8新特性

  • Lambda表达式:它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理,使得代码更加简洁
  • 接口可定义默认方法和静态方法
  • 可重复注解
  • 方法引用,与Lambda表达式联合使用
  • 最新的Date/Time API
  • 数组并行(parallel)操作
  • 新增base64加解密API
  • JVM的PermGen空间(持久代)被移除:取代它的是Metaspace(JEP 122)元空间

146、Lamnda表达式的使用

Lambda表达式的基本语法:(parameters) -> expression    或   (parameters) ->{ statements; }

  • 遍历集合类
Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );
Arrays.asList( "a", "b", "d" ).forEach(System.out::println);
  • 代替内部类
//使用匿名内部类  
new Thread(new Runnable() {  
    @Override  
    public void run() {  
        System.out.println("Hello world !");  
    }  
}).start();  
  
//使用 lambda expression  
new Thread(() -> System.out.println("Hello world !")).start();
Arrays.sort(players, new Comparator<String>() {  
    @Override  
    public int compare(String s1, String s2) {  
        return (s1.compareTo(s2));  
    }  
});
//使用 lambda expression 排序
Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2))); 

147、接口默认方法和静态方法

  • 默认方法

默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写。默认方法的出现提供了在不破坏接口的兼容性的前提下对接口进行扩展。

  • 静态方法

使用与一般Java类的静态方法一样

public interface AA {
	//静态方法
	static void helloWorld(){
		System.out.println("hello world");
	}
	//默认方法
	default void doSomething(){
		System.out.println("doSomething in AA");
	}
}
public interface BB extends AA{
    //重新了父接口的默认方法
    default void doSomething(){
        System.out.println("doSomething in CC");
        //只有直接父类才可以通过XX.super.xxMethod()的方式调用父类默认方法
        AA.super.doSomething();
    }
}
public class Test1 implements BB{
    public static void main(String[] args) {
        BB bb = new Test1();
        bb.doSomething();
        AA.helloWorld();
    }
}

148、线程通信方式

  • 管道
  • 信号
  • 消息队列
  • 共享内存
  • 信号量
  • 套接字

149、正则表达式

作用:

  • 字符匹配
  • 字符串查找
  • 字符串替换

例如:

  • IP地址是否正确
  • 从网页中揪出email
  • 从网页中揪出链接等

类:

  • java.lang.String
  • java.util.regex.Pattern
  • java.util.regrx.Matcher

               

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值