Java面试题总结

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lululul123/article/details/62898807
1、 String,StringBuilder和StringBuffer有什么区别?
(1)String和(StringBuilder,StringBuffer)的比较。(String不可变,显然线程安全。)
相同点:StringBuilder,StringBuffer,String都代表字符串都会引发新的。
不同点:String类是不可变类,任何对String的改变都会引发新的String对象的生成。  说明:String类中使用字符数组保存字符串,
private final char [] value;由final可知,String对象不可变。
 StringBuffer和StringBuilder都是可变类,任何对它所指代的字符串的改变都不会产生新的对象。  说明:StringBuilder与StringBuffer都是继承AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串的,char[] value;由此可以看出这两个类是可变的。
(2)StringBuffer和StringBuilder之间 比较。
相同点:StringBuilder和StringBuffer都有公共父类AbstractStringBuilder(抽象类)。StringBuilder和StringBuffer都会调用AbstractStringBuilder中的公共方法。如append(),expendCapacity()、insert()和indexOf()等方法。他两的原理和操作基本相同。
抽象类与接口的区别:抽象类中可以定义一些子类的公共方法,子类只需要增加新的功能,不需要重复写已经存在的方法。而接口中只是对方法的声明和常量的定义。

不同点:StringBuffer支持并发操作,线程安全,适合多线程中使用。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。如:


StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。该方法不支持并发操作,线程不安全,不适合多线程在多线程中使用。新引入的StringBuilder类不是线程安全的,但其在单线程中的性能比StringBuffer高。

2、对安全线程的理解?
线程安全:就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取读取完,其他线程才能使用。不会出现数据不一致或者数据污染。
线程不安全:即不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
注:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而其他的变量的值也和预期是一样的,就是线程安全的。
一般线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量,静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的。若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

3、java中数据的存储方式?
java中数据存储方式最底层的两种结构,一种是数组,另一种就是链表。
数组的特点:连续空间,寻址迅速,但是在删除或者添加元素的时候需要有较大幅度的移动,所以查询速度快,增删较慢。
链表的特点:链表正好相反,由于空间不连续,寻址困难,增删元素只需修改指针,所以查询慢、增删快。
哈希表:有一种数据结构来可以综合一下数组和链表,以便发挥他们各自的优势。哈希表具有较快(常量级)的查询速度,及相对较快的增删速度,所以很适合在海量数据的环境中使用。哈希表的方法采用“拉链法”,我们可以理解为“链表的数组”。

存储规则:hash(key)%len;如12%16=12;28%16=12;

4、拆箱和装箱。

装箱:从值类型到引用类型。将基本数据类型转换为包装器类型;java SE5之前,要将数值10转换为Interger对象的方法为:Interger i=new Integer(10);

Java SE5之后出现了自动装箱---》Integer i=10;

拆箱:将包装器类型转换为基本数据类型;Object obj=i;int j=(int)obj;

基本类型对应的包装器类型。

装箱过程通过调用包装器的valueOf方法,而拆箱过程是通过调用包装器的xxxValue实现。
面试会问:
I、
public class Main {
    public static void main(String[] args) {
 
        Integer i1 = 100;
        Integer i2 = 100;
        Integer i3 = 200;
        Integer i4 = 200;
 
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}
第一:true;    第二:false;
原因:装箱时,通过valueOf方法创建Integer对象时,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对像。所有i1和i2指向同一个对象,而i3和i4指向不同的对象。
II、
public class Main {
    public static void main(String[] args) {
 
        Double i1 = 100.0;
        Double i2 = 100.0;
        Double i3 = 200.0;
        Double i4 = 200.0;
 
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}
第一:false;  第二:false;
原因:Double类的valueOf方法会采用与Integer类不同的valueOf方法实现。很简单:在某个范围内整型数值的个数是有限的,而浮点数确不是。
注意:Integer,Short,Byte,Character,Long这几个类的valueOf方法的实现是类似的。
Double、Float的valueOf方法的实现是类似的。

III、
public class Main {
    public static void main(String[] args) {
 
        Boolean i1 = false;
        Boolean i2 = false;
        Boolean i3 = true;
        Boolean i4 = true;
 
        System.out.println(i1==i2);
        System.out.println(i3==i4);
    }
}
结果:true,true;
VI、
谈谈Integer i = new Integer(xxx)和Integer i =xxx;这两种方式的区别。
当然,这个题目属于比较宽泛类型的。但是要点一定要答上,我总结一下主要有以下这两点区别:
1)第一种方式不会触发自动装箱的过程;而第二种方式会触发;
2)在执行效率和资源占用上的区别。第二种方式的执行效率和资源占用在一般性情况下要优于第一种情况(注意这并不是绝对的)。
VII、
public class Main {
    public static void main(String[] args) {
 
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        Long h = 2L;
 
        System.out.println(c==d);//true
        System.out.println(e==f);//false
        System.out.println(c==(a+b));//true
        System.out.println(c.equals(a+b));true
        System.out.println(g==(a+b));//true
        System.out.println(g.equals(a+b));//false
        System.out.println(g.equals(a+h));//true
    }}
当 “==”运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。
c.equals(a+b)会先触发自动拆箱过程,再触发自动装箱过程,也就是说a+b,会先各自调用intValue方法,得到了加法运算后的数值之后,便调用Integer.valueOf方法,再进行equals比较。

5、访问修饰符作用域


Public:修饰的成员可以在任何范围内直接访问,是一种最宽松的访问控制等级。所谓的直接访问仍需要先创建或获得一个相应类的对象然后才可以使用”对象名.成员“的方式访问其属性或调用其方法,但是出于信息封装和隐藏的需要一般不提倡把成员声明为public的,而构造方法和需要外界直接调用的普通方法则适合声明为public.

Protected:protected修饰的成员可以在其所在类中、同一包中及子类中(无论子类在不在同一个包)被直接访问,但不能在位于不同包中的非子类中被直接访问,这里需要特别声明:在位于不同包的子类中必须是子类的对象才能直接访问其父类的protected成员,而父类自身的对象反而不能访问其所在类中声明的protected成员。


可以想象如果想让Java类中的成员在所在包中可以直接访问,且在其子类中也能访问(子类有可能和父类不再在一个包中),但不愿意在更大更范围内公开时,就可以声明为 protected.
Default:缺省访问修饰符的成员只能在其所在类中或包中直接访问,在不同包中即使不同包的子类也不能直接访问。
Private:private成员只能在所在类中被直接访问,是四种访问等级最高的一个。

6、 Object类中的所有方法。

(1)clone()方法

创建并返回此对象的一个副本。“副本”的准确含义可能依赖于对象的类。

克隆目的:快速创建一个已有对象的副本。

Clone方法首先会判断对象是否实现了Cloneable接口,若无会抛出CloneNotSupportedException,最后会调用internalClone方法。该方法是一个native方法,一般来说native方法的执行效率高于非native方法。

当某个类要复写clone方法时,要继承Cloneable接口。

克隆就是复制一个对象的副本。若只需要复制对象的字段值:

A、若为基本数据类型(int ,long,float等),则复制值;使用浅克隆即可。

B、若为复合数据类型仅复制字段值,如数组变量则复制地址。要使用深克隆。

例子:

1.	public class Test{  
2.	    public static void main(String[] args) throws CloneNotSupportedException{  
3.	        ShadowClone c1 = new ShadowClone();  
4.	        //对c1赋值  
5.	        c1.setA(100) ;  
6.	        c1.setB(new int[]{1000}) ;  
7.	          
8.	        System.out.println("克隆前c1:  a="+c1.getA()+" b="+c1.getB()[0]);  
9.	        //克隆出对象c2,并对c2的属性A,B,C进行修改  
10.	        ShadowClone c2 = (ShadowClone) c1.clone();  
11.	        //对c2进行修改  
12.	        c2.setA(50) ;  
13.	        int []a = c2.getB() ;  
14.	        a[0]=5 ;  
15.	        c2.setB(a);  
16.	        System.out.println("克隆前c1:  a="+c1.getA()+" b="+c1.getB()[0]);  
17.	        System.out.println("克隆后c2:  a="+c2.getA()+ " b[0]="+c2.getB()[0]);  
18.	    }  
19.	}  
浅克隆结果:
克隆前c1:  a=100 b=1000
克隆前c1:  a=100 b=5
克隆后c2:  a=50 b[0]=5


  深克隆结果:
克隆前c1:  a=100 b=1000
克隆前c1:  a=100 b=1000
克隆后c2:  a=50 b[0]=5


浅克隆源码:

1.	public class ShadowClone implements Cloneable{  
2.	         
3.	    private int a;   // 基本类型  
4.	    private int[] b; // 非基本类型  
5.	    // 重写Object.clone()方法,并把protected改为public  
6.	    @Override  
7.	    public Object clone(){  
8.	        ShadowClone sc = null;  
9.	        try  
10.	        {  
11.	            sc = (ShadowClone) super.clone();  
12.	        } catch (CloneNotSupportedException e){  
13.	            e.printStackTrace();  
14.	        }  
15.	        return sc;  
16.	    }  
17.	    public int getA()  
18.	    {  
19.	        return a;  
20.	    }  
21.	    public void setA(int a)  
22.	    {  
23.	        this.a = a;  
24.	    }  
25.	    public int[] getB() {  
26.	    return b;  
27.	    }  
28.	    public void setB(int[] b) {  
29.	    this.b = b;  
30.	    }    
31.	}  
深克隆源码:
1.	public class DeepClone implements Cloneable {  
2.	  
3.	    private int a;   // 基本类型  
4.	    private int[] b; // 非基本类型  
5.	    // 重写Object.clone()方法,并把protected改为public  
6.	    @Override  
7.	    public Object clone(){  
8.	        DeepClone sc = null;  
9.	        try  
10.	        {  
11.	            sc = (DeepClone) super.clone();  
12.	            int[] t = sc.getB();  
13.	            int[] b1 = new int[t.length];  
14.	            for (int i = 0; i < b1.length; i++) {  
15.	                b1[i] = t[i];  
16.	            }  
17.	            sc.setB(b1);  
18.	        } catch (CloneNotSupportedException e){  
19.	            e.printStackTrace();  
20.	        }  
21.	        return sc;  
22.	    }  
(2)hashCode()方法
返回该对象的哈希码值。由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的。)
规定:
1、 在java应用程序执行期间,在同一对象上多次调用hashCode方法时,必须一致的返回相同的整数,前提是对象上equals比较中所用的信息没有被修改。
2、 如果根据equals(object)方法,两个对象是相等的,那么在两个对象上调用hashCode方法都必须生成相同的整数结果。
(3)equals(obj)方法
如果A.equals(obj)方法中,对象A与obj参数相同,则返回true。否则返回false。
Object类的equals方法实现对象上差别可能性最大的相等关系。即:对于任何非空引用值x和y,当且仅当x和y引用同一个对象时,此方法才返回true。
(4)toString()方法
返回该对象的字符串表示形式。
一个字符串和另一种类型连接的时候,另一种类型自动调用toString()方法,然后在和字符串连接。
基本类型转换为字符串比较简单,按照它们的数字转换过来就成了;对于引用类型来说,如Person p=new Person();一个字符串加上这个p,就没有那么简单了。
最好每个子类都重写toString()方法;否则打印出来的就是dog=cn.galc.test.Dog@150bd4d
任何一个类都是从Object类继承下来的,因此字啊任何一个类里面都可以重写这个toString()方法。
toString()方法的作用是当一个引用对象和字符串做连接的时候,或者是直接打印这个引用对象的时候,这个引用对象都会自动调用toString()方法,通过这个方法返回一个表示引用对象自己正常信息的字符串,而这个字符串的内容由我们自己去定义,默认的字符串内容是“类名+哈希编码”。因此我们可以通过在类里面重写toString()方法,把默认的字符串内容改成我们自己想要表达的正常信息的字符串内容。
(5)notify()
唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;
(6)notifyAll()
唤醒在此对象监视器上等待的所有线程。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。 
(7)wait()方法
导致当前的线程等待,直到其他线程调用此对象的notify()或notifyAll()方法。
Wait(),notify(),notifyAll()方法总结:
Object.wait()方法:让拥有object对象的锁的线程进入等待状态,并释放对象锁。
Object.wait()和object.notify()和object.notifyAll()必须写在Synchronized方法内部或者Synchronized块内部,因为:这几个方法要求当前正在运行的object.wait()方法的线程拥有object的对象锁。
(1)wait() 方法用来控制当前线程停止执行,等待其他线程对此Object实例调用notify或者notifyAll方法之后再继续执行 
(2)wait(long timeout) 此方法的作用和wait()类似,但是增加了一个超时的设置,如果等待时间超过了timeout设定的毫秒数,那么当前线程会继续执行 
(3)notify()方法从所有wait线程中选择一个线程,让它开始执行 
(4)notifyAll()方法通知所有等待此对象的线程,开始执行.

7、多态

I、面向对象 的三大特征:封装,继承,多态。
II、多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。
   而实现多态的技术为:接口实现,继承父类进行方法重写,同一类中进行方法重载。 
III、举一个现实生活中的多态 的例子:
比方说按下 F1 键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。
VI、使多态存在的三个必要条件
A、要有继承。  B、要有重写;  C、父类引用指向子类对象。
VII、多态的好处:
A、可替换性:多态对已存在代码具有可替换性。如:多态对圆类工作,对其他任何圆形几何体,如圆环,也同样工作。
B、可扩充性:多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性,继承性,以及其他特性的运行和操作。实际上,增加新子类更容易获得多态功能。
C、接口性:多态是超类通过方法签名,向子类提供一个共同接口,由子类来完善或者覆盖它而实现的 。
D、灵活性:它在应用中体现了灵活多样的操作,提高了使用效率。
E、简化性:多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。
IV、java中多态的分类
在java中,多态大致分为三种情况:

一、person为父类,student为子类,那么:person p=new student();

二、fliable为接口,bird为实现接口的类,那么:fliable f=new bird();

三、fliable为抽象类,bird为继承fliable的类,那么:fliablef=new bird();

多态时需要说明p声明为父类的引用,但他实际为子类引用。但是他只能调用父类中的方法。除非子类中方法覆盖了父类中的方法。

对于接口来说同样,但注意,如果bird有一个方法在接口fliable中没有定义,那么f不能调用。

IIV、instanceof运算符

Java语言的多态机制导致了引用变量的声明类型和其实际引用对象的类型可能不一致,再结合虚方法调用规则可以得出结论:声明为同种类型的两个引用变量调用同一个方法时也可能会有不同的行为。

personp=new student();可以将P转为student;

儿子转换为父亲可以直接转换,而父亲想转换为儿子,需要强行转换。

if(pinstanceof student){student s=(student) p;}

例子:


        A a1 = new A();  
        A a2 = new B();  
        B b = new B();  
        C c = new C();   
        D d = new D();   

a1.show(b));//A and A :A类中没有B类参数方法,但含有A类参数的方法,根据子类对象父类可用原则;
(a1.show(c));//A and A:C类是B类的子类,而B类又是A类的子类,所以C类对象可以当做A类对象使用。
(a1.show(d));//A and D:根据参数类型直接调用A中的方法show(D obj)   
(a2.show(b));//B and A:a2本来就是一个对象,但是将其赋给了A类变量,所以a2只保留了与父类A同名的属性和方法。A2.show(b)调用B类中的保留的与父类同名同参的方法。
(a2.show(c));//B and A:B类的保留方法中没有c类参数方法,但是有含有C的父类B的参数方法,所以调用的方法为show(A obj)
(a2.show(d));//A and D:调用的是A类中的
(b.show(b));//B and B:调用的是B类中的
(b.show(c));//B and B:B类中没有C类参数的方法,但是有B类参数的方法,所以调用show(B obj)
(b.show(d));//A and D:同8
总结:子类对象可以直接当成父类使用,但反过来不可以。举例:人是父类,学生是人的子类,所以学生对象可以当做人对象来使用,但是人对象不能当做学生对象使用。注意当把子类对象当成父类对象使用时,子类对象将失去所有的子类特性,只保留与父类同名的属性和方法。
所需知识点:
方法签名:用来区分不同方法的标识符。方法签名对于同名不同类,同类不同名的方法的意义是不大的。但对于重载方法来说意义就十分重大了。
JVM为我们提供的方法签名实际上是由方法名,形参列表,返回值三部分构成。
即:全类名.方法名(形参数据类型列表)返回值数据类型。
 重载:判断重载的重要依据,方法名和形参数据类型列表可以唯一的确定一个方法,与方法的返回值一点关系都没有,这是判断重载的重要依据。
方法重载是让类以同一的方式处理不同类型数据的一种手段。多个同名函数同时存在,具有不同的参数个数/类型;重载是一个类中多态的一种表现。
Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义。调用方法时通过传递给他们的不同参数个数和参数类型来决定具体使用哪个方法,这就是多态性。
重载的时候,方法名要一样,但参数类型和个数不一样,返回值可以相同也可以不相同。无法返回类别作为重载函数的区分标准。
重写:重写只能存在于具有继承关系中,重写方法只能重写父类非私有的方法。
如果父类中的某个方法被private时,son类不能重写处父类中的这个方法,此时子类中的该方法相当于在子类中定义的一个方法。
父类中的某方法被默认修饰符修饰时,只能在同一包中,被其子类重写,如果不在同一包则不能重写。
父类中的某方法被protected修饰时,不仅在同一包中,被其子类重写,还可被不同包的子类重写。
父类中的方法一旦被final时,无论该方法的修饰符是什么,子类根本不能重写父类中的这个被final的方法。试图这么做的话,编译器会报错。
父类与子类之间的多态性,对父类的函数进行重新定义。如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写。在java中,子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。方法重写又称为方法覆盖。
若子类中的方法与父类中的某一方法具有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法。如需父类中原有的方法,可使用super关键字,该关键字引用了当前类的父类。子类函数的访问修饰权限不能少于父类。
总结:多态性是面向对象编程的一种特性,和方法无关,简单说,就是同样的一个方法能够根据输入数据的不同,做出不同的处理,即方法的重载—有不同的参数列表(静态多态性)
而当子类继承自父类的相同方法,输入数据一样但要做有别于父类的响应时,你就要覆盖父类方法,即在子类中重写该方法—相同参数,不同实现(动态多态性)
重写方法的规则:
1、 参数列表必须完全与被重写的方法相同,否则不能称其为重写而是重载;
2、 返回类型必须一直与被重写的方法的返回类型相同,否则不能称其为重写而是重载。
3、 访问修饰符的限制一定要大于被重写方法的修饰符(public>protected>default>private)
4、 重写方法一定不能抛出新的检查异常或者比被重写方法声明更加宽泛的检查型异常。如:父类的一个方法声明了一个检查异常IOException,在重写这个方法时就不能抛出Exception,只能抛出IOException子类异常,可以抛出非检查异常。
重载的规则:
1、 必须具有不同的参数列表;
2、 可以有不同的返回类型,只要参数列表不同就可以了。
3、 可以有不同的访问修饰符。
4、 可以抛出不同的异常。
重写与重载的区别在于:
重写多态性起作用,对调用被重载过的方法可以大大减少代码的输入量,同一个方法名只要往里面传递不同的参数就可以拥有不同的功能或返回值。
用好重写和重载可以设计一个结构清晰而简洁的类,可以说重写和重载在编写代码过程 中的作用非同一般。
继承:可以基于已经存在的类构造一个新类。继承已经存在的类就可以复用这些类的方法和域。在此基础上可以添加新的方法或域,从而扩充了类的功能。
接口中的成员变量public static final
接口中的成员变量必须声明为public static final,而为什么使用final?首先,接口实际上比抽象类更加抽象,它是最高层次的抽象。接口中的所有方法都是抽象方法,不会有任何实现方面的代码,而它的具体实现是要靠实现这个接口的具体类来具体实现的。而且,这个接口中的方法是不可更改的。同样的,接口中的变量也是不可更改的。所以,要声明为final。
接口是一种协议,协议是所有实现接口的类都必须遵守的。如果变量不是final,那么每个实现接口的类就可以更改这个变量的值,就违反了OCP原则。
为什么要使用static?
该问题与java语言本身的机制有关。我们知道java不支持多继承,但是一个类可以实现多个接口。我们假设有两个接口A和B,而类C实现了接口A和B。假设,此时,A和B中都有一个变量N,如果N不是static类型的,那么在C中该如何区分N到底是A的还是B的呢?而,如果是static类型的,我们可以通过A.N和B.N来区别调用A或者B中的成员变量N、
接口和抽象类的联系和区别:
区别:
1、 抽象类是“定义”(什么是人),接口是规范类(人的种类,如黑人白人)。即抽象类是通过分析实体类的联系得到的,而接口则是规范,实体类必须来实现这个接口的方法。
2、 接口定义的方法都是抽象的,而接口使用的是implements,在java中一般不允许使用多重继承,所以只能继承一个抽象类,却可以实现多个接口。
3、 接口定义的方法都是抽象的,而抽象类可定义非抽象方法。
4、 Abstract class和interface所反映出的设计理念不同。(Abstract class表示的是(Is-a)关系,interface表示的是(like a)关系。
什么是抽象类?
在面向对象的概念中,所有的对象都是用类来描述的,就是说某一个新的对象是通过实例化一个类而来的,但是,并不是所有的类都是用来描述对象的,也就是说并不是所有的类都可以用来实例化,所以说如果一个类并没有足够的信息来描绘一个具体的对象,那这个类就是抽象类。
如:一个Dog类,一个cat类,一个bird类,他们都有自己的概念,但同时他们又都是Animal这一个抽象的类,Dog,Cat和Bird都可以被实例化,但是Animal,动物这个抽象类无法实例化。
抽象类的特点:
A、 抽象类有自己的成员变量,由一个或多个抽象方法,当然也可以有非抽象方法,(抽象类另一个定义可以是:含有抽象方法的类叫做抽象类)
B、 抽象类不能被实例化,但是他有自己的构造器,构造器的作用是在子类在实例化的时候回默认调用父类(抽象类)的构造器,就是初始化的时候有用。
C、 抽象类的子类必须为抽象类中的抽象方法提高方法定义,否则,该子类也是抽象类。
补充:java编程思想中提到,我们也可以创建一个没有任何抽象方法的抽象类。如果有一个类包含Abstract方法都显得没有实际意义,而我们也需要阻止生产这个类的任何对象,那么这样做就很有用了。
什么是接口?
接口是一个极度抽象的类,它只有方法的声明,没有任何的实现方法,也就是说它只知道应该做什么,至于具体怎么去做而不去管。
特点:
A、 接口中的成员变量默认都是static和final类型的,即使省略不写也是这种情况;成员变量在定义的时候必须直接初始化它。
B、 接口中的方法默认都是Abstract类型的,即使省略不写也是这种情况;
C、 接口中的成员变量和成员方法的访问权限都是public类型,即使省略不写也是这种情况。
D、 接口是可以继承的,表示接口的继承依然是extends(接口不能被继承,只能被实现);
E、 一个类可以实现多个接口,多个接口名之间用逗号间隔。
什么时候用接口什么时候用抽象类?
自我感觉,抽象类都是从一些相似的对象中抽象出来一个相对无法具体描述的一个类,它的子类之间是由相似性的;
接口更侧重于对相同的动作进行抽象封装。当我们关注一个事务的本质的时候,用抽象类;当我们关注的是一个操作的时候,用接口。
反射定义:
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
反射机制能做什么?反射机制主要提供的功能:1、在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。
Java.long.reflect里常用方法
获取类自身的信息:
获取包路径:cls.getPackage();
获取类名:cls.getName();或者cls.getSimpleName();
获取类修饰符:field.getModifiers();
                   而获取基本的类型的值:
filed.getModifiers();
         filed.getInt(obj);
         filed.getDouble(obj);
获取类方法:cls.getMethods();或者cls.getDeclareMethods();
cls.getMethods()该反射方法可以获得自身类及父类中所定义的方法。
getDeclareMethods()该方式所定义的public,protected,defaul private访问权限的方法,但是不会取父类的方法或继承父类的方法。这个方法常用。
获取类属性:cls.getFields();或者cls.getDeclaredFields();(和获取类方法是相同的原理。)
获取类构造器:cls.getConstructors();
获取标注信息:cls.getAnnotations();
该类是否为基本类型数据 cls.isPrimitive();
获取系统类的getClassLoader();这个也较为常用,加载类的时候。
获取资源信息:
Cls.getResource(“http://xx/xx”);
Cls.getResourceAsStream(“xx.properties”);
判断一个方法的修饰符:


Method:

返回类型method.getReturnType();

参数类型method.getParameterTypes();

名字method.getName();    修饰符:method.getModifiers();

标注:method.getAnnotations();

反射调用方法method.invoke(obj,args);是一个关键的方法。

 

异常:阻止当前方法或作用域继续执行的问题。虽然java中有异常处理机制,但要知道,决不应该用“正常”的态度来看待异常。决定一点说异常就是某种意义上的错误,就是问题。它可能会导致程序失败。之所以java要提出异常处理机制,就是要告诉开发人员,你的程序出现了不正常情况,要注意。

Java的异常体系:

Throwable类是java语言中所有错误或异常的超类(这就是一切皆可抛的东。西)。它有两个子类:Error和Exception。

Error:用于指示合理的应用程序不应该试图捕获的严重问题。这种情况是很大的问题,大到你不能处理了,所以听之任之就可以了,不用管。如VirtualMachineError:当Java虚拟机崩溃或用尽了它继续操作所需的资源时,抛出该错误。好吧,就算这个异常存在了,那么应该何时,如何处理它那?交给JVM吧,没有比它更专业的啦。

异常:程序在运行期间发生的不正常事件,它会打断指令的正常流程。异常都是发生在程序的运行期,编译出现的问题叫语法错误。

异常处理机制:

1)       当程序在运行过程中出现了异常,JVM自动创建一个该类型的异常对象。同时把这个异常对象交给运行时系统(抛出异常)

2)       运行时系统接受到一个异常对象时,它会在产生异常的代码附近查找相应的处理方式。

3)       异常处理的两种方式:

1、       捕获并处理:在异常的代码附近显示用try/catch处理(不合理),运行时系统捕获后会查询相应的catch处理块,在catch处理块中对该异常进行处理。

2、       查看异常的方法是否有向上声明异常,有向上声明,向上级查询处理语句,如果没有向上声明,JVM中断程序的运行并处理。用throws向外声明。

异常的分类:


Exception:它指出了合理的应用程序想要捕获的条件。Exception又分为两类:一种是CheckedException,一种是UncheckedException。这两种Exception的区别主要是CheckedException需要用try…catch显示的捕获,而UncheckedException不需要捕获。通常UncheckedException又叫做RuntimeException。

不要为了使用异常而使用异常。异常是程序设计的一部分,对它的设计也要考究点。

未检查异常(运行时异常)和已检查异常:

已检查异常:该种异常必须掌握在你的代码里,如SQLException。对于此种异常,编译器会检查是否提供了处理器。对已检查处理异常的几种处理方式:

1】继续抛出,这是一个消极方法,一直可以抛到java虚拟机处理。2】用try…catch捕获3】对该种检查异常必须处理,或者必须捕获,要不就必须抛出。

未检查异常:编译器不会查看是否为这些错误提供了处理器。对于未检查异常的处理方法:1】捕获,2】继续抛出,3】不处理。

Try/catch/finally
Finally一定会被执行,如果finally里有return语句,则覆盖try/catch里面的return。最好不要在catch和finally块中加return语句。
例1
public class Test3 {
    public static void main(String[] args) {
        try {
            System.out.println("try...");
            return;
        } finally {
            System.out.println("finally...");
        }
  结果为:
try...
finally...

这个大家都知道,在try中的return真正返回之前会执行finally中的语句。
例子:
public class Test3 {
    public static void main(String[] args) {
        System.out.println("main..." + getValue());
    }
    public static int getValue() {
        try {
            System.out.println("try...");
            return 0;
        } finally {
            System.out.println("finally...");
            return 1;
        }}}
 结果为:
try...   finally...    main...1
 此处在执行finally中语句的时候,return 1;返回了,所以try中的return语句不会得到执行。
例3

public class Test3 {
    public static void main(String[] args) {
        System.out.println("main..." + getValue());
    }
    public static int getValue() {
        int i = 0;
        try {
            System.out.println("try...");
         return i;
        } finally {
            System.out.println("finally...");
            i++;
        }
    }
}


结果为:
try...
finally...
main...0
这个很多人都不知道为什么了,按道理说,在try的return执行之前,在finally之中已经更改了i的值,为什么return的值任然是0而不是1呢? 
  实际上,Java 虚拟机会把 finally 语句块作为 subroutine(对于这个 subroutine 不知该如何翻译为好,干脆就不翻译了,免得产生歧义和误解。)直接插入到 try 语句块或者 catch 语句块的控制转移语句之前。但是,还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是 finally 语句块)之前,try 或者 catch 语句块会保留其返回值到本地变量表(Local Variable Table)中。待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)。 
  所以,上例中try中返回的不是finally中的i,而是在执行finally之前放在本地变量表中的i。所以返回的仍然是0。
例4:

public class Test3 {
    public static void main(String[] args) {
        System.out.println("main..." + getValue());
    }
    public static int getValue() {
        int i = 0;
        try {
            System.out.println("try...");
        } finally {
            System.out.println("finally...");
            i++;
            return i;
        }
    }
}

结果为:

try...  finally...  main...1

由于try中没有返回语句,所以不会将try中的i放到本地变量表,执行完finally之后也不会再返回try,而是使用finally中的i直接返回。

5

结果为:
try...
finally...
main...1
虽然try在执行finally之前将返回值保存到了本地变量表,但是finally中的i是另外一个副本,被finally操作后直接返回,不再回到try中,所以try中的i无法恢复
例6:

public class Test3 {
    public static void main(String[] args) {
        System.out.println("main..." + getValue());
    }
    public static int getValue() {
        int i = 0;
        try {
            System.out.println("try...");
        } finally {
            System.out.println("finally...");
            i++;
        }
        return i;
    }
}

结果:

try...

finally...

main...1

因为try中没有return语句,所以在执行finally之前不会存放i到本地变量表,也不会再返回到try

7

public class Test3 {
    public static void main(String[] args) {
        System.out.println("main..." + getValue());
    }
    public static int getValue() {
        int i = 0;
        try {
            System.out.println("try...");
            return test();
        } finally {
            System.out.println("finally...");
            i++;
        }
    }
    public static int test() {
        System.out.println("test...");
        return 10;
    }
}

结果:

try...

test...

finally...

main...10

此处try中的return test()就等同于int r = test();return r;,另外,catchfinally的关系和上面是一样的,这里省略了。

集合类:

ListSetList

Collection接口包含三个子接口分别为SetQueueList

List接口主要有两个实现类ArrayListLinkedListList接口是有序集合、元素可以重复,次序是List接口最重要的特点,他是以元素的添加的顺序作为集合的顺序。其中ArrayList底层是通过数组实现的,数组的初始长度为10,可以扩展数组。LinkedList底层是通过双向链表实现的,因此可以在首添加删除元素,因此可以作为栈、队列、双端队列使用。

Set接口主要有两个实现类HashSetTreeSetSet是无序的、不可重复的。

HashSet/TreeSet/HashMap/TreeMap/hashTable这些类的底层实现。
TreeMap底层基于红黑树,先指定一个根节点,若该跟节点不存在,则说明TreeMap里面没有元素,将当前准备加入的元素加入进去当成根节点。如果里面已经存在元素,在判断TreeMap里面是否有比较器,若比较器不为空,就按照比较器的比较方法进行比较,如果为空,按照该Key自己的内部自然顺序进行比较,前提是该key是由比较性的,否则将该类继承comparable并重写compareTo方法,使之具有比较性。
TreeSet:该底层是一个TreeMap,但是Value值为一个虚拟值。此原理和TreeMap一致。
HashMap:底层基于Hash表,HashMap排序是无序的,Value可以当成key的附属品,一旦key值指定,则value
就在该位置。指定key的位置的方法,首先,每一个key都有自己的hashCode,根据该hashCode利用hash算法,可以算出该key在当前table的位置,一旦位置确定,判断位置是否有对应的值,如果有,在通过equals比较,根据返回值,如果返回值为真,则覆盖老的value值,key值不覆盖,但是他们都指向同一个value值,key形成链。如果没有值,则直接将该key存在指定位置。其实HashMap底层就是一个Entry数组。HashMap中的每一元素都有自己的索引,指定的位置,也就是东西该放的地方,
HashSet:底层基于HashMap实现。
HashMap和Hashtable的底层实现都是数组+链表结构实现


hashMap和hashTable还有concurrentHashMap之关联。
I、HashMap和hashTable的区别?

HashMap是非线程安全的,HashTable是线程安全的,HashTable内部的方法基本都是synchronized。
HashMap的键和值都允许有null值存在,而HashTable则不行。
因为线程安全的问题,HashMap效率比HashTable的要高。
相同点:他两的内部存储结构都是哈希表。
II、ConcurrentHashMap是线程安全的HashMap;ConcurrentHashMap实现线程安全,肯定是不可能是每个方法加Synchronized,那样就变成了HashTable。从ConcurrentHashMap代码中可以看出,它引入一个“分段锁的概念”,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中。


HashMap和Hashtable的区别?
1. 两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全
Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合(Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单的说就是Collections.synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步,类似的其它Collections.synchronizedXX方法也是类似原理)
2. HashMap可以使用null作为key,而Hashtable则不允许null作为key
虽说HashMap支持null值作为key,不过建议还是尽量避免这样使用,因为一旦不小心使用了,若因此引发一些问题,排查起来很是费事
HashMap以null作为key时,总是存储在table数组的第一个节点上
3. HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类
4. HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75
HashMap扩容时是当前容量翻倍即:capacity*2,Hashtable扩容时是容量翻倍+1即:capacity*2+1

1. 两者计算hash的方法不同
Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模


HashMap和Hashtable的底层实现都是数组+链表结构实现
hashSet和HashMap有什么区别


什么是hashSet?
HashSet实现了Set接口,它不允许集合中有重复的值,当我们提到HashSet时,第一件事情就是在将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。
public boolean add(Object o)方法用来在Set中添加元素,当元素值重复时则会立即返回false,如果成功添加的话会返回true。

什么是HashMap

HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许重复的键。Map接口有两个基本的实现,HashMapTreeMapTreeMap保存了对象的排列次序,而HashMap则不能。HashMap允许键和值为nullHashMap是非synchronized的,但collection框架提供方法能保证HashMap synchronized,这样多个线程同时访问HashMap时,能保证只有一个线程更改Map

public Object put(Object Key,Object value)方法用来将元素添加到map中。

hashTable和concurrentHashMap的区别
ConcurrentHashMap融合了hashtable和hashmap二者的优势。
hashtable是做了同步的,hashmap未考虑同步。所以hashmap在单线程情况下效率较高。hashtable在的多线程情况下,同步操作能保证程序执行的正确性。
但是hashtable每次同步执行的时候都要锁住整个结构。看下图:


图左侧清晰的标注出来,lock每次都要锁住整个结构。
ConcurrentHashMap正是为了解决这个问题而诞生的。
ConcurrentHashMap锁的方式是稍微细粒度的。 ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。
试想,原来 只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。
更令人惊讶的是ConcurrentHashMap的读取并发,因为在读取的大多数时候都没有用到锁定,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(这一点在桶更多时表现得更明显些)。只有在求size等操作时才需要锁定整个表。

而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。

初始化,java类的成员初始化顺序和初始化块知识
顺序:
1、父类静态成员和静态初始化块,按在代码中出现的顺序依次执行。
2、子类静态成员和静态初始化块,按在代码中出现的顺序依次执行。
3、父类的实例成员和实例初始化块,按在代码中出现的顺序一次执行。
4、执行父类的构造方法。
5、子类实例成员和实例初始化块,按在代码中出现的顺序依次执行。
6、执行子类的构造方法。
例子:

1.	public class Test {  
2.	    /** 
3.	     * Description 
4.	     * @param args 
5.	     */  
6.	    public static void main(String[] args) {  
7.	        Son s = new Son();  
8.	    }  
9.	  
10.	}  
11.	  
12.	class Parent{  
13.	  
14.	    {  
15.	        System.out.println("parent中的初始化块");  
16.	    }  
17.	    static{  
18.	        System.out.println("parent中static初始化块");  
19.	    }  
20.	      
21.	    public Parent(){  
22.	        System.out.println("parent构造方法");  
23.	    }  
24.	}  
25.	  
26.	class Son extends Parent{  
27.	    {  
28.	        System.out.println("son中的初始化块");  
29.	    }  
30.	      
31.	    static{  
32.	        System.out.println("son中的static初始化块");  
33.	    }  
34.	      
35.	    public Son(){  
36.	        System.out.println("son构造方法");  
37.	    }  
38.	      
39.	}  
结果:
1. parent中static初始化块  
2. son中的static初始化块  
3. parent中的初始化块  
4. parent构造方法  
5. son中的初始化块  
6. son构造方法  
初始化块主要用于对象的初始化操作,在创建对象时调用,可以用于完成初始化属性值、加载其他的类的功能。初始化块和构造方法功能类似,可以在创建对象的时候完成一些初始化的操作,一般的情况下,构造方法初始和初始化块初始化可以通用。
构造方法在初始化的时候可以通过参数传值,但是初始化块不能,初始化块的初始化在构造方法之前执行,如果搞糟方法多次重载,此时可以考虑构造方法中共通的代码放到初始化块中进行初始化。

静态初始化块和非静态初始化块的区别?
非静态初始化块主要是用于对象的初始化操作,在每次创建对象的时都要调用一次,其执行顺序在构造方法之前。
在初始化块之前有static修饰,则为静态初始化块。由于非静态成员不能再静态方法中使用,同样也不能在静态初始化块中,因此,静态初始化块主要用于初始化静态变量和静态方法,静态初始化块只调用一次,是在类的第一次加载到内存时,并非一定要创建对象才执行,
静态初始化块比非静态初始化块先执行。



进程和线程的概念:

进程,是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。每一个进程都有一个自己的地址空间,即进程空间或(虚空间)。进程空间的大小只与处理机的位数有关,一个 16 位长处理机的进程空间大小为 216 ,而 32 位处理机的进程空间大小为 232 。进程至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。

    线程,在网络或多用户环境下,一个服务器通常需要接收大量且不确定数量用户的并发请求,为每一个请求都创建一个进程显然是行不通的,——无论是从系统资源开销方面或是响应用户请求的效率方面来看。因此,操作系统中线程的概念便被引进了。线程,是进程的一部分,一个没有线程的进程可以被看作是单线程的。线程有时又被称为轻权进程或轻量级进程,也是 CPU 调度的一个基本单位。

    说到这里,我们对进程与线程都有了一个大体上的印象,现在开始说说二者大致的区别。进程的执行过程是线状的,尽管中间会发生中断或暂停,但该进程所拥有的资源只为该线状执行过程服务。一旦发生进程上下文切换,这些资源都是要被保护起来的。这是进程宏观上的执行过程。而进程又可有单线程进程与多线程进程两种。我们知道,进程有 一个进程控制块 PCB ,相关程序段 和 该程序段对其进行操作的数据结构集 这三部分,单线程进程的执行过程在宏观上是线性的,微观上也只有单一的执行过程;而多线程进程在宏观上的执行过程同样为线性的,但微观上却可以有多个执行操作(线程),如不同代码片段以及相关的数据结构集。线程的改变只代表了 CPU 执行过程的改变,而没有发生进程所拥有的资源变化。出了 CPU 之外,计算机内的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。与进程控制表和 PCB 相似,每个线程也有自己的线程控制表 TCB ,而这个 TCB 中所保存的线程状态信息则要比 PCB 表少得多,这些信息主要是相关指针用堆栈(系统栈和用户栈),寄存器中的状态数据。进程拥有一个完整的虚拟地址空间,不依赖于线程而独立存在;反之,线程是进程的一部分,没有自己的地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。

    线程可以有效地提高系统的执行效率,但并不是在所有计算机系统中都是适用的,如某些很少做进程调度和切换的实时系统。使用线程的好处是有多个任务需要处理机处理时,减少处理机的切换时间;而且,线程的创建和结束所需要的系统开销也比进程的创建和结束要小得多。最适用使用线程的系统是多处理机系统和网络系统或分布式系统。

1. 线程的执行特性。
    线程只有 3 个基本状态:就绪,执行,阻塞。
    线程存在 5 种基本操作来切换线程的状态:派生,阻塞,激活,调度,结束。
2. 进程通信。
    单机系统中进程通信有 4 种形式:主从式,会话式,消息或邮箱机制,共享存储区方式。
        主从式典型例子:终端控制进程和终端进程。
        会话式典型例子:用户进程与磁盘管理进程之间的通信。

Synchronize和volatile的区别与联系

volatile是一个变量修饰符,而synchronized是一个方法或块的修饰符。所以我们使用这两种关键字来指定三种简单的存取变量的方式。

        int i1;                      int geti1(){return i1;}

volatileint i2;                      intgeti2() {return i2;}

     inti3;          synchronized int geti3(){return i3;}

geti1()在当前线程中立即获取在i1变量中的值。线程可以获得变量的本地拷贝,而所获得的变量的值并不一定与其他线程所获得的值相同。特别是,如果其他的线程修改了i1的值,那么当前线程获得的i1的值可能与修改后的值有所差别。实际上,Java有一种主内存的机制,使用一个主内存来保存变量当前的正确的值。线程将变量的值拷贝到自己独立的内存中,而这些线程的内存拷贝可能与主内存中的值不同。所以实际当中可能发生这样的情况,在主内存中i1的值为1,线程1和线程2都更改了i1,但是却没把更新的值传回给主内存或其他线程中,那么可能在线程1中i1的值为2,线程2中i1的值却为3。

另一方面,geti2()可以有效的从主内存中获取i2的值。一个volatile类型的变量不允许线程从主内存中将变量的值拷贝到自己的存储空间。因此,一个声明为volatile类型的变量将在所有的线程中同步的获得数据,不论你在任何线程中更改了变量,其他的线程将立即得到同样的结果。由于线程存取或更改自己的数据拷贝有更高的效率,所以volatile类型变量在性能上有所消耗。

那么如果volatile变量已经可以使数据在线程间同步,那么synchronizes用来干什么呢?两者有两方面的不同。首先,synchronized获取和释放由监听器控制的锁,如果两个线程都使用一个监听器(即相同对象锁),那么监听器可以强制在一个时刻只有一个线程能处理代码块,这是最一般的同步。另外,synchronized还能使内存同步。在实际当中,synchronized使得所有的线程内存与主内存相同步。所以geti3()的执行过程如下:

1.   线程从监听器获取对象的锁。(这里假设监听器非锁,否则线程只有等到监听器解锁才能获取对象锁)

2.   线程内存更新所有的变量,也就是说他将读取主内存中的变量使自己的变量保证有效。(JVM会使用一个“脏”标志来最优化过程,使得仅仅具有“脏”标志变量被更新。详细的情况查询JAVA规范的17.9)

3.   代码块被执行(在这个例子中,设置返回值为刚刚从主内存重置的i3当前的值。)

4.   任何变量的变更将被写回到主内存中。但是这个例子中geti3()没有什么变化。

5.   线程释放对象的锁给监听器。

所以volatile只能在线程内存和主内存之间同步一个变量的值,而synchronized则同步在线程内存和主内存之间的所有变量的值,并且通过锁住和释放监听器来实现。显然,synchronized在性能上将比volatile更加有所消耗。

 

=============关于两者的区别===================

1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile
仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
3.
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
4.volatile
不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5.volatile
标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

 

红字体部分的原因如下:
线程A修改了变量还没结束时,另外的线程B可以看到已修改的值,而且可以修改这个变量,而不用等待A释放锁,因为Volatile变量没上锁

1volatile
    
它所修饰的变量不保留拷贝,直接访问主内存中的。
   
Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。


2synchronized

      当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码

    一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

     二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

     三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

     四、当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

     五、以上规则对其它对象锁同样适用.

 

区别:

  一、volatile是变量修饰符,而synchronized则作用于一段代码或方法。

 二、volatile只是在线程内存和内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。 

 

Sleep和wait的区别

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

获取对象锁进入运行状态。

Sleep和yield的区别

1、sleep()方法会给其他线程运行的机会,而不考虑其他线程的优先级,因此会给较低线程一个运行的机会;yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。

  2、当线程执行了sleep(long millis)方法后,将转到阻塞状态,参数millis指定睡眠时间;当线程执行了yield()方法后,将转到就绪状态。

  3、sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明抛出任何异常

  4、sleep()方法比yield()方法具有更好的移植性

  如果希望明确地让一个线程给另外一个线程运行的机会,可以采取以下的办法之一:

  1、调整各个线程的优先级

  2、让处于运行状态的线程调用Thread.sleep()方法

  3、让处于运行状态的线程调用Thread.yield()方法

  4、让处于运行状态的线程调用另一个线程的join()方法

  首先,wait()和notify(),notifyAll()是Object类的方法,sleep()和yield()是Thread类的方法。

  (1)。常用的wait方法有wait()和wait(long timeout):

  voidwait() 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。

  voidwait(long timeout) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。

  wait()后,线程会释放掉它所占有的"锁标志",从而使线程所在对象中的其它synchronized数据可被别的线程使用。

  wait()和notify()因为会对对象的"锁标志"进行操作,所以它们必须在synchronized函数或synchronized代码块中进行调用。如果在non- synchronized函数或non-synchronized代码块中进行调用,虽然能编译通过,但在运 行时会发生IllegalMonitorStateException的异常。

  (2)。Thread.sleep(long millis),必须带有一个时间参数。

  sleep(long)使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;

  sleep(long)可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;

  sleep(long)是不会释放锁标志的。

  (3)。yield()没有参数。

  sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出CPU占有权,但让出的时间是不可设定的。yield()也不会释放锁标志。

  实际上,yield()方法对应了如下操作: 先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则继续运行原来的线程。所以yield()方法称为"退让",它把运行机会让给了同等优先级的其他线程。

  sleep方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以不可能让出较低优先级的线程些时获得CPU占有权。 在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I/O阻塞,那么较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

  yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。所以yield()只能使同优先级的线程有执行的机会。

  volitile 语义

  volatile相当于synchronized的弱实现,也就是说volatile实现了类似synchronized的语义,却又没有锁机制。它确保对volatile字段的更新以可预见的方式告知其他的线程。

  volatile包含以下语义:

  (1)Java 存储模型不会对valatile指令的操作进行重排序:这个保证对volatile变量的操作时按照指令的出现顺序执行的。

  (2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个valatile变量的写操作后,其后的任何读操作理解可见此写操作的结果。

  尽管volatile变量的特性不错,但是volatile并不能保证线程安全的,也就是说volatile字段的操作不是原子性的,volatile变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!

  数据同步:

  线程同步的特征:

  1、如果一个同步代码块和非同步代码块同时操作共享资源,仍然会造成对共享资源的竞争。因为当一个线程执行一个对象的同步代码块时,其他的线程仍然可以执行对象的非同步代码块。(所谓的线程之间保持同步,是指不同的线程在执行同一个对象的同步代码块时,因为要获得对象的同步锁而互相牵制)

  2、每个对象都有唯一的同步锁

  3、在静态方法前面可以使用synchronized修饰符,但是要注意的是锁对象是类(用Object.class而不能用this),而不是这个类的对象。

  4、当一个线程开始执行同步代码块时,并不意味着必须以不间断的方式运行,进入同步代码块的线程可以执行Thread.sleep()或者执行Thread.yield()方法,此时它并不释放对象锁,只是把运行的机会让给其他的线程。

  5、synchronized声明不会被继承,如果一个用synchronized修饰的方法被子类覆盖,那么子类中这个方法不在保持同步,除非用synchronized修饰。

6、synchronized 关键字能够修饰一个对象实例中的函数或者代码块。 在一个非静态方法中 this 关键字表示当前的实例对象。 在一个synchronized 修饰的静态的方法中,这个方法所在的类使用 Class 作为实例对象

 

Lock和Synchronized的区别

在分布式开发中,锁是线程控制的重要途径。Java为此也提供了2种锁机制,synchronizedlock。做为Java爱好者,自然少不了对比一下这2种机制,也能从中学到些分布式开发需要注意的地方。
 

我们先从最简单的入手,逐步分析这2种的区别。
 

一、synchronizedlock的用法区别
 

synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
 

lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
 

用法区别比较简单,这里不赘述了,如果不懂的可以看看Java基本语法。
 

二、synchronizedlock性能区别
 

synchronized是托管给JVM执行的,而lockjava写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
 

说到这里,还是想提一下这2中机制的具体区别。据我所知,synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
 

Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare andSwap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
 

现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
 

我也只是了解到这一步,具体到CPU的算法如果感兴趣的读者还可以在查阅下,如果有更好的解释也可以给我留言,我也学习下。
 

三、synchronizedlock用途区别
 

synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。
 

1.某个线程在等待一个锁的控制权的这段时间需要中断
2.
需要分开处理一些wait-notifyReentrantLock里面的Condition应用,能够控制notify哪个线程
3.
具有公平锁功能,每个到来的线程都将排队等候

线程的几种状态及转换?

线程在一定条件下,状态会发生变化。线程一共有以下几种状态:

1、新建状态(New):新创建了一个线程对象。

2就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,只等待获取CPU的使用权即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。

3、运行状态(Running)就绪状态的线程获取了CPU,执行程序代码。

4、阻塞状态(Blocked)阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

阻塞的情况分三种:

(1)等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入等待池中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()notifyAll()方法才能被唤醒,

(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

(3)其他阻塞:运行的线程执行sleep()join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5、死亡状态(Dead)线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程变化的状态转换图如下:


注:拿到对象的锁标记,即为获得了对该对象(临界区)的使用权限。即该线程获得了运行所需的资源,进入“就绪状态”,只需获得CPU,就可以运行。因为当调用wait()后,线程会释放掉它所占有的“锁标志”,所以线程只有在此获取资源才能进入就绪状态。
下面小小的作下解释: 
1、线程的实现有两种方式,一是继承Thread类,二是实现Runnable接口,但不管怎样,  当我们new了这个对象后,线程就进入了初始状态; 
2、当该对象调用了start()方法,就进入就绪状态; 
3、进入就绪后,当该对象被操作系统选中,获得CPU时间片就会进入运行状态; 
4、进入运行状态后情况就比较复杂了 
    4.1、run()方法或main()方法结束后,线程就进入终止状态; 
    4.2、当线程调用了自身的sleep()方法或其他线程的join()方法,进程让出CPU,然后就会进入阻塞状态(该状态既停止当前线程,但并不释放所占有的资源即调用sleep ()函数后,线程不会释放它的“锁标志”。)。当sleep()结束或join()结束后,该线程进入可运行状态,继续等待OS分配CPU时间片。典型地,sleep()被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。
    4.3、线程调用了yield()方法,意思是放弃当前获得的CPU时间片,回到就绪状态,这时与其他进程处于同等竞争状态,OS有可能会接着又让这个进程进入运行状态; 调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间片从而需要转到另一个线程。yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
   4.4、当线程刚进入可运行状态(注意,还没运行),发现将要调用的资源被synchroniza(同步),获取不到锁标记,将会立即进入锁池状态,等待获取锁标记(这时的锁池里也许已经有了其他线程在等待获取锁标记,这时它们处于队列状态,既先到先得),一旦线程获得锁标记后,就转入就绪状态,等待OS分配CPU时间片;
4.5. suspend() 和 resume()方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume()被调用,才能使得线程重新进入可执行状态。典型地,suspend()和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume()使其恢复。 
   4.6、wait()和 notify() 方法:当线程调用wait()方法后会进入等待队列(进入这个状态会释放所占有的所有资源,与阻塞状态不同),进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒(由于notify()只是唤醒一个线程,但我们由不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒,因此在实际使用时,一般都用notifyAll()方法,唤醒有所线程),线程被唤醒后会进入锁池,等待获取锁标记。 
wait() 使得线程进入阻塞状态,它有两种形式:
一种允许指定以毫秒为单位的一段时间作为参数;另一种没有参数。前者当对应的 notify()被调用或者超出指定时间时线程重新进入可执行状态即就绪状态,后者则必须对应的 notify()被调用。当调用wait()后,线程会释放掉它所占有的“锁标志”,从而使线程所在对象中的其它synchronized数据可被别的线程使用。waite()和notify()因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronizedblock中进行调用。如果在non-synchronized函数或non-synchronizedblock中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。
 
注意区别:初看起来wait() 和 notify() 方法与suspend()和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的suspend()及其它所有方法在线程阻塞时都不会释放占用的锁(如果占用了的话),而wait() 和 notify() 这一对方法则相反。
上述的核心区别导致了一系列的细节上的区别
首先,前面叙述的所有方法都隶属于 Thread类,但是wait() 和 notify() 方法这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用任意对象的notify()方法则导致因调用该对象的 wait()方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
其次,前面叙述的所有方法都可在任何位置调用,但是wait() 和 notify() 方法这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException异常。
wait() 和 notify()方法的上述特性决定了它们经常和synchronized方法或块一起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于block和wake up 原语(这一对方法均声明为 synchronized)。它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。
关于 wait() 和 notify() 方法最后再说明两点:
第一:调用notify() 方法导致解除阻塞的线程是从因调用该对象的 wait()方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
第二:除了notify(),还有一个方法 notifyAll()也可起到类似作用,唯一的区别在于,调用 notifyAll()方法将把因调用该对象的 wait()方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend()方法和不指定超时期限的wait()方法的调用都可能产生死锁。遗憾的是,Java并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。













展开阅读全文

没有更多推荐了,返回首页