一、面向对象的特点有哪些?
抽象、继承、封装、多态。
二、接口和抽象类有什么联系和区别?
1、我所理解的抽象类
1、1 抽象类和类的相样韵味
1、抽象类和类一样,都是可以用来继承的 2、类可以有的成分,抽象类都可以拥有【包括构造方法、static静态修饰成分等】
抽象类正如这个名字定义一样,它也是一个类
1、2 抽象方法
讲不同样韵味之前很有必要要先深知的抽象方法: 1、抽象方法没有方法体 2、抽象方法必须用abstract关键字修饰 3、有抽象方法的类必然是抽象类 4、抽象方法必须为public或者protected,缺省情况下默认为public
抽象类不一定有抽象方法
1、3 抽象类和类的异样韵味
1、抽象类必须用abstract关键字进行修饰,有abstract修饰的类就是抽象类! 2、抽象类可有可无抽象方法 3、抽象类虽然有构造方法但不能用来直接创建对象实例 4、抽象类不能用final、private修饰 5、外部抽象类不能用Static修饰,但内部的抽象类可以使用static声明。
1、4 掌握抽象类
抽象类就是为了继承而存在的,定义了一个抽象类,却不去继承它,创建的这个抽象类就毫无意义!
抽象类虽然有构造方法但不能直接被实例化,要创建对象涉及向上转型,主要是用于被其子类调用
还有对于抽象类可以没有抽象方法这句话,这只是一个要记住的重要概念,一定要记住!实际开发中抽象类一般都有抽象方法,不然该抽象类就失去存在意义,和普通类没啥两样!
一个普通类A继承一个抽象类B,则子类A必须实现父类B的所有抽象方法。如果子类A没有实现父类B的抽象方法,则必须将子类A也定义为为abstract类,也就是抽象类。
2、我所理解的接口
接口(interface)可以说成是抽象类的一种特例,抽象类与接口这两者有太多相似的地方,又有太多不同的地方。相对的,接口更像是一种行为的抽象!
2、1 接口特性
1、接口中的方法默认为public abstract类型,接口中的成员变量类型不写默认为public static final。 2、接口没有构造方法 3、接口可以实现“多继承”,一个类可以实现多个接口,实现写法格式为直接用逗号隔开即可。
2、2 接口必知
接口中只能含有public static final变量,不写默认是public static final,用private修饰会编译失败。
接口中所以的方法会被隐式地指定为public abstract方法且只能是public abstract方法,用其他关键字,比如private、protected、static、 final等修饰会编译失败。
3、抽象类和接口本质区别
抽象类和接口本质区别主要从语法区别和设计思想两方面下手
3、1 语法区别
1.抽象类可以有构造方法,接口中不能有构造方法。
2.抽象类中可以有任何类型成员变量,接口中只能有public static final变量
3.抽象类中可以包含非抽象的普通方法,接口中的可以有非抽象方法,比如deaflut方法
4.抽象类中的抽象方法的访问类型可以是public,protected和(默认类型,虽然 eclipse下不报错,但应该也不行),但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。
5.抽象类中可以包含静态方法,接口中不能包含静态方法
6.抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。
7.一个类可以实现多个接口,但只能继承一个抽象类。
3、2 设计思想区别
对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现(相当于写普通类的普通方法并添加方法体的实现代码),子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。
从设计角度来讲抽象类是对一种对类抽象,抽象类是对整个类整体进行抽象,包括属性、行为。而接口是对行为的抽象,接口是对类局部(行为)进行抽象。从某一角度来讲,接口更像是抽象的抽象!
三、重载和重写有什么区别?
1、方法的重写(Override)(方法覆盖)
(1)重写介绍:
java SE5增加@Override注解,并不是关键字,覆写某个方法时,添加此注解。@Override可以防止在不想重载时而意外地进行重载。
父类与之类之间的多态性,对父类的函数进行重新定义。同方法名和同参数;
子类中的方法与父类中的某一方法具有相同的方法名、返回类型和参数列表,则新方法将覆盖原有的方法,若需要父类中原有的方法,使用super关键字;
子类函数的访问修饰符不能小于父类的;(父类为public,则子类不能为protected或者private等,只能是public的)
子类抛出异常小于等于父类方法抛出异常;
重写Override,运行时,方法重写(子类继承父类并重写方法)以及对象造型(父引用子类型对象),父子类的多态性表现,是动态分派,指令invokevirtual,在运行期间根据常量池中的类方法的符号引用解析到实际的类型上;
(2)两同两小一大原则:
两同:方法名相同,参数类型相同
两小:子类返回类型小于等于父类方法返回类型,
子类抛出异常小于等于父类方法抛出异常,
一大:子类访问权限大于等于父类方法访问权限。
参数列表必须完全与被重写方法的相同;
返回类型必须完全与被重写方法的返回类型相同;(备注:这条信息是标准的重写方法的规则,但是在java 1.5 版本之前返回类型必须一样,1.5(包含)j 版本之后ava放宽了限制,返回类型必须小于或者等于父类方法的返回类型 )。才有了子类返回类型小于等于父类方法返回类型。在java里面这个怎么样都是正确的,谨记。
访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。否则会有以下信息:Cannot reduce the visibility of the inherited method from Base
父类的成员方法只能被它的子类重写。
声明为final的方法不能被重写。
声明为static的方法不能被重写,但是能够被再次声明。(static和final的都不能被重写)
子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法。
重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者不能抛出比被重写方法声明的更广泛的强制性异常,反之则可以。
构造方法不能被重写。(构造方法属于static的)
如果不能继承一个方法,则不能重写这个方法。
2、方法的重载overload
(1)重载介绍
方法名一样,但参数类型和个数一样,返回值类型可以相同也可以不同,无法以返回类型作为重载函数的区分标准;
方法重载就是在类中创建多个方法,方法名相同,参数列表不同,调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法,体现多态性;
方法重载是让类以统一的方法处理不同类型数据(同方法名,不同参数列表)的一种手段;
重载overload,编译时,同一个类中,同名方法有不同参数列表(个数、类型),是一个类中多态性的表现,是静态分派,在编译期,通过静态类型而不是实际类型作为判定依据(静态方法也可以重载);
(2)构造方法的重载
【构造方法的重载】:只需要看参数即可,如果想在一个构造方法中调用另一个构造方法,那么可以使用this()的方法调用,this()括号中的参数表示目标构造方法的参数,this()必须要作为构造方法的第一跳语句,换句话说,this()之前不能有任何可执行的代码。
3、重写和重载:(多态)
底层实现(静态分派和动态分派)
重载overload,编译时,同一个类中,同名方法有不同参数列表(个数、类型),是一个类中多态性的表现,是静态分派,在编译期,通过静态类型而不是实际类型作为判定依据(静态方法也可以重载);
重写override,运行时,方法重写(子类继承父类并重写方法)以及对象造型(父引用子类型对象),父子类的多态性表现,是动态分派,指令invokevirtual,在运行期间根据常量池中的类方法的符号引用解析到实际的类型上;
多态
1.多态性是面向对象编程的一种特性,和方法无关;
2.同样的一个方法能够根据输入数据的不同,做出不同的处理,即方法的重载——有不同的参数列表(静态多态性);
3.当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法, 即在子类中重写该方法——相同参数,不同实现(动态多态性);
4、区别总结:
单词:重载overload,重写loverride;
形式:重载,方法名相同,参数列表不同(个数、类型、顺序);重写,方法名、返回值、参数列表都相同;
范围:重载,是编译时的静态分派,同一个类中;重写,运行时动态分派,发生在继承的父子类中;
权限:重载,没有权限限制;重写,子类的覆写的方法权限不能比父类的访问权限小;
四、java有哪些基本数据类型?
byte(字节) 1(8位) (bit是比特位,一个字节占8个比特位)
shot(短整型) 2(16位)
int(整型) 4(32位)
long(长整型) 8(64位)
float(浮点型) 4(32位)
double(双精度) 8(64位)
char(字符型) 2(16位)
boolean(布尔型) 1位 字节是系统分配内存的最小单位。boolean是分配一个字节,但是只有一个bit有用,其他无效置0
String是基本数据类型吗?(String不是基本数据类型)
String的长度是多少,有限制?(长度受内存大小的影响)
五、数组有没有length()方法?String有没有length()方法? 数组没有length()方法,它有length属性。
String有length()方法。
集合求长度用size()方法。
六、Java支持的数据类型有哪些?什么是自动拆装箱?
1、java支持的数据类型有哪些?
1)8种基本数据类型:
byte 8位 取值范围 -2^7 ~ 2^7 -1
short 16位 取值范围 -2^15 ~ 2^15 - 1
char 16位 取值范围 0 ~ 2 ^16 - 1
boolean 位数不明确 取值 true false
int 32位 取值范围 -2^31 ~ 2^31 - 1
long 64位 取值范围 -2^63 ~ 2^ 63 - 1
float 32位 取值范围 1.4e-45 ~ 3.40e38
double 64位 取值范围 4.9e-324 ~ 1.79e30
2)引用类型,包括类、接口、数组
需要注意的是,String不是基本数据类型,而是引用类型
引用类型声明的变量,是指该变量在内存中实际上存储的是个引用地址,创建的对象实际是在堆中
2、什么是自动拆装箱?
自动拆装箱,是指基本数据类型和引用数据类型之间的自动转换
如Integer 和 int 可以自动转换; Float和float可以自动转换
//基本类型转换成包装类型,称为装箱
Integer intObjct = new Integer(2); //装箱
//Integer intObjct = 2 //自动装箱
//自动装箱,如果一个基本类型值出现在需要对象的环境中,会自动装箱
//开箱
int a = 3 + new Integer(3); //加法需要的是数值,所以会自动开箱
Integer b = 3 + new Integer(3); //自动开箱,再自动装箱
Double x = 3.0;
//Double x = 3; //编译器不给过
//double y = 3; //而这个可以
七、int 和 Integer 有什么区别?
int是整形数字,是java的8个原始数据类型(Primitive Types,boolean,byte,short,char,int,float,double,long)之一。
Integer是int对应的包装类,他有一个int类型的字段存储数据,并且提供了基本操作,比如数学运算、int和字符串之间的转换。当在java5中引入了自动装箱,自动拆箱功能,可以根据上下文,自动进行转换,简化了相关编程。
关于Integer的值缓存,java5中另一个改进。构建Integer对象的传统方式是直接调用构造器,new一个对象。但是根据实践发现,大部分数据操作都是集中在有限的、较小的数值范围,因而,java5中新增了静态工厂方法valueOf,在调用它的时候会利用一个缓存机制(常量池),带来了明显的性能改进。这个值默认缓存为-128到127之间。
java有常量池的类型型:boolean,byte,character,short,int,long,String,不包括float和double。
何谓自动装箱、拆箱?
java平台为我们自动进行了一些转换,保证不同的写法在运行时等价,它们发生在编译阶段,也就是生成的字节码是一致的。
整数,javac替我们把自动装箱转换为Integer.valueOf(),把自动拆箱替换为Integer.intValue(),
八、Java类的实例化顺序?
父类静态成员和静态代码块 -> 子类静态成员和静态代码块 -> 父类非静态成员和非静态代码块 -> 父类构造方法 -> 子类非静态成员和非静态代码块 -> 子类构造方法
九、什么是值传递和引用传递?
值传递,是对于基本数据类型的变量而言的。传递的是该变量的一个副本,改变副本并不影响原变量
引用传递,是对于对象型变量而言的。传递的是该变量地址的一个副本,并不是该对象本身
public void add(int a) { int b = a; } 这个可以看作是值传递,a是基本数据类型,他把他的值传给了bpublic void add(Object obj) { Object objTest = obj; }这个可以看作是址传递,obj是引用数据类型,它是把栈中指向堆中的对象的地址值赋值给了objTest.这时候就同时有两个引用指向了堆中的某个Object对象 其实这样看来,java应该只有值传递的。如果是基本数据类型,传递的就是实际的值. 如果是引用数据类型,传递的就是该引用的地址值.
十、String能被继承吗?为什么?
String 类是不能被继承的,因为他是被final关键字修饰的。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
还有一点就是String类的Immutable(不可变)属性,从上面代码也可以看出,String类实际是一个char[]数组存储数据的。而这个数组也是被final关键字修饰的
下图详细列出了final关键字的用法,以及与finally 和finalize的区别
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况(是线程不安全的)
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况(一般很少)(是线程安全的)
首先说运行速度,或者说是执行速度,在这方面运行速度快慢为:StringBuilder > StringBuffer > String
十一、Java集合框架的基础接口有哪些?
Collection:代表一组对象,每一个对象都是它的子元素
Set:不包括重复元素的Collection
List:有顺序的Collection,并且可以包含重复元素
Map:可以把键(key)映射到值(value)的对象,键不能重复
十二、Java集合框架是什么?说出一些集合框架的优点?
总共有两大接口:Collection 和Map ,一个元素集合,一个是键值对集合; 其中List和Set接口继承了Collection接口,一个是有序元素集合,一个是无序元素集合; 而ArrayList和 LinkedList 实现了List接口,HashSet实现了Set接口,这几个都比较常用; HashMap 和HashTable实现了Map接口,并且HashTable是线程安全的,但是HashMap性能更好;
Java集合类里最基本的接口有:
Collection:单列集合的根接口
List:元素有序 可重复
ArrayList:类似一个长度可变的数组 。适合查询,不适合增删
LinkedList:底层是双向循环链表。适合增删,不适合查询。
Set:元素无序,不可重复
HashSet:根据对象的哈希值确定元素在集合中的位置
TreeSet: 以二叉树的方式存储元素,实现了对集合中的元素排序
Map:双列集合的根接口,用于存储具有键(key)、值(value)映射关系的元素。
HashMap:用于存储键值映射关系,不能出现重复的键key
TreeMap:用来存储键值映射关系,不能出现重复的键key,所有的键按照二叉树的方式排列
十三、HashMap 与HashTable有什么区别?
十四、ArrayList 和 LinkedList 有什么区别?
1. LinkedList和ArrayList的差别主要来自于Array和LinkedList数据结构的不同。ArrayList是基于数组实现的,LinkedList是基于双链表实现的。另外LinkedList类不仅是List接口的实现类,可以根据索引来随机访问集合中的元素,除此之外,LinkedList还实现了Deque接口,Deque接口是Queue接口的子接口,它代表一个双向队列,因此LinkedList可以作为双向对列,栈(可以参见Deque提供的接口方法)和List集合使用,功能强大。
2. 因为Array是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的,可以直接返回数组中index位置的元素,因此在随机访问集合元素上有较好的性能。Array获取数据的时间复杂度是O(1),但是要插入、删除数据却是开销很大的,因为这需要移动数组中插入位置之后的的所有元素。
3. 相对于ArrayList,LinkedList的随机访问集合元素时性能较差,因为需要在双向列表中找到要index的位置,再返回;但在插入,删除操作是更快的。因为LinkedList不像ArrayList一样,不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重新装入一个新的数组,这是ArrayList最坏的一种情况,时间复杂度是O(n),而LinkedList中插入或删除的时间复杂度仅为O(1)。ArrayList在插入数据时还需要更新索引(除了插入数组的尾部)。
4. LinkedList需要更多的内存,因为ArrayList的每个索引的位置是实际的数据,而LinkedList中的每个节点中存储的是实际的数据和前后节点的位置。
使用场景:
(1)如果应用程序对数据有较多的随机访问,ArrayList对象要优于LinkedList对象;
(2) 如果应用程序有更多的插入或者删除操作,较少的数据读取,LinkedList对象要优于ArrayList对象;
(3)不过ArrayList的插入,删除操作也不一定比LinkedList慢,如果在List靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。
十五、简单介绍Java异常框架?Error与Exception有什么区别?
Throwable
Throwable是 Java 语言中所有错误或异常的超类。
Throwable包含两个子类: Error 和 Exception。它们通常用于指示发生了异常情况。
Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。
Exception
Exception及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。
RuntimeException
RuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。
编译器不会检查RuntimeException异常。例如,除数为零时,抛出ArithmeticException异常。RuntimeException是ArithmeticException的超类。当代码发生除数为零的情况时,倘若既"没有通过throws声明抛出ArithmeticException异常",也"没有通过try…catch…处理该异常",也能通过编译。这就是我们所说的"编译器不会检查RuntimeException异常"!
如果代码会产生RuntimeException异常,则需要通过修改代码进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!
Error
和Exception一样,Error也是Throwable的子类。它用于指示合理的应用程序不应该试图捕获的严重问题,大多数这样的错误都是异常条件。
和RuntimeException一样,编译器也不会检查Error。比如说内存溢出
Java异常分类:
1 被检查的异常(Checked Exception),
2 运行时异常(RuntimeException),
3 错误(Error)。
(01) 运行时异常
定义: RuntimeException及其子类都被称为运行时异常。
特点: Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fail机制产生的ConcurrentModificationException异常等,都属于运行时异常。
虽然Java编译器不会检查运行时异常,但是我们也可以通过throws进行声明抛出,也可以通过try-catch对它进行捕获处理。
如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!
(02) 被检查的异常
定义: Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常。
特点: Java编译器会检查它。此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。
被检查异常通常都是可以恢复的。
(03) 错误
定义: Error类及其子类。
特点: 和运行时异常一样,编译器也不会对错误进行检查。
当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。例如,VirtualMachineError就属于错误。
按照Java惯例,我们是不应该是实现任何新的Error子类的!
我们通常遇到和看到基本都是运行时异常。
十六、java中的throw 和 throws关键字有什么区别?
抛出异常有三种形式
Throw throws 系统自动抛异常
十七、列举几个你了解的几个常见的运行时异常?
ArithmeticException(算术异常)
- ClassCastException (类转换异常)
- IllegalArgumentException (非法参数异常)
- IndexOutOfBoundsException (下标越界异常)
- NullPointerException (空指针异常)
- SecurityException (安全异常)
十八、final, finally, finalize有什么区别?
final 表示最终的、不可改变的。用于修饰类、方法和变量。
finally 异常处理的一部分,它只能用在try/catch语句中,表示希望finally语句块中的代码最后一定被执行(但是不一定会被执行)
finalize()是在java.lang.Object里定义的,Object的finalize方法什么都不做,对象被回收时finalized方法会被调用。
特殊情况下,可重写finalize方法,当对象被回收的时候释放一些资源。但注意,要调用super.finalize()。
1、final修饰符(关键字)。被final修饰的类,就意味着不能再派生出新的子类,不能作为父类而被子类继承。因此一个类不能既被abstract声明,又被final声明。将变量或方法声明为final,可以保证他们在使用的过程中不被修改。被声明为final的变量必须在声明时给出变量的初始值,而在以后的引用中只能读取。被final声明的方法也同样只能使用,即不能方法重写。
【例】
public class finalTest{
final int a=6;//final成员变量不能被更改
final int b;//在声明final成员变量时没有赋值,称为空白final
public finalTest(){
b=8;//在构造方法中为空白final赋值
}
int do(final x){//设置final参数,不可以修改参数x的值
return x+1;
}
void doit(){
final int i = 7;//局部变量定义为final,不可改变i的值
}
}
2、finally是在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出、捕获,finally块都会被执行。try块中的内容是在无异常时执行到结束。catch块中的内容,是在try块内容发生catch所声明的异常时,跳转到catch块中执行。finally块则是无论异常是否发生,都会执行finally块的内容,所以在代码逻辑中有需要无论发生什么都必须执行的代码,就可以放在finally块中。
3、finalize是方法名。java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者被执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
十九、描述Java内存模型?
程序的执行过程:
当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
而随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。
按照数据读取顺序和与CPU结合的紧密程度,CPU缓存可以分为一级缓存(L1),二级缓存(L3),部分高端CPU还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。
这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。
那么,在有了多级缓存之后,程序的执行就变成了:
当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。
单核CPU,多线程。进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。
多核CPU,多线程。每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。
在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。
并发编程,为了保证数据的安全,需要满足以下三个特性:
原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性即程序执行的顺序按照代码的先后顺序执行。
有没有发现,缓存一致性问题其实就是可见性问题。而处理器优化是可以导致原子性问题的。指令重排即会导致有序性问题。所以,后文将不再提起硬件层面的那些概念,而是直接使用大家熟悉的原子性、可见性和有序性。
什么是内存模型
前面提到的,缓存一致性问题、处理器器优化的指令重排问题是硬件的不断升级导致的。那么,有没有什么机制可以很好的解决上面的这些问题呢?
最简单直接的做法就是废除处理器和处理器的优化技术、废除CPU缓存,让CPU直接和主存交互。但是,这么做虽然可以保证多线程下的并发问题。但是,这就有点因噎废食了。
所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型。
为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。
内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。本文就不深入底层原理来展开介绍了,感兴趣的朋友可以自行学习。
什么是Java内存模型
前面介绍过了计算机内存模型,这是解决多线程场景下并发问题的一个重要规范。那么具体的实现是如何的呢,不同的编程语言,在实现上可能有所不同。
我们知道,Java程序是需要运行在Java虚拟机上面的,Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
Java内存模型的实现
了解Java多线程的朋友都知道,在Java中提供了一系列和并发处理相关的关键字,比如volatile、synchronized、final、concurrent包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。
在开发多线程的代码的时候,我们可以直接使用synchronized等关键字来控制并发,从来就不需要关心底层的编译器优化、缓存一致性等问题。所以,Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。
本文并不准备把所有的关键字逐一介绍其用法,因为关于各个关键字的用法,网上有很多资料。读者可以自行学习。本文还有一个重点要介绍的就是,我们前面提到,并发编程要解决原子性、有序性和一致性的问题,我们就再来看下,在Java中,分别使用什么方式来保证。
原子性
在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter和monitorexit。在synchronized的实现原理文章中,介绍过,这两个字节码,在Java中对应的关键字就是synchronized。
因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。
可见性
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。
Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。
除了volatile,Java中的synchronized和final两个关键字也可以实现可见性。只不过实现方式不同,这里不再展开了。
有序性
在Java中,可以使用synchronized和volatile来保证多线程之间操作的有序性。实现方式有所区别:
volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。
好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了,好像synchronized关键字是万能的,他可以同时满足以上三种特性,这其实也是很多人滥用synchronized的原因。
但是synchronized是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用。
二十、java中垃圾收集的方法有哪些?
标记-清除:
这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。
复制算法:
为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。
于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那份内存交Eden区,其余是两块较小的内存区叫Survior区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制复制到老年代中。(java堆又分为新生代和老年代)
标记-整理
标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。
标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。
复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。
分代收集
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。