常见面试题汇总(持续更新ing)
1.JRE和JDK的区别
-
JRE:java runtime environment java运行时环境,包含java虚拟机,java基础类库,是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的
-
JDK:JAVA Development kit 顾名思义是java开发工具包,是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的,JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序。
-
JVM 当我们运行一个程序时,JVM负责将字节代码转换为特定机器代码,JVM提供了内存管理/垃圾回收和安全机制等。这种独立于硬件和操作系统,正是java程序可以一次编写多处执行的原因。
2.object有哪些公用的方法
- 1、方法equal()测试的是两个对象是否相等
- 2、方法clone()进行对象拷贝
- 3、方法getClass()返回和当前对象相关的class对象
- 4、hashCode()返回该对象的哈希码值
- 5、finalize()当垃圾回收器确定不存在该对象的更多引用时,由对象的垃圾回收器调用此方法
- 6、toString()返回该对象的字符串表示
- 7、方法notify(),notifyall(),wait()都是用来对给定对象进行线程同步的
3、finalize()方法
finalize()是在java.lang.object里定义的,也就是说每一个对象都有这么个方法。GC在销毁对象之前,会调用finalize()方法,完了之后再销毁对象,finalize()可以使该对象重新变成可达状态,从而他不会被收回(垃圾回收算法中常用到的算法)
public void finalize(){
ft=this;
}
4、为什么object类中会有wait和notify这两个方法
Object obj = newObject();
synchronized(obj){
try{
obj.wait();
}catch(Exception e){}
obj.notify();
}
注意:wait(),nofity(),notifyAll()都必须使用在同步中,要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步,才具有锁。调用wait方法时会释放掉锁,为什么这些线程操作的方法要定义在object类中呢?
简单来说:因为synchronized中的这把锁可以是任意对象,所以任意对象可以调用wait()和notify(),所以wait和notify属于object 专业说:因为这些方法在操作同步线程时,都必须要标识他们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒,不可以对不同锁中的线程进行唤醒,也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,可以被任意对象调用的方法是定义在object类中。
5、nofity(),notifyAll()有什么区别
先说两个概念:锁池和等待池
- 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
- 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中
然后再来说notify和notifyAll的区别:
- 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
综上,所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。
有了这些理论基础,后面的notify可能会导致死锁,而notifyAll则不会的例子也就好解释了
6、final方法使用
当用在参数上时,表示的该参数对应的应用时不可变更(同时意味着,如果该应用为一个对象,该对象中的值可以修改的) 用在方法上时,表示该方法不可被重写 当用在类上面时,表示该类不能被继承
7、java关键字总结(final、static、this、super)
final关键字
- final关键字主要用在三个地方:变量,方法,类
- 对于一个final变量,如果是基本类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象
- 当用final修饰一个类时,表明这个类不能被继承,final类中所有的成员方法都会被隐式的指定为final方法
- 使用final方法的原因有两个,第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率,在早期的java版本中,会将final方法转为内嵌调用,但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能的提升(现在的java版本已经不需要使用final方法进行这些优化了),类中所有的private方法都隐式地指定为final
static关键字
static关键字主要有以下四种使用场景:
-
修饰成员变量和成员方法:被static修饰的成员变量属于类,不属于单个这个类的某个对象,被类中所有的对象共享,可以并且建议通过类名调用。被static声明的成员变量属于静态成员变量,静态变量存放在java内存区域的方法区,调用格式:类名.静态变量名,类名.静态方法名
-
静态代码块:静态代码块定义在类中方法外,静态代码块在非静态代码块之前执行(静态代码块——>非静态代码——>构造方法)该类不管创建多少对象,静态代码块只执行一次
-
静态内部类(static修饰类的话只能修饰内部类):静态内部类与非静态内部类之间存在一个最大的区别:非静态内部类在编译完成之后会隐含的保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:它的创建是不需要依赖外围类的创建,他不能使用任何外围类的非static成员变量和方法
-
静态导包(用来导入类中的静态资源,1.5之后的新特性):格式为import static这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和方法
this关键字
this关键字用于引用类的当前实例,例如:
class Manager{
Employees[] employees;
void manageEmployees() {
int totalEmp = this.employees.length;
System.out.println("Total employees: " + totalEmp);
this.report();
}
void report() { }
}
在上面示例中,this关键字用于两个地方:
- this.employee.length:访问类manage的当前实例的变量
- this.report():调用类manager的当前实例的方法
此关键字是可选的,这意味着如果上面的实例在不使用此关键字的情况下表现相同,但是,使用此关键字可能会是代码更易读或易懂
super关键字
super关键字用于从子类访问父类的变量和方法,例如:
public class Super {
protected int number;
protected showNumber() {
System.out.println("number = " + number);
}
}
public class Sub extends Super {
void bar() {
super.number = 10;
super.showNumber();
}
}
从上面的例子中,sub类访问父类成员变量number并调用其父类的super的shownumber()方法 使用this和super要注意的问题:
- 在构造器中使用super()调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this调用本类中其他构造方法时,也要放在首行
- this,super不能用在static方法中
简单解释一下:
被static修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享(有种全局变量的感觉)。而this代表对本类对象的引用,指向本类对象,而super代表对父类对象的引用,指向父类对象;所以,this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西.
8、string类为什么是final的
string有很多实用的特性,比如说“不可变性”,final的出现就是为了不想改变,不想改变的理由有两个点:设计(安全)或者效率
换言之,如果有string的引用,他的引用的一定是一个string对象,而不可能是其他类的对象,假如string没有声明为final,name你的stringchilld就可能被复写为“可变的”这样就打破了成为共识的基本约定,
[第二个回答](https://www.zhihu.com/question/31345592/answer/51535735)很好
9、关于string.valueOf()和toString()的区别
俩个方法都是将对象转换成字符串,两者有何区别呢?
看看valueOf()方法的源码,可知 valueOf()方法做了是否为null的判断,如果是null就返回null,不是就返回toString()后的字符串。
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
valueOf()做了非空判断,所以就不会报出空指针异常。
10、Override和Overload的含义以及区别
-
Overload 顾名思义是重新加载,它可以表现类的多态性,可以是函数里面可以有相同的函数名但是参数名、返回值、类型不能相同;或者说可以改变参数、类型、返回值但是函数名字依然不变。
-
就是ride(重写)的意思,在子类继承父类的时候子类中可以定义某方法与其父类有相同的名称和参数,当子类在调用这一函数时自动调用子类的方法,而父类相当于被覆盖(重写)了。
11、基本类型大小
linux系统下 在64位windows系统之下sizeof(long)=4
12、默认初始化值(这个也太简单了吧,感觉不会问)
类变量,实例变量,数组都有默认的初始化值 而局部变量没有初始化值 byte、short、int和long类型的初始化值为0; float、double类型的初始化值为0.0 Char类型的初始化值为“\u0000” boolean类型的初始化值为false; 应用类型的初始化值为null
13、数据类型转换
自动数据类型转换
自动转换按从低到高的顺序转换,不同类型的数据间的优化关系如下:
低—————————————————————————–—>高 byte,short,char->int ->long->float->double
强制数据类型转换
大转小需要强转,强转会失去精度
14、int类型转换成string类型
- 方法1
int i=0;
String s=‘’+i;
这是利用java的toString机制来做的转换,任何类型在和string相加的时候,都会先转换成为string
- 方法2
int i=0
String s=String.valueOf(i);
这是利用string类提供的方法来做的转换,同样的情况下,也使用于float,double,byte等类型向string转换的情况
- 方法3
Integer a=new Integer(1);
String s=Integer.toString(a);
15、java方法的参数到底是值传递还是引用传递
java中方法参数传递方式是 按值传递。
所谓值传递,就是将实际参数值得副本(复制品)传入方法内,而自己本身不会受到任何影响。
如果是参数传递是基本类型,传递的是基本类型的字面量值的拷贝 如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值得拷贝 参数传递基本上就是复制操作(=)
16、线程和进程的区别
进程与线程的区别
- 进程是资源分配最小单位,线程是程序执行的最小单位;
- 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,线程没有独立的地址空间,它使用相同的地址空间共享数据;
- CPU切换一个线程比切换进程花费小;
- 创建一个线程比进程开销小;
- 线程占用的资源要⽐进程少很多。
- 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)
- 多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间);
- 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换;
17、Java中实现多线程
18、Java线程的状态
java线程在某个时刻只能处于以下七个状态中的一个
- New (新创建)一个线程刚刚被创建出来,还没有调用start()方法
- runnable(可运行状态)当线程对象调用start()方法的时候,线程启动进入可运行的状态
- running(运行状态)获取了时间片,运行run()方法中的代码
- blocked(阻塞状态)表示线程阻塞于锁
- waiting(等待状态),表示线程进入等待状态,需要其他线程做出一些特定的动作
- timed_waiting(超时等待)不同于waiting,他是在指定时间内自行返回
- terminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡
19、wait()和sleep()的区别
-
1.sleep()来自thread类,wait()来自object()类;
-
调用sleep()方法的过程中,线程不会释放对象锁,而调用wait()方法线程就会释放对象锁,sleep()只是抱着锁睡觉,会释放cpu时间片,wait()把时间片和资源的锁就全都释放出去了 sleep(milliseconds)需要指定一个睡眠时间,时间一到就会自动被唤醒,wait()需要notify()唤醒
-
sleep()是当前线程进入停滞状态(阻塞当前线程),让出cpu的使用,目的就是不让当前线程独自霸占该进程所获的cpu资源,以留一定的时间给其他县城执行的机会
sleep()是thread类的static(静态)的方法,因此他不能改变对象的机制,所以当在一个synchronized块中调用sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象,即使睡着也持有对象锁 sleep()休眠时间期满后,改线程不一定会立即执行,这是因为其他线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级 wait()方法是object类中的方法,当一个线程执行到wait()方法时,他就进入到一个和该对象相关的等待池中,同时失去了释放了对象的机锁 但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出interruptedexception(但是不建议使用该方法)
20、ThreadLocal为什么保证线程私有
Thread有一个成员是ThreadLocalMap,所以每次调用threadlocal的set和get方法都是从每个线程私有的threadlocalmap中进行插入获取元素,相当于每个线程都有自己的map,虽然线程都共享threalocal这个键,但是都是从自己的map中进行获取元素,自然保证线程私有
21、单精度和双精度的区别
会损失精度,java默认是双精度的
22、hashcode()叫哈希码值,
hashcode() 叫哈希码值 是检查2个对象是否相等的必要条件, ==和equal()方法的区别 在于equal其实也是调用了== 他们的区别在于 基本数据类型比较的值,equal用于比较对象的内存存放地址,部分类库重写了equal()方法比如String Integer
23、solid(单一功能,开闭原则,里式替换,接口隔离以及依赖反转)设计原则
- 单一功能(Single Responsibility Principle):一个类只负责一项职责 。但是会出现职责扩散,就是因为某种原因,职责p被分化为更细粒度的职责p1和p2 主要是约束类,其次才是接口和方法
- 开闭原则:对扩展开放,对修改关闭 用抽象构建框架,用实现扩展细节,
- 里式替换原则:不要破坏继承体系,子类可以扩展父类的功能,但是不能改变父类原有的功能,子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法;子类可以增加自己特有的方法;当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松;当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格
- 依赖反转原则(依赖倒置原则):面向接口编程 ——底层模块都要有抽象类或者接口,或者两者都有;变量的声明类型精良是抽象类或接口;使用继承是遵循里式替换原则
- 接口隔离原则:设计接口的时候要精简单一。客户端不应该依赖他不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上,建立单一的接口,不要建立庞大臃肿的接口,尽量细节接口,对接口依赖的隔离,主要针对接口,主要针对抽象
- 迪米特法则(最少知道原则):降低耦合 一个对象应该对其他对象保持最少的了解;类于类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大;尽量降低类于类之间的耦合
24、悲观锁和乐观锁
-
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
-
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
-
两种锁的使用场景:没有哪种锁好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样就可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况下,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
- 乐观锁常用的两种实现方式:一般会用版本号机制或cas算法实现
- 版本号机制:一般是在数据表中加上一个数据版本号version字段,表示数据别修改的次数,当数据被修改时,version值会加一,当线程A更新数据值时,在读数据的同时也会读取version,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才可以更新,否则重试更新操作,直到更新成功。
- 举个例子:假设数据库中账户信息表中有个version字段,当前值为1;而当前账户余额字段balance为¥100
- 操作员A此时将其读出(version=1),并从账户余额中扣除¥50(100-50)
- 在操作员A操作的过程中,操作员B也读入此用户信息(version=1),并从其账户余额中扣除¥20(100-20)
- 操作员A完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
- 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
- 这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能
- 举个例子:假设数据库中账户信息表中有个version字段,当前值为1;而当前账户余额字段balance为¥100
- CAS算法
- 即compare and swap(比较与交换)是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫做非阻塞同步。CAS算法涉及到三个操作数:
- 需要读写的内存值 V;
- 进行比较的值A;
- 拟写入的新值 B
- 当且仅当v的值等于A时,CAS通过原子方式用新值来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)一般情况下是一个自旋操作,即不断地重试
- 即compare and swap(比较与交换)是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫做非阻塞同步。CAS算法涉及到三个操作数:
- 版本号机制:一般是在数据表中加上一个数据版本号version字段,表示数据别修改的次数,当数据被修改时,version值会加一,当线程A更新数据值时,在读数据的同时也会读取version,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才可以更新,否则重试更新操作,直到更新成功。
- 乐观锁的缺点:
- ABA问题:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 “ABA”问题。JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 循环时间长开销大:自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
- 只能保证一个共享变量的原子操作:CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。
- CAS与synchronized的使用场景
- 简单来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适合于写比较多的情况下(多写场景,冲突一般较多)
- 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
- 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
- 补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
25.使用同步解决线程问题带来的弊端有哪些?
- 使用同步后方法的效率低下
- 如果出现同步嵌套,容易出现死锁
第一个问题出现告诉我们要慎用同步,只有在必要的位置添加同步方法,而不是为了安全添加不必要的同步,这样会导致效率的降低,如果使用了同步会有锁机制,执行相关代码前会对锁进行相应的检查,这样就会导致效率的降低
26.产生死锁怎么办?
什么是死锁:由于系统存在一些不可剥夺资源,而当两个或两个以上的进程占有自身资源,并请求对方资源时,会导致每个进程都无法前进,这就是死锁 产生死锁的原因:(1)因为资源不足 (2)进程运行推行的顺序不合适 (3)资源分配不当
首先回答死锁产生的必要条件是:
- 互斥条件:要求分配的资源师排他性的,即最多只能同时给一个进程使用
- 不可剥夺条件:是指进程在使用资源完毕之前,资源不能被强行夺走
- 请求与保持条件:请求保持条件是指进程占有自身本来拥有的资源并要求其他资源
- 循环等待条件:循环等待条件是指存在一种进程资源的循环等待链
这是死锁产生的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁
预防死锁:
- 避免死锁产生的四个必要条件,在使用前进行判断,只允许不会产生死锁的进程申请资源
- 两种避免方法:
- 如果一个进程的请求会导致死锁,则不启动该进程
- 如果一个进程的增加资源请求会导致死锁,则拒绝申请
- 避免死锁通常使用银行家算法:
JAVA中有几种数组复制的方法
有四种:
- for 循环逐一复制 循环灵活但是代码不够简洁
- system.arraycopy 【效率最高】
Java编程思想中有提到过,“java标准类库提供有static方法,system.arraycopy(),用它复制数组比用for循环复制快得多”
源码中是native方法:native关键字说明其修饰方法:native关键字说明其修饰的方法是一个原生态方法,方法对应的现实不是在当前文件,而是再用其他语言(c或者c++)实现的文件中,可以将native方法比作java程序同吃程序的接口
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,int length);
- Array.copyof
public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
从源码中可以看出,本质上是调用了arraycopy方法,所以其效率就是比不上arraycopy的
- 使用clone方法 clone方法返回的是object[],需要强制转换,一般用clone效率是最差的,
integer的缓存范围是多少?
默认缓存范围是-128~127.
int和integer的基本使用对比
- 1、Integer是int的包装类;int是基本数据类型
- 2、integer变量必须实例化后才能使用,int变量不需要
- 3、integer实际是对象的引用,指向此new的integer对象;int是直接存储数据值
- 4、integer的默认值是null;int的默认值是0;
integer和int的深入对比
1、由于integer变量实际上是对一个integer对象的引用,所以两个通过new生成的integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)
Integer i=new Integer(100);
Integer j=new Integer(100);
system.out.print(i==j);//false
2、integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true(因为包装类integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
Integer i=new Integer(100);
int j=100;
system.out.print(i==j);//true
4、对于两个非new生成的integer对象,进行比较的时候,如果变量区间在-128到127之间,则比较结果为true,如果两个变量的值不在此区间中,则比较结果为false
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false
对于第4条的原因:java在编译Integer i = 100 时,会翻译成为Integer i = Integer.valueOf(100)。而java API中对Integer类型的valueOf的定义如下,对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了。
public static Integer valueOf(int i){
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high){
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}
对象的生命周期
- 1、创建阶段(created)
- 2、应用阶段 (in use)
- 3、不可见阶段(invisible)
- 4、不可达阶段(unreachable)
- 5、收集阶段(collected)
- 6、终结阶段(finalized)
- 7、对象空间重分配阶段(de-allocated)
1.创建阶段(Created) 在创建阶段系统通过下面的几个步骤来完成对象的创建过程
- 为对象分配存储空间
- 开始构造对象
- 从超类到子类对static成员进行初始化
- 超类成员变量按顺序初始化,递归调用超类的构造方法
- 子类成员变量按顺序初始化,子类构造方法调用 一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段
2.应用阶段(In Use) 对象至少被一个强引用持有着。
3.不可见阶段(Invisible) 当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。
简单说就是程序的执行已经超出了该对象的作用域了。
举例如下图:本地变量count在25行时已经超出了其作用域,则在此时称之为count处于不可视阶段。当然这种情况编译器在编译的过程中会直接报错了。
图2. 不可见阶段示例
4.不可达阶段(Unreachable) 对象处于不可达阶段是指该对象不再被任何强引用所持有。
与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为”GC root”。存在着这些GC root会导致对象的内存泄露情况,无法被回收。
5.收集阶段(Collected) 当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了finalize()方法,则会去执行该方法的终端操作。
这里要特别说明一下:不要重载finazlie()方法!原因有两点:
-
会影响JVM的对象分配与回收速度 在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗CPU时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC。
-
可能造成该对象的再次“复活” 在finalize()方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又重新变为“应用阶段”。这个已经破坏了Java对象的生命周期进程,且“复活”的对象不利用后续的代码管理。
6.终结阶段 当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
7.对象空间重新分配阶段 垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。