title: Java面试题(一)
date: 2021-6-10
updated: 2021-6-12
tags:
- Java
- JVM
categories: - 面试
- Java面试题(一)
什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
答案一:
Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。
Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
答案二:
Java 的跨平台不是Java 源程序的跨平台 ,如果是这样,那么所以语言都是跨平台的, Java 源程序先经过javac 编译器编译成二进制的 .class 字节码文件(Java的跨平台指的就是 .class 字节码文件的跨平台,.class 字节码文件是与平台无关的),.class 文件再运行在 jvm上,java解释器(jvm的一部分)会将其解释成对应平台的机器码执行,所以 java 所谓的跨平台就是在不同平台上安装了不同的 jvm,而在不同平台上生成的 .class 文件都是一样的,而 .class 文件再由对应平台的 jvm 解释成对应平台的机器码执行。
机器码和字节码的区别:
机器码,完全依附硬件而存在,并且不同硬件由于内嵌指令集不同,即使相同的0 1代码 意思也可能是不同的,换句话说,根本不存在跨平台性,比如:不同型号的CPU,你给他个指令10001101,他们可能会解析为不同的结果。
我们知道JAVA是跨平台的,为什么呢?因为他有一个 jvm,不论哪种硬件,只要你装有 jvm,那么他就认识这个JAVA字节码,至于底层的机器码,咱不用管,有jvm搞定,他会把字节码再翻译成所在机器认识的机器码。
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执 行的二进制机器码---->程序运行。
JDK 和 JRE 的区别是什么?
JRE: Java Runtime Environment
JDK:Java Development Kit
JRE 顾名思义是 java 运行时环境,包含了java虚拟机,java基础类库。是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的。
JDK 顾名思义是java开发工具包,是程序员使用 java 语言编写 java 程序所需的开发工具包,是提供给程序员使用的。JDK 包含了JRE,同时还包含了编译j ava 源码的编译器javac,还包含了很多java程序调试和分析的工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序。
如果你需要运行java程序,只需安装JRE就可以了。如果你需要编写java程序,需要安装JDK。
JRE根据不同操作系统(如:windows,linux等)和不同JRE提供商(IBM,ORACLE等)有很多版本
“static”关键字是什么意思?Java 中是否可以覆盖(override)一个 private 或者是 static 的方法?
答案一:
“static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。
Java 中 static 方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而 static 方法是编译时静态绑定的。static 方法跟类的任何实例都不相关,所以概念上不适用。
Java 中也不可以覆盖 private 的方法,因为 private 修饰的变量和方法只能在当前类中使用,如果是其他的类继承当前类是不能访问到private变量或方法的,当然也不能覆盖。
例子:
class Super{
static String greeting(){
return "Good night";
}
String name(){
return "Richard";
}
}
class Sub extends Super{
static String greeting(){
return "Hello";
}
String name(){
return "Dick";
}
}
class Test{
public static void main(String[] args){
Super s = new Sub();
System.out.println(s.greeting()+","+s.name());
}
}
// 运行结果:Good night,Dick
// 这个例子说明“实例方法被覆盖,静态方法被隐藏”
是否可以在 static 环境中访问非 static 变量?
答案一:
static 变量在 Java 中是属于类的,它在所有的实例中的值是一样的。当类被 Java 虚拟机载入的时候,会对static 变量进行初始化。如果你的代码尝试不用实例来访问非 static 的变量,编译器会报错,因为这些变量还没有被创建出来,还没有跟任何实例关联上。
答案二:
因为静态的成员属于类,随着类的加载而加载到静态方法区内存,当类加载时,此时不一定有实例创建,没有实例,就不可以访问非静态的成员。
Java支持的数据类型有哪些?什么是自动拆装箱?
答案一:
Java语言支持的 8 种基本数据类型是(六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。):
数据类型 | byte | short | Int | long | float | double | boolean | char | void |
---|---|---|---|---|---|---|---|---|---|
二进制位数 | 8 | 16 | 32 | 64 | 32 | 64 | 1 | 16 | – |
字节 | 1 | 2 | 4 | 8 | 4 | 8 | 1/8 | 2 | – |
封装器类 | Byte | Short | Integer | Long | Float | Double | Boolean | Character | Void |
基本数据类型:
- **整数值型:**byte,short,int,long
- **浮点类型:**float,double
- **布尔型:**boolean
- **字符型:**char
整数默认 int 型,小数默认是 double 型。float 和 long 类型的必须加后缀。
首先知道 String 是引用类型不是基本类型,引用类型声明的变量是指该变量在内存中实际存储的是一个引用地址,实体在堆中。引用类型包括类、接口、数组等。String类还是 final 修饰的。
包装类就属于引用类型,自动装箱和拆箱就是基本类型和引用类型之间的转换,至于为什么要转换,因为基本类型转换为引用类型后,就可以 new 对象,从而调用包装类中封装好的方法进行基本类型之间的转换或者toString(当然用类名直接调用也可以,便于一眼看出该方法是静态的),还有就是如果集合中想存放基本类型,泛型的限定类型只能是对应的包装类型。
答案二:
另一种是引用类型:如 String 等,其实是对象的引用,JVM 中虚拟栈中存的是对象的地址,创建的对象实质在堆中,通过地址来找到堆中的对象的过程,即为引用类型。
自动装箱就是 Java 编译器在基本数据类型和对应的对象包装类型间的转化,即 int 转化为 Integer,自动拆箱是 Integer 调用其方法将其转化为 int 的过程
Java 中的方法覆盖( Overriding )和方法重载( Overload )是什么意思?
答案一:
Java 中的方法重载发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况。与此相对,方法覆盖是说子类重新定义了父类的方法。方法覆盖必须有相同的方法名、参数列表和返回类型。覆盖者可能不会限制它所覆盖的方法的访问。
答案二:
overload 多个加载,所以在一个类中进行加载,方法名相同,参数个数或类型不同,与返回值无关。
override 覆盖优先执行,指的是子类重写父类的方法,方法名、返回类型和参数必须相同。除了访问权限 >= 父类,抛出异常必须 <= 父类(或者不抛异常)。(特殊点,如果子类重写的方法所抛异常小于父类,那么就不能调用父类的方法,因为子类的异常小,捕获不了 ), private 修饰的父类方法不能被重写 。
答案三:
方法重写的原则:
- 重写方法的方法名称、参数列表必须与原方法的相同,返回类型可以相同也可以是原类型的子类型(从Java SE5开始支持)。
- 重写方法不能比原方法访问性差(即访问权限不允许缩小)。
- 重写方法不能比原方法抛出更多的异常。
- 被重写的方法不能是 final 类型,因为 final 修饰的方法是无法重写的。
- 被重写的方法不能为 private,否则在其子类中只是新定义了一个方法,并没有对其进行重写。
- 被重写的方法不能为 static。如果父类中的方法为静态的,而子类中的方法不是静态的,但是两个方法除了这一点外其他都满足重写条件,那么会发生编译错误;反之亦然。即使父类和子类中的方法都是静态的,并且满足重写条件,但是仍然不会发生重写,因为静态方法是在编译的时候把静态方法和类的引用类型进行匹配。
- 重写是发生在运行时的,因为编译期编译器不知道并且没办法确定该去调用哪个方法,JVM会在代码运行的时候作出决定。
方法重载的原则:
- 方法名称必须相同。
- 参数列表必须不同(个数不同、或类型不同、参数类型排列顺序不同等)。
- 方法的返回类型可以相同也可以不相同。
- 仅仅返回类型不同不足以成为方法的重载。
- 重载是发生在编译时的,因为编译器可以根据参数的类型来选择使用哪个方法。
重写和重载的不同:
- 方法重写要求参数列表必须一致,而方法重载要求参数列表必须不一致。
- 方法重写要求返回类型必须一致(或为其子类型),方法重载对此没有要求。
- 方法重写只能用于子类重写父类的方法,方法重载用于同一个类中的所有方法。
- 方法重写对方法的访问权限和抛出的异常有特殊的要求,而方法重载在这方面没有任何限制。
- 父类的一个方法只能被子类重写一次,而一个方法可以在所有的类中可以被重载多次。
- 重载是编译时多态,重写是运行时多态。
Java 中,什么是构造方法?什么是构造方法重载?什么是复制构造方法?
当新对象被创建的时候,构造方法会被调用。每一个类都有构造方法。在程序员没有给类提供构造方法的情况下,Java 编译器会为这个类创建一个默认的构造方法。
Java 中构造方法重载和方法重载很相似。可以为一个类创建多个构造方法。每一个构造方法必须有它自己唯一的参数列表。
Java 不支持像 C++ 中那样的复制构造方法,这个不同点是因为如果你不自己写构造方法的情况下,Java 不会创建默认的复制构造方法。
Java 支持多继承么?
Java 中类不支持多继承,只支持单继承(即一个类只有一个父类)。 但是 Java 中的接口支持多继承,即一个子接口可以有多个父接口。(接口的作用是用来扩展对象的功能,一个子接口继承多个父接口,说明子接口扩展了多个功能,当类实现接口时,类就扩展了相应的功能)。
接口和抽象类的区别是什么?
Java 提供和支持创建抽象类和接口。它们的实现有共同点,不同点在于:
- 接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
- 类可以实现很多个接口,但是只能继承一个抽象类。
- 类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。
- 抽象类可以在不提供接口方法实现的情况下实现接口。
- Java 接口中声明的变量默认都是 final 的。抽象类可以包含非 final 的变量。
- Java 接口中的成员函数默认是 public 的。抽象类的成员函数可以是 private,protected 或者是 public。
- 接口是绝对抽象的,不可以被实例化,抽象类也不可以被实例化。
从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
在 Java 中为什么很多人说有值传递和引用传递?引用传递的本质是什么?
首先,不要纠结于 Pass By Value 和 Pass By Reference 的字面上的意义,否则很容易陷入所谓的“一切传引用其实本质上是传值”这种并不能解决问题无意义论战中。
更何况,要想知道 Java 到底是传值还是传引用,起码你要先知道传值和传引用的准确含义吧?可是如果你已经知道了这两个名字的准确含义,那么你自己就能判断 Java 到底是传值还是传引用。
这就好像用大学的名词来解释高中的题目,对于初学者根本没有任何意义。
1. 搞清楚 基本类型 和 引用类型的不同之处
int num = 10;
String str = "hello";
如图所示,num 是基本类型,值就直接保存在变量中。而 str 是引用类型,变量中保存的只是实际对象的地址。一般称这种变量为"引用",引用指向实际对象,实际对象中保存着内容。
2. 搞清楚赋值运算符(=)的作用
num = 20;
str = "java";
对于基本类型 num ,赋值运算符会直接改变变量的值,原来的值被覆盖掉。
对于引用类型 str,赋值运算符会改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变(重要)。
如上图所示,“hello” 字符串对象没有被改变。(没有被任何引用所指向的对象是垃圾,会被垃圾回收器回收)
3. 调用方法时发生了什么?参数传递基本上就是赋值操作。
第一个例子:基本类型
void foo(int value) {
value = 100;
}
foo(num); // num 没有被改变
第二个例子:没有提供改变自身方法的引用类型
void foo(String text) {
text = "windows";
}
foo(str); // str 也没有被改变
第三个例子:提供了改变自身方法的引用类型
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
builder.append("4");
}
foo(sb); // sb 被改变了,变成了"iphone4"。
第四个例子:提供了改变自身方法的引用类型,但是不使用,而是使用赋值运算符。
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
builder = new StringBuilder("ipad");
}
foo(sb); // sb 没有被改变,还是 "iphone"。
重点理解为什么,第三个例子和第四个例子结果不同?
下面是第三个例子的图解:
builder.append("4")
之后
下面是第四个例子的图解:
builder = new StringBuilder("ipad");
之后
进程和线程的区别是什么?
答案一:
进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。线程又叫做轻量级进程。
线程与进程的区别归纳:
**a. 地址空间和其它资源:**进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
**b. 通信:**进程间通信 IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
**c. 调度和切换:**线程上下文切换比进程上下文切换要快得多。
d. 在多线程 OS 中,进程不是一个可执行的实体。
答案二:
- 进程是运行中的程序,线程是进程的内部的一个执行序列
- 进程是资源分配的单元,线程是执行行单元
- 进程间切换代价大,线程间切换代价小
- 进程拥有资源多,线程拥有资源少
- 多个线程共享进程的资源
开个QQ,开了一个进程;开了迅雷,开了一个进程。
在QQ的这个进程里,传输文字开一个线程、传输语音开了一个线程、弹出对话框又开了一个线程。
所以运行某个软件,相当于开了一个进程。在这个软件运行的过程里(在这个进程里),多个工作支撑的完成 QQ 的运行,那么这“多个工作”分别有一个线程。
所以一个进程管着多个线程。
通俗的讲:“进程是爹妈,管着众多的线程儿子”…
创建线程有几种不同的方式?你喜欢哪一种?为什么?
答案一:
有4种方式可以用来创建线程:
- 继承 Thread 类
// 从 Thread 派生一个自定义类,然后覆写 run() 方法:
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}
- 实现 Runnable 接口
// 创建 Thread 实例时,传入一个 Runnable 实例:
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
- 使用实现了 Executor 接口的 ThreadPoolExecutor 来创建线程池。
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 核心线程数
taskExecutor.setCorePoolSize(5);
// 最大线程数
taskExecutor.setMaxPoolSize(15);
// 队列大小 默认使用LinkedBlockingQueue
taskExecutor.setQueueCapacity(100);
// 线程最大空闲时间
taskExecutor.setKeepAliveSeconds(300);
// 拒绝策略 默认new ThreadPoolExecutor.AbortPolicy()
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 线程名称前缀
taskExecutor.setThreadNamePrefix("My-Task-Executor-");
//交给spring托管的会自动初始化,因为实现了InitializingBean接口
//taskExecutor.initialize();
return taskExecutor;
}
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
public void sendMsg() {
threadPoolTaskExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("发送消息");
}
});
}
- 实现 callable 接口,重写 call 方法,有返回值。
实现 Runnable 接口这种方式更受欢迎,因为这不需要继承 Thread 类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而 Java 不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。
答案二:
①继承 Thread 类(真正意义上的线程类),是 Runnable 接口的实现。
②实现 Runnable 接口,并重写里面的 run 方法。
③使用 Executor 框架创建线程池。Executor 框架是 juc 里提供的线程池的实现。调用线程的 start():启动此线程;调用相应的 run() 方法
继承于 Thread 类的线程类,可以直接调用start方法启动线程(使用 static 也可以实现资源共享),一个线程(对象)只能够执行一次 start(),而且不能通过 Thread 实现类对象的 run() 去启动一个线程。
实现 Runnable 接口的类需要再次用 Thread 类包装后才能调用 start 方法。(三个 Thread 对象包装一个类对象,就实现了资源共享)。
线程的使用的话,注意锁和同步的使用。(多线程访问共享资源容易出现线程安全问题)
一般情况下,常见的是第二种。
Runnable接口有如下好处:
- 避免点继承的局限,一个类可以继承多个接口。
- 适合于资源的共享
Thread 的常用方法:
start()
:启动线程并执行相应的 run() 方法。run()
:子线程要执行的代码放入 run() 方法中。currentThread()
:静态的,调取当前的线程。getName()
:获取此线程的名字。setName()
:设置此线程的名字。yield()
:调用此方法的线程释放当前 CPU 的执行权(很可能自己再次抢到资源)。join()
:在 A 线程中调用 B 线程的 join() 方法,表示:当执行到此方法,A 线程停止执行,直至 B 线程执行完毕,A 线程再接着 join() 之后的代码执行。isAlive()
:判断当前线程是否还存活。sleep(long l)
:显式的让当前线程睡眠l毫秒 (只能捕获异常,因为父类 run 方法没有抛异常)。- 线程通信(方法在Object类中):
wait()、notify()、notifyAll()
wait()
:当一个线程调用了 object.wait(),那么它就会进入 object 对象的等待队列。这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。notify()
:当 object 对象调用了obj.notify() 方法,就会从这个等待队列,随机选择一个线程将其唤醒。这里的选择是不公平的完全是随机的。notifyAll()
:它与 notify() 方法的功能基本一致,不同的是它在唤醒这个等待队列中的所有等待线程而不是随机选择一个
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notify(); // 唤醒在this锁等待的线程
}
public synchronized String getTask() {
while (queue.isEmpty()) {
// 释放this锁:
this.wait();
// 重新获取this锁
}
return queue.remove();
}
}
设置线程的优先级(非绝对,只是相对几率大些)
getPriority()
:返回线程优先值
setPriority(int newPriority)
:改变线程的优先级
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
就绪状态:
当线程对象调用了**start()**方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- **等待阻塞:**运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- **同步阻塞:**线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- **其他阻塞:**通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当 sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
概括的解释下线程的几种可用状态
-
**新建( new ):**新创建了一个线程对象。
-
**可运行( runnable ):**线程对象创建后,其他线程(比如 main 线程)调用了该对象 的
start()
方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权 。 -
**运行( running ):**可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) ,执行程序代码。
-
**阻塞( block ):**阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有 机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种:
- **等待阻塞:**运行( running)的线程执行
o.wait()
方法, JVM 会把该线程放入等待队列( waitting queue )中。 - **同步阻塞:**运行(running)的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。
- 其他阻塞: 运行(running)的线程执行
Thread.sleep(long ms)
或t.join()
方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。当sleep()
状态超时、join()
等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。
- **等待阻塞:**运行( running)的线程执行
-
**死亡( dead ):**线程
run()
、main()
方法执行结束,或者因异常退出了run()
方法,则该线程结束生命周期。死亡的线程不可再次复生。
操作系统线程的 5 种状态:
- 新建状态:
线程创建但没有启动- 就绪状态:
线程处于可运行的状态,当线程获得CPU的时间片后会被执行,时间片耗尽或主动放弃当次时间片( yield 方法)时再次进入就绪状态。- 运行状态:
线程得到时间片被 CPU 执行- 阻塞状态:
线程放弃 CPU 的时间片(一直到某个条件达成),主动进入阻塞的状态。
- 同步阻塞:线程由于尝试获得对象的同步锁但无法取得时,进入锁池,等待其他线程释放该对象的锁。
- 等待阻塞:线程主动放弃对对象上的锁的占用,进入等待对象通知的队列。指 wait 方法
- 其他阻塞:线程主动进入休眠状态,等待条件达成。指sleep、join方法或I/O请求。
- 终止状态:
线程任务结束或异常退出Java 线程定义的6种状态:
- 新建状态NEW:
线程创建,但没有启动- 可运行状态(就绪状态)RUNNABLE: 代表线程正在运行或者不处于阻塞、等待状态的可以被运行的状态。线程创建后或脱离阻塞、等待状态后进入可运行状态。
- 阻塞状态BLOCKED:
代表线程尝试获得一个锁时无法对锁占用,进入阻塞状态;当该线程能够获得锁时脱离阻塞状态。- 等待状态WAITING:
等待线程主动进入等待条件达成的状态,可以使用join、wait、sleep方法。- 计时等待状态TIMED_WAITING:
等待状态添加计时器,当等待条件达成或计时器耗尽时脱离等待状态。- 中断状态(终止状态)TERMINATED:
线程任务结束或手动设置中断标记
可以用早起坐地铁来比喻这个过程:
还没起床:sleeping
起床收拾好了,随时可以坐地铁出发:Runnable
等地铁来:Waiting
地铁来了,但要排队上地铁:I/O阻塞
上了地铁,发现暂时没座位:synchronized阻塞
地铁上找到座位:Running
到达目的地:Dead
同步方法和同步代码块的区别是什么?
答案一:
同步方法默认用 this 或者当前类 class 对象作为锁;
同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法;
同步方法使用关键字 synchronized 修饰方法,而同步代码块主要是修饰需要进行同步的代码,用 synchronized(object){} 代码内容}进行修饰;
答案二:
Java 中每个对象都有一把锁, 线程可以通过 synchronized 关键字来获取对象上的锁
- 同步方法(粗粒度锁):
- 修饰一般方法:
public synchronized void method (){...}
, 获取的是当前调用对象 this 上的锁 - 修饰静态方法:
public static synchronized void method (){...}
, 获取当前类的 字节码对象上的锁(此时如果调用该静态方法,将会锁住整个类)
- 修饰一般方法:
- 同步代码块(细粒度锁):
synchronized (obj) {...}
, 同步代码块可以指定获取哪个对象上的锁,obj 任意
在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?
答案一:
监视器和锁在 Java 虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
答案二:
在 java 虚拟机中,每个对象(Object 和 class )通过某种逻辑关联监视器,每个监视器和一个对象引用相关联,为了实现监视器的互斥功能,每个对象都关联着一把锁。
一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码
另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案
答案三:
JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。本质都是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由 synchronized 所保护对象的监视器
代码块同步是使用 monitorenter 和 monitorexit 指令实现的。monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处,JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之配对。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。
什么是死锁(deadlock)?
死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
造成的原因:
- 因为系统资源不足。
- 进程运行推进顺序不合适。
- 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
死锁的必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁的解除与预防:
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。
// 当线程1和2都已经成功获取到第一个锁时,死锁就发生了。
//线程1
Synchorized(objectA){
Synchorized(objectB){
//操作
}
}
//线程2
Synchorized(objectB){
Synchorized(objectA){
//操作
}
}
如何确保 N 个线程可以访问 N 个资源同时又不导致死锁?
答案一:
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。
多线程产生死锁需要四个条件,分别是互斥性,保持和请求,不可剥夺性还有要形成闭环,这四个条件缺一不可,只要破坏了其中一个条件就可以破坏死锁,其中最简单的方法就是线程都是以同样的顺序加锁和释放锁,也就是破坏了第四个条件。
答案二:
预防死锁,预先破坏产生死锁的四个条件。互斥不可能破坏,所以有如下三种方法:
-
破坏请求和保持条件
- 进程必须等所有要请求的资源都空闲时才能申请资源,这种方法使资源浪费严重(有些资源可能仅在运行初期或结束时才使用,甚至根本不使用).
- 允许进程获取初期所需资源后,便开始运行,运行过程中再逐步释放自己占有的资源,比如有一个进程的任务是把数据复制到磁盘中再打印,前期只需获得磁盘资源而不需要获得打印机资源,待复制完毕后再释放掉磁盘资源。这种方法比第一种方法好,会使资源利用率上升。
-
破坏不可抢占条件
这种方法代价大,实现复杂。
-
破坏循坏等待条件
对各进程请求资源的顺序做一个规定,避免相互等待。这种方法对资源的利用率比前两种都高,但是前期要为设备指定序号,新设备加入会有一个问题,其次对用户编程也有限制。
答案三:
破坏造成死锁的必要条件。线程互斥基本无法避免。
- 指定加锁顺序:所有线程按照相同的顺序获得资源的锁,不先获得顺序靠前的锁,无法获得后续的锁。则先获得锁的线程不会请求后获得锁的线程占用的资源,因为后获得锁的线程还没能获得先获得锁的线程未释放的锁,更无法占用先获得锁的线程还没获得的顺序靠后的锁。缺点:需要手动对锁的获得顺序进行分析。
- **指定加锁时限:**线程指定超时时间,若无法获得锁的占用权,进行回退操作,并释放已占用的锁,经一段延时后再尝试进行任务。缺点 :线程过多的话,可能造成频繁回退,运行效率不高。
- **死锁检测:**将线程和已获得锁的情况记录下来,定时检测是否所有死锁现象(线程循环等待现象),回退处于死锁状态的线程,延时后,重试这些线程,与添加加锁时限类似,缺点也同。
Java 集合类框架的基本接口有哪些?
答案一:
集合类接口指定了一组叫做元素的对象。集合类接口的每一种具体的实现类都可以选择以它自己的方式对元素进行保存和排序。有的集合类允许重复的键,有些不允许。
Java 集合类提供了一套设计良好的支持对一组对象进行操作的接口和类。Java 集合类里面最基本的接口有:
**Collection:**代表一组对象,每一个对象都是它的子元素。
**Set:**不包含重复元素的 Collection。
**List:**有顺序的 collection,并且可以包含重复元素。
**Map:**可以把键(key)映射到值(value)的对象,键不能重复。
答案二:
总共有两大接口:Collection 和 Map ,一个元素集合,一个是键值对集合;其中 List 和 Set 接口继承了Collection 接口,一个是有序元素集合,一个是无序元素集合;而 ArrayList 和 LinkedList 实现了List接口,HashSet 实现了Set接口,这几个都比较常用;HashMap 和 HashTable 实现了Map接口,并且 HashTable 是线程安全的,但是 HashMap 性能更好;
答案三:
从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。
集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:
- **接口:**是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象
- **实现(类):**是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
- **算法:**是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。
除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。
equals 和 == 的区别
初步了解 JVM 中的内存分配
在 JVM 中,内存分为堆内存跟栈内存。他们二者的区别是: 当我们创建一个对象(new Object)时,就会调用对象的构造函数来开辟空间,将对象数据存储到堆内存中,与此同时在栈内存中生成对应的引用,当我们在后续代码中调用的时候用的都是栈内存中的引用。还需注意的一点,基本数据类型是存储在栈内存中。
初步认识 equals 与 == 的区别:
- == 是判断两个变量或实例是不是指向同一个内存空间,equals 是判断两个变量或实例所指向的内存空间的值是不是相同(需要重写 Object 方法的 equals 方法)
- == 是指对内存地址进行比较 , equals() 是对字符串的内容进行比较
- == 指引用是否相同, equals() 指的是值是否相同
equals 与 == 的区别详解:
== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。equals 用来比较的是两个对象的内容是否相等,由于所有的类都是继承自 java.lang.Object 类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是 Object 类中的方法,而 Object 中的 equals 方法返回的却是 == 的判断。
String s=“abcd” 是一种非常特殊的形式,和 new 有本质的区别。它是 Java 中唯一不需要 new 就可以产生对象的途径。以 String s=“abcd”;形式赋值在 Java 中叫直接量;它是在常量池中而不是象 new 一样放在压缩堆中。 这种形式的字符串,在 JVM 内部发生字符串拘留,即当声明这样的一个字符串后,JVM会在常量池中先查找有有没有一个值为"abcd"的对象,如果有,就会把它赋给当前引用,即原来那个引用和现在这个引用指点向了同一对象;如果没有,则在常量池中新创建一个"abcd",下一次如果有 String s1 = “abcd”;又会将 s1 指向"abcd"这个对象,即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象。
String a = “abc”; 和 String a = new String(“abc”); 的区别:
而 String s = new String(“abcd”); 和其它任何对象一样。每调用一次就产生一个对象,只要它们调用。
也可以这么理解:String str = “hello”; 先在内存中找是不是有"hello"这个对象,如果有,就让 str 指向那个"hello".如果内存里没有"hello",就创建一个新的对象保存"hello"。String str=new String (“hello”) 就是不管内存里是不是已经有"hello"这个对象,都新建一个对象保存"hello"。
为什么集合类没有实现 Cloneable 和 Serializable 接口?
答案一:
克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由集合类的具体实现来决定如何被克隆或者是序列化。
实现 Serializable 序列化的作用
- 将对象的状态保存在存储媒体中以便可以在以后重新创建出完全相同的副本;
- 按值将对象从一个从一个应用程序域发向另一个应用程序域。
实现 Serializable 接口的作用就是可以把对象存到字节流,然后可以恢复。所以你想如果你的对象没有序列化,怎么才能进行网络传输呢?要网络传输就得转为字节流,所以在分布式应用中,你就得实现序列化。如果你不需要分布式应用,那就没必要实现实现序列化。
答案二:
Cloneable 标识一个类可以被克隆,Serializable 标识一个类可以被序列化
集合的接口没有实现这两个接口
但是集合具体的类是有实现这两个接口的
接口不是具体的容器,所以不需要实现这两个接口,也没有意义