首页 > 编程语言 >Java面试题

Java面试题

时间:2022-12-19 09:35:06浏览次数:52  
标签:面试题 Java 对象 线程 内存 方法 Class

Java基础

Java语言具有哪些特点?

1.Java为纯面向对象语言。(

所有的静态内容( static 关键修饰的变量和方法)不属于任何对象?

JVM 在创建对象的时候,实际上会创建两个对象:

  • 一个是实例对象。
  • 另一个是Class 对象。该 Class 对象在JVM内仅仅会装载一次,该类的静态方法和静态属性也一同装载,JVM使用该 Class 对象来创建具体的实例对象(如上面的对象)。

所有基本类型(char,boolean,byte,short,int,long,float,double)都不是对象?

Java 官方为每一个原始类型推出了对应的包装类(比如:Integer 对应 int,Long 对应 long,Character 对应 char),所以,其实现在我们可以为原始类型创建一个包装对象,同时对它们做对象相关的操作。并且,由于自动拆装箱,我们可以把一个原始类型值赋值给它对应的包装类的引用。

2.具有平台无关性。Java利用虚拟机运行字节码,无论在Windows、Linux还是MacOS等其他平台对Java程序进行编译,编译后的程序可在其他平台运行。Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。

 

 

 

3.Java为解释性语言,编译器把Java代码编译成平台无关的中间代码,然后在JVM上解释运行,具有很好的可移植性。

编译:将源代码一次性转换成目标代码的过程

解释:将源代码逐条转换成目标代码同时逐条运行的过程。

Java和其他的语言不太一样。因为java针对不同的平台有不同的JVM,实现了跨平台。所以Java语言有一次编译到处运行的说法。

(1)可以说它是编译型的:每个java文件都需要编译成.class字节码文件,不编译就什么用都没有。
(2)可以说它是解释型的:java代码编译完不能直接运行,它是解释运行在JVM上的,所以它是解释运行的。

  • 编译型 :编译型语言会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
  • 解释型 :解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。

4.Java提供了很多内置的类库,通过这些类库,简化了开发人员的程序设计工作,同时缩短了项目的开发时间,例如,Java语言提供了对多线程的支持,提供了对网络通信的支持,最主要的是提供了垃圾回收器,这使得开发人员从内存的管理中解脱出来。

5.Java具有较好的安全性和健壮性。Java语言经常被用在网络环境中,为了增强程序的安全性,Java语言提供了一个防止恶意代码攻击的安全机制(数组边界检测和字节码校验等)。Java的强类型机制、垃圾回收器、异常处理和安全检查机制使得用Java语言编写的程序具有很好的健壮性。

6.Java语言提供了对Web应用开发的支持。例如,Applet、Servlet和JSP可以用来开发Web应用程序;Socket、RMI可以用来开发分布式应用程序。

AOT与JIT

JIT,即Just-in-time,动态(即时)编译,边运行边编译;AOT,Ahead Of Time,指运行前编译,是两种程序的编译方式

区别
这两种编译方式的主要区别在于是否在“运行时”进行编译

JIT优点:
可以根据当前硬件情况实时编译生成最优机器指令(ps. AOT也可以做到,在用户使用是使用字节码根据机器情况在做一次编译)
可以根据当前程序的运行情况生成最优的机器指令序列
当程序需要支持动态链接时,只能使用JIT
可以根据进程中内存的实际情况调整代码,使内存能够更充分的利用
JIT缺点:
编译需要占用运行时资源,会导致进程卡顿
由于编译时间需要占用运行时间,对于某些代码的编译优化不能完全支持,需要在程序流畅和编译时间之间做权衡
在编译准备和识别频繁使用的方法需要占用时间,使得初始编译不能达到最高性能
AOT优点:
在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗
可以在程序运行初期就达到最高性能
可以显著的加快程序的启动
AOT缺点:
在程序运行前编译会使程序安装的时间增加
牺牲Java的一致性
将提前编译的内容保存会占用更多的外

Java与C++的区别

虽然,Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:

  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
  • Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
  • C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。

面向对象的三大特性:

继承:对象的一个新类可以从现有的类中派生,派生类可以从它的基类那继承方法和实例变量,且派生类可以修改或新增新的方法使之更适合特殊的需求。

1、子类拥有父类非private的属性和方法;

2、子类可以拥有自己属性和方法,即子类可以对父类进行扩展;

3、子类可以用自己的方式实现父类的方法。

调用父类的构造方法我们使用super()即可。

构建过程是从父类“向外”扩散的,也就是从父类开始向子类一级一级地完成构建。而且我们并没有显示的引用父类的构造器,这就是java的聪明之处:编译器会默认给子类调用父类的构造器。但是,这个默认调用父类的构造器是有前提的:父类有默认构造器。如果父类没有默认构造器,我们就要必须显示的使用super()来调用父类构造器,否则编译器会报错:无法找到符合父类形式的构造器。对于子类而已,其构造器的正确初始化是非常重要的,而且当且仅当只有一个方法可以保证这点:在构造器中调用父类构造器来完成初始化,而父类构造器具有执行父类初始化所需要的所有知识和能力。

对于继承而言,子类会默认调用父类的构造器,但是如果没有默认的父类构造器,子类必须要显示的指定父类的构造器,而且必须是在子类构造器中做的第一件事(第一行代码)。

protected关键字:对于protected而言,它指明就类用户而言,他是private,但是对于任何继承与此类的子类而言或者其他任何位于同一个包的类而言,他却是可以访问的。

封装:将客观事物抽象成类,每个类可以把自身数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏。封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性和方法,如果不想被外界方法,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。

优势有(1)良好的封装能够减少耦合;(2)类内部的结构可以自由修改;(3)可以对成员进行更加精准的控制;(4)隐藏信息,实现细节。

多态:允许不同类的对象对同一消息作出响应。不同对象调用相同方法即参数也相同,最终表现行为不一样。

Java实现多态有三个必要条件:继承、重写、向上转型。

在Java中有两种形式可以实现多态:继承和接口。

字节序定义以及Java属于哪种字节序?

字节序是指多字节数据在计算机内存中存储或网络传输时字节的存储顺序。通常由小端和大端两组方式。

小端:低位字节存放在内存的低地址端,高位字节存放在内存的高地址端。

大端:高位字节存放在内存的低地址端,低位字节存放在内存的高地址端。

Java语言的字节序是大端。

JDK、JRE、JVM的区别和联系

JDK:JDK(Java Development Kit) 是整个JAVA的核心,包括了Java运行环境(Java Runtime Envirnment),一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)。在目录下面有 六个文件夹、一个src类库源码压缩包、和其他几个声明文件。其中,真正在运行java时起作用的 是以下四个文件夹:bin、include、lib、 jre。

JRE:JRE(Java Runtime Environment,Java运行环境),包含JVM标准实现及Java核心类库。JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器)

JRE是指java运行环境。光有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib。 (JRE里有运行.class的java.exe)

JVM:JVM(Java Virtual Machine),即java虚拟机, java运行时的环境,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。针对java用户,也就是拥有可运行的.class文件包(jar或者war)的用户。里面主要包含了JVM和java运行时基本类库(rt.jar)。rt.jar可以简单粗暴地理解为:它就是java源码编译成的jar包。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

联系:

JVM不能单独搞定class的执行,解释class的时候JVM需要调用解释所需要的类库lib。在JDK下面的的JRE目录里面有两个文件夹bin和lib,在这里可以认为bin里的就是JVM,lib中则是JVM工作所需要的类库,而JVM和 lib和起来就称为JRE。JVM+Lib=JRE。总体来说就是,我们利用JDK(调用JAVA API)开发了属于我们自己的JAVA程序后,通过JDK中的编译程序(javac)将我们的文本java文件编译成JAVA字节码,在JRE上运行这些JAVA字节码,JVM解析这些字节码,映射到CPU指令集或OS的系统调用。

区别:

a.JDK和JRE区别:在bin文件夹下会发现,JDK有javac.exe而JRE里面没有,javac指令是用来将java文件编译成class文件的,这是开发者需要的,而用户(只需要运行的人)是不需要的。JDK还有jar.exe, javadoc.exe等等用于开发的可执行指令文件。这也证实了一个是开发环境,一个是运行环境。
b.JRE和JVM区别:JVM并不代表就可以执行class了,JVM执行.class还需要JRE下的lib类库的支持,尤其是rt.jar。

Java访问修饰符

default:默认访问修饰符,在同一包内可见

private:在同一类可见,不能修饰类

protected:对同一包内的类和所有子类可见,不能修饰类

public:对所有类可见

 

 

 构造方法、成员变量初始化以及静态成员变量三者的初始化顺序

先后顺序:静态成员变量、成员变量、构造方法。

当类第一次被加载的时候,静态变量会首先初始化,接着编译器会把实例变量初始化为默认值,然后执行构造方法。

Java程序的初始化一般遵循以下三个原则(以下三原则优先级依次递减):

① 静态对象(变量)优先于非静态对象(变量)初始化,其中,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次;

② 父类优先于子类进行初始化;

③ 按照成员变量定义顺序进行初始化,即使变量定义散布于方法定义中,它们依然在任何方法(包括构造方法)被调用之前先初始化。

详细的先后顺序:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数。

接口和抽象类的相同点和区别?

相同点:

1.都不能被实例化

2.接口的实现类和抽象类的子类需要实现接口或抽象类中响应的方法才能被实例化

不同点:

1.接口只能有方法定义,不能有方法的实现,而抽象类可以有方法的定义与实现

2.实现接口的关键字为implement,继承抽象的关键字为extends。一个类可以实现多个接口,只能继承一个抽象类。

3.当子类和父类之间存在逻辑上的层次结构,推荐使用抽象类,有利于功能的积累。当功能不需要,希望支持差别较大的两个或更多对象间的特定交互行为,推荐使用接口。使用接口能降低软件系统的耦合度,便于日后维护或添加删除方法。

 

 

 为什么Java不支持多重继承

1.为了程序的结构更加清晰从而便于维护。假设Java语言支持多重继承,类C继承自类A和类B,如果类A和类B都有自定义相同名称的成员方法f(),那么当代码待用类C的f()会产生二义性。Java语言通过实现多个接口间接支持多重继承,接口由于只包含方法定义,不包含方法的实现,类C继承接口A与接口B时即使它们都有方法f(),也不能直接调用方法,需要实现具体方法f()才能调用,不会产生二义性

2.多重继承会使类型转换、构造方法的调用顺序变得复杂,会影响到性能。

Java提供的多态机制

Java提供了两种用于多态的机制,分别是重载与覆盖。

重载:重载是指同一个类中有多个同名的方法,但是这些方法有不同的参数,在编译期间就可以可以确定调用的哪个方法。

覆盖(重写):覆盖是指派生类重写基类的方法,使用基类指向其子类的实例对象,或接口的引用变量指向其实现的实例对象,在程序调用的运行期根据引用变量所指的具体实例对象调用正在运行的那个对象的方法,即需要到运气期才能确定调用哪个方法。

覆盖和重载的区别:

1.覆盖是父类与子类之间的关系,是垂直关系;重载是同一类中方法之间的关系,是水平关系。

2.覆盖只能由一个方法或一对方法产生关系;重载是多个方法之间的关系。

3.覆盖要求参数列表相同;重载要求参数列表不同。

4.覆盖中,调用方法体根据对象的类型来决定,而重载是根据调用时实参表与形参表来对应选择方法体。

5.重载方法可以改变返回值的类型,覆盖的方法不能改变返回值类型。

深拷贝、浅拷贝和引用拷贝

  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
  • 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
  • 引用拷贝就是两个不同的引用指向同一个对象。

 

 

final、finally、finalize的区别是什么?

1.final用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承

2.finally作为异常处理的一部分,只能在try/catch语句中使用,finally附带一个语句块用来表示这个语句最终一定被执行,经常被用在需要释放资源的情况下。

3.finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收的对象的finalize()方法。当垃圾回收器准备好释放对象对象占用空间时,首先会调用finalize()方法,并在下一次垃圾回收动作发生时真正回收对象占用的内存。

 

出现在Java程序中的finally代码块是否一定会执行?

当遇到下面情况不会执行。

1.当程序在进入try语句块之前就出现异常时会直接结束。

2.当程序在try块中强制退出时,如果使用System.exit(0),也不会执行finally块中的代码。

 

Java语言中关键字static的作用是什么?

static的主要作用有两个:

1.为某种特定数据类型或者对象分配与创建对象个数无关的单一的存储空间。

2.使得某个方法或属性与类而不是与对象关联在一起,即在不创建对象的情况下可以通过类直接调用方法或使用类的属性。

具体而言static又可以分为4种使用方式:

1.修饰成员变量。用static关键字修饰的静态变量在内存中只有一个副本。只要静态变量所在的类被加载,这个静态变量就会被分配空间,可以使用“类.静态变量”和“对象.静态变量”的方法使用。

2.修饰成员方法。static修饰的方法无需创建对象就可被调用。static方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态变量和静态成员方法。

3.修饰代码块。JVM在加载类的时候会执行static代码块。static代码块常用于初始化静态变量。static代码块只会被执行一次。

4.修饰内部类。static内部类可以不依赖外部类实例对象而被实例化。静态内部类不能与外部类有相同的名字,不能访问普通成员变量,只能访问外部类中的静态成员和静态成员方法。

 

Java代码块执行顺序

1.父类静态代码块(只执行一次)

2.子类静态代码块(只执行一次)

3.父类构造代码块

4.父类构造函数

5.子类构造代码块

6.子类构造函数

7.普通代码块

public class CodeBlock {
 
    static{
 
        System.out.println("静态代码块");
 
    }
 
    {
 
        System.out.println("构造代码块");
 
    }
 
}

String、StringBuffer和StringBuilder有什么区别

String用于字符串操作,属于不可变类(字符串常量)。String对象一旦被创建,其值将不能被改变。

StringBuffer是可变类,当创建对象后,仍然可以对其值进行修改(字符串变量);线程安全

String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)

String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。

String的优点:

1.节省空间:字符串常量存储在JVM的字符串池中可以被用户共享使用。

2.提高效率:String会被不同线程共享,是线程安全的。在涉及多线程操作中不需要同步操作。

3.安全:String常被用于用户名、密码、文件名等使用,由于其不可变,可避免黑客行为对其恶意修改。

 

判等运算符== 与 equals区别

== 比较的是引用,equals比较的是内容

  • “==”是运算符,如果是基本数据类型,则比较存储的值;如果是引用数据类型,则比较所指向对象的地址值。
  • equals是Object的方法,比较的是所指向的对象的地址值,一般情况下,重写之后比较的是对象的值。

equals 方法不能用于比较基本数据类型,如果没有对 equals 方法进行重写,则相当于“==”,比较的是引用类型的变量所指向的对象的地址值。
String类对equals()方法进行了重写。

public static void main(String[] args) {
        //基本数据类型的比较
        int num1 = 10;
        int num2 = 10;
        System.out.println(num1 == num2);   //true

        //引用数据类型的比较
        //String类(重写了equals方法)中==与equals的比较
        
        String s1 = "hello";
        String s2 = "hello";
        System.out.println(s1 == s2);    //true,比较地址值:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
        System.out.println(s1.equals(s2));//true,比较内容:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
        System.out.println(s1.equals("hello")); //true

       
        String s3 = new String("hello");
        String s4 = new String("hello");
        System.out.println(s3 == s4);        //false,比较地址值:s3和s4在堆内存中的地址值不同
        System.out.println(s3.equals(s4));    //true,比较内容:内容相同

        //没有重写equals方法的类中==与equals的比较 
        People p1 = new People();
        People p2 = new People();
        People p = p2;
        System.out.println(p1);//People@135fbaa4
        System.out.println(p2);//People@45ee12a7
        System.out.println(p); //People@45ee12a7
        System.out.println(p1.equals(p2));       //false,p1和p2的地址值不同
        System.out.println(p.equals(p2));        //true,p和p2的地址值相同
    }

对于String s2 = new String(“world”);
首先在堆内存中申请内存存储String类型的对象,将地址值赋给s2;
在方法区的常量池中找,有无hello:
若没有,则在常量池中开辟空间存储hello,并将该空间的地址值赋给堆中存储对象的空间;
若有,则直接将hello所在空间的地址值给堆中存储对象的空间。

对于String s1 = “hello”;
在方法区的常量池中找,有无hello,如果没有,就在常量池中开辟空间存储hello。
然后只需要将hello所在空间的地址值赋给 s1。

String s1 = "hello";//在字符串常量池中创建"hello",并将地址值赋值给s1;
String s2 = new String("world");//通过new关键字在堆中创建对象,并将对象地址值赋值给s2.

 

一篇十分详细的解析:

https://blog.csdn.net/weixin_46460843/article/details/110310604?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165811330116781647546401%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165811330116781647546401&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-110310604-null-null.142^v32^experiment_2_v1,185^v2^control&utm_term=%3D%3D%20%E4%B8%8E%20equals&spm=1018.2226.3001.4187

序列化和反序列化

1、序列化和反序列化的定义:

(1)Java序列化 就是指把Java对象转换为字节序列的过程

Java反序列化 就是指把字节序列恢复为Java对象的过程。

(2)序列化最重要的作用:在传递和保存对象时.保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。

反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。

总结:核心作用就是对象状态的保存和重建。(整个过程核心点就是字节流中所保存的对象状态及描述信息)

2、json/xml的数据传递:

在数据传输(也可称为网络传输)前,先通过序列化工具类将Java对象序列化为json/xml文件。

在数据传输(也可称为网络传输)后,再将json/xml文件反序列化为对应语言的对象

3、序列化优点:

①将对象转为字节流存储到硬盘上,当JVM停机的话,字节流还会在硬盘上默默等待,等待下一次JVM的启动,把序列化的对象,通过反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间(永久性保存对象)。

②序列化成字节流形式的对象可以进行网络传输(二进制形式),方便了网络传输。

③通过序列化可以在进程间传递对象。

 

实际开发中有哪些用到序列化和反序列化的场景?

  1. 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  2. 将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化。
  3. 将对象存储到缓存数据库(如 Redis)时需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。

 

具体实现:实现Serializable,或实现Externalizable接口中的writerExternal()与readExternal()方法。

Java中的Class对象

Java中对象可以分为实例对象和Class对象,每一个类都有Class对象,其中包含了该类有关的信息。

获取Class对象的方法:

(1)Class.forName("类的全限定名")

(2)实例对象.getClass()

(3)类名.class

当我们编写一个新的java类时,JVM就会帮我们编译成class对象,存放在同名的.class文件中。在运行时,当需要生成这个类的对象,JVM就会检查此类是否已经装载内存中。若是没有装载,则把.class文件装入到内存中。若是装载,则根据class文件生成实例对象。

当我们new一个新对象或者引用静态成员变量时,Java虚拟机(JVM)中的类加载器子系统会将对应Class对象加载到JVM中,然后JVM再根据这个类型信息相关的Class对象创建我们需要实例对象或者提供静态变量的引用值。需要特别注意的是,手动编写的每个class类,无论创建多少个实例对象,在JVM中都只有一个Class对象,即在内存中每个类有且只有一个相对应的Class对象,

 

 

(1)Class类也是类的一种,与class关键字是不一样的。

(2)手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且这个Class对象保存在同名.class的文件中(字节码文件),比如创建一个Shapes类,编译Shapes类后就会创建其包含Shapes类相关类型信息的Class对象,并保存在Shapes.class字节码文件中。

(3)每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。

(4)Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载

(5)Class类的对象作用是运行时提供或获得某个对象的类型信息,这点对于反射技术很重要(关于反射稍后分析)。

所有的类都是在对其第一次使用时动态加载到JVM中的,当程序创建第一个对类的静态成员引用时,就会加载这个被使用的类(实际上加载的就是这个类的字节码文件),注意,使用new操作符创建类的新实例对象也会被当作对类的静态成员的引用(构造函数也是类的静态方法),由此看来Java程序在它们开始运行之前并非被完全加载到内存的,其各个部分是按需加载,所以在使用该类时,类加载器首先会检查这个类的Class对象是否已被加载(类的实例对象创建时依据Class对象中类型信息完成的),如果还没有加载,默认的类加载器就会先根据类名查找.class文件(编译后Class对象被保存在同名的.class文件中),在这个类的字节码文件被加载时,它们必须接受相关验证,以确保其没有被破坏并且不包含不良Java代码(这是java的安全机制检测),完全没有问题后就会被动态加载到内存中,此时相当于Class对象也就被载入内存了(毕竟.class字节码文件保存的就是Class对象),同时也就可以被用来创建这个类的所有实例对象。

Class.forName

Class.forName("com.zejian.Gum");

其中forName方法是Class类的一个static成员方法,记住所有的Class对象都源于这个Class类,因此Class类中定义的方法将适应所有Class对象。这里通过forName方法,我们可以获取到Gum类对应的Class对象引用。从打印结果来看,调用forName方法将会导致Gum类被加载(前提是Gum类从来没有被加载过)。

public static void main(String[] args) {

    try{
      //通过Class.forName获取Gum类的Class对象
      Class clazz=Class.forName("com.zejian.Gum");
      System.out.println("forName=clazz:"+clazz.getName());
    }catch (ClassNotFoundException e){
      e.printStackTrace();
    }

    //通过实例对象获取Gum的Class对象
    Gum gum = new Gum();
    Class clazz2=gum.getClass();
    System.out.println("new=clazz2:"+clazz2.getName());

  }


Class字面常量

//字面常量的方式获取Class对象
Class clazz = Gum.class;

这种方式相对前面两种方法更加简单,更安全。因为它在编译器就会受到编译器的检查同时由于无需调用forName方法效率也会更高,因为通过字面量的方法获取Class对象的引用不会自动初始化该类。

 

 

加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象

链接:验证字节码的安全性和完整性,准备阶段正式为静态域分配存储空间,注意此时只是分配静态成员变量的存储空间,不包含实例成员变量,如果必要的话,解析这个类创建的对其他类的所有引用。

初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量。

由此可知,我们获取字面常量的Class引用时,触发的应该是加载阶段,因为在这个阶段Class对象已创建完成,获取其引用并不困难,而无需触发类的最后阶段初始化。

 

获取Class对象引用的方式3种,通过继承自Object类的getClass方法,Class类的静态方法forName以及字面常量的方式”.class”。

其中实例类的getClass方法和Class类的静态方法forName都将会触发类的初始化阶段,而字面常量获取Class对象的方式则不会触发初始化。

初始化是类加载的最后一个阶段,也就是说完成这个阶段后类也就加载到内存中(Class对象在加载阶段已被创建),此时可以对类进行各种必要的操作了(如new对象,调用静态成员等),注意在这个阶段,才真正开始执行类中定义的Java程序代码或者字节码。

Java反射机制

反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。一直以来反射技术都是Java中的闪亮点,这也是目前大部分框架(如Spring/Mybatis等)得以实现的支柱。在Java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。

在反射包中,我们常用的类主要有

Class类可以获取类属性方法、

Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、

Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、

Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private),

注解与元注解

Java注解用于为Java代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。其可以用于提供信息给编译器,在编译阶段时给软件提供信息进行相关的处理,在运行时处理响应代码,做对应操作。

 

元注解可以理解为注解的注解,即在注解中使用,实现想要的功能。

@Retention:表示注解存在阶段是保留在源码,还是在字节码(类加载)或者运行期(JVM中运行)。

@Target:表示注解作用的范围。

@Documented:将注解中的元素包含到Javadoc中去。

@Inherited:一个被@Inherited注解了的注解修饰了一个父类,如果他的子类没有被其他注解修饰,则它的子类也继承了父类的注解。

@Repeatable:被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含义。

 

异常

 

 

  • Exception的直接子类:编译时异常(要求程序员在编写程序阶段必须预先对这些异常进行处理,如果不处理编译器报错,因此得名编译时异常。)。
  • RuntimeException:运行时异常。(在编写程序阶段程序员可以预先处理,也可以不管,都行。)

 

  • throws 在方法声明位置上使用,表示上报异常信息给调用者。一般用于方法声明上,代表该方法可能会抛出的异常列表。
  • throw 手动抛出异常!一般是用在方法体内部,由开发者定义当程序出现问题后主动抛出一个异常。
    public void pop() throws StackOperationException {
        if(index < 0){
            throw new MyStackOperationException("弹栈失败,栈已空!");//手动抛出异常
        }
    }

泛型

泛型,即“参数化类型”,解决不确定对象具体类型的问题。在编译阶段有效。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型在类中称为泛型、接口中称为泛型接口和方法中称为泛型方法。

Java编译器生成的字节码是不包含泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程被称为泛型擦除。

Java泛型的实现是靠类型擦除技术实现的,类型擦除是在编译期完成的,也就是在编译期,编译器会将泛型的类型参数都擦除成它指定的原始限定类型,如果没有指定的原始限定类型则擦除为object类型,之后在获取的时候再强制类型转换为对应的类型,因此生成的Java字节码中是不包含泛型中的类型信息的,即运行期间并没有泛型的任何信息。

Java值传递

https://www.zhihu.com/question/31203609/answer/576030121

自动装箱拆箱

对于Java基本数据类型(byte、short、int、long、float、double、char、boolean),均对应一个包装类。

装箱就是自动将基本数据类型转换为包装器类型,如int -> Integer

拆箱就是自动将包装器类型转换为基本数据类型,如Integer -> int

在进行自动装箱时,Java 虚拟机会自动调用 Integer.valueOf()。
在进行自动拆箱时,Java 虚拟机会自动调用 Integer.intValue()。
其他数据类型的自动装箱和自动拆箱的过程和 Integer 类似,都是调用类似 xxxValue()、valueOf() 等方法。

Object类常用方法

1.hashCode:通过对象计算出的散列码。用于map型或equals方法。哈希值根据对象的地址或字符串或数字使用hash算法计算出来的int类型的数值。一般情况下相同对象返回相同哈希码。

public class TestStudent {
    public static void main(String[] args) {
        Student s1 = new Student("aaa",20);
        Student s2 = new Student("bbb",22);
        //判断s1和s2是不是同一个类型
        Class class1 = s1.getClass();
        Class class2 = s2.getClass();
        if(class1==class2) {
            System.out.println("s1和s2属于同一个类型");
        }else {
            System.out.println("s1和s2不属于同一个类型");
        }
        
        //hashCode 方法
        System.out.println(s1.hashCode());  //804564176
        
        System.out.println(s2.hashCode());  //1421795058
        
        Student s3=s1;
        System.out.println(s3.hashCode());  //804564176
    }

}

2.equals:判断两个对象是否一致。需保证equals方法相同对应的对象hashCode也相同。

3.toString:用字符串表示该对象。

package com.zhuo.qf;

public class TestStudent {
    public static void main(String[] args) {
// 3. toString方法
        System.out.println("----------3toString-------------");
        System.out.println(s1.toString()); //com.zhuo.qf.Student@2ff4acd0 -->十六进制
        System.out.println(s2.toString()); //com.zhuo.qf.Student@54bedef2
    }
}

4.getClass:通常用于判断两个引用中实际存储对象类型是否一致

 

/**
 * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
 */
public final native Class<?> getClass()
/**
 * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
 */
public native int hashCode()
/**
 * 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
 */
public boolean equals(Object obj)
/**
 * naitive 方法,用于创建并返回当前对象的一份拷贝。
 */
protected native Object clone() throws CloneNotSupportedException
/**
 * 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
 */
public String toString()
/**
 * native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
 */
public final native void notify()
/**
 * native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
 */
public final native void notifyAll()
/**
 * native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
 */
public final native void wait(long timeout) throws InterruptedException
/**
 * 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。
 */
public final void wait(long timeout, int nanos) throws InterruptedException
/**
 * 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
 */
public final void wait() throws InterruptedException
/**
 * 实例被垃圾回收器回收的时候触发的操作
 */
protected void finalize() throws Throwable { }

内部类

作用:间接解决类无法多继承引起的一系列问题

/**
 1. Outer类继承了ClassA,实现了IFunctionA
*/
public class Outer extends ClassA implements IFunctionA{ 
    /**
    *    Inner类继承了ClassB,实现了IFunctionB
    */
    public class Inner extends ClassB implements IfunctionB{
    //
    } 
}

1、内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
2、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
3、内部类提供了更好的封装,除了该外围类,其他类都不能访问。
4、创建内部类对象的时刻并不依赖于外围类对象的创建。

成员内部类:作为成员对象的内部类。可以访问private及以上外部类的属性和方法。外部类想要访问内部类属性或方法时,必须创建一个内部类对象,然后通过该对象访问内部类的属性或方法。外部类也可以访问private修饰的内部类属性。

/**
 * 外部类、成员内部类的定义
 */
public class Outer {
    /**
     * 成员方法
     */
    public void outerMethod() {
        System.out.println("我是外部类的outerMethod方法");
    }
    /**
     * 静态方法
     */
    public static void outerStaticMethod() {
        System.out.println("我是外部类的outerStaticMethod静态方法");
    }
    /**
     * 内部类
     */
    public class Inner {
        private int commonVariable = 20;

        /**
         * 构造方法
         */
        public Inner() {
        }
        /**
         * 成员方法,访问外部类信息(属性、方法)
         */
        public void innerShow() {
            //当和外部类冲突时,直接引用属性名,是内部类的成员属性
            System.out.println("内部的commonVariable:" + commonVariable);
            //内部类访问外部属性
            System.out.println("outerVariable:" + outerVariable);
            //当和外部类属性名重叠时,可通过外部类名.this.属性名
            System.out.println("外部的commonVariable:" + Outer.this.commonVariable);
            System.out.println("outerStaticVariable:" + outerStaticVariable);
            //访问外部类的方法
            outerMethod();
            outerStaticMethod();
        }
    }
    /**
     *    外部类访问内部类信息
     */
    public void outerShow() {
        Inner inner = new Inner();
        inner.innerShow();
    }
}

 

局部内部类:存在于方法中的内部类。访问权限类似局部变量,只能访问外部类的final变量。

匿名内部类:只能使用一次,没有类名,只能访问外部类的final变量。

/**
*    外部内、内部类
*/
public class Outer {

    public static IAnimal getInnerInstance(String speak){
        return new IAnimal(){
            @Override
            public void speak(){
                System.out.println(speak);
            }};
            //注意上一行的分号必须有
    }
    
    public static void main(String[] args){
        //调用的speak()是重写后的speak方法。
        Outer.getInnerInstance("小狗汪汪汪!").speak();
    }
}

静态内部类:类似类的静态成员变量。

https://blog.csdn.net/weixin_42762133/article/details/82890555?ops_request_misc=&request_id=&biz_id=102&utm_term=Java%20%E5%86%85%E9%83%A8%E7%B1%BB&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-3-82890555.142^v32^experiment_2_v1,185^v2^control&spm=1018.2226.3001.4187

Java中线程安全的基本数据结构有哪些?

HashTable:哈希表的线程安全版,效率低

ConcurrentHashMap:哈希表的线程安全版,效率高,用于代替HashTable

Vector:线程安全版Arraylist

Stack:线程安全版栈

BlockingQueue及其子类:线程安全版队列

集合篇

List

List

(1)ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
(2)LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
(3)Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素

Set

(1)HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
具体实现唯一性的比较过程:存储元素首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashCode值处的元素对象;如果hashCode相等,存储元素的对象还是不一定相等,此时会调用equals()方法判断两个对象的内容是否相等,如果内容相等,那么就是同一个对象,无需存储;如果比较的内容不相等,那么就是不同的对象,就该存储了,此时就要采用哈希的解决地址冲突算法,在当前hashCode值处类似一个新的链表, 在同一个hashCode值的后面存储存储不同的对象,这样就保证了元素的唯一性。

(2)LinkedHashSet底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
(3)TreeSet底层数据结构采用二叉树来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性。根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造),自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;比较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法;

 

 

Map

 

为什么HashMap线程不安全?

在JDK1.7中,HashMap采用头插法插入元素,因此并发情况下会导致环形链表,产生死循环。虽然JDK1.8采用尾插法解决了这个问题,但是并发下的put操作也会使前一个key被后一个key覆盖。由于HashMap有扩容机制存在,也存在A线程进行扩容后,B线程执行get方法出现失误的情况。

Collection和Collections有什么区别?

1.Collection是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如List、Set等

2.Collections是一个包装类,它包含了很多静态方法,不能被实例化,而是作为工具类使用,比如提供的排序方法:Collections.sort(list);提供反转的方法Collections.recerse(list)

 

HashSet中,equals与hashCode之间的关系?

equals和hashCode这两个方法都是从object类中继承过来的,equals主要用于判断对象的内存地址引用是否同一个地址;hashCode根据定义的哈希规则将对象的内存地址转换为一个哈希码。HashSet中存储的元素是不能重复的,主要通过hashCode与equals两个方法来判断存储的对象是否相同;

规范要求做到的结果

1.如果两个对象的hashCode值不同,说明两个对象不相同;

2.如果两个对象的hashCode值相同,接着会调用对象的equals方法,如果equals方法返回结果为true,那么说明两个对象相同,否则不相同。

 JUC

简述Java内存模型(JMM)

Java内存模型定义了程序中各种变量的访问规则。其规定所有变量都存储在主内存,线程均有自己的工作内存。工作内存中保存被该线程使用的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。

简述as-if-serial

编译器等会对原始的程序进行指令重排和优化。但不管怎么重排序,其结果和用户原始程序输出预定结果一致。

简述happens-before八大原则

程序次序规则:一个线程内写在前面的操作先行发生于后面的。

锁定规则:unlock操作先行发生于后面对同一个锁的lock操作。

volatile规则:对volatile变量的写操作先行发生于后面的读操作。

线程启动规则:线程的start方法先行于线程的每个动作。

线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

线程终止规则:线程中所有操作先行发生于对线程的终止检测。

对象终结规则:对象的初始化先行发生于finalize方法。

传递性规则:如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。

as-if-serial和happens-before的区别

as-if-serial保证单线程程序的执行结果不变,happens-before保证正确同步的多线程程序的执行结果不变,

简述线程的可见性

可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile、synchronized、final都能保证可见性。

简述有序性

即虽然多线程存在并发和指令优化等操作,在本线程内观察该线程的所有执行操作是有序的。

简述Java中volatile关键字作用

1.保证变量对所有线程的可见性。当一条线程修改了变量值,新值对于其他线程来说是立即可用得知的。

2.禁止指令重排序优化。使用volatile变量进行写操作,汇编指令带有lock前缀,相当于一个内存屏障,编译器不会将后面的指令重排到内存屏障之前。

Java线程的实现方式

1.实现Runnable接口

2.继承Thread类

3.实现Callable接口

简述线程通信的方式

1.volatile关键字修饰变量,保证所有线程对变量访问的可见性。

2.synchronized关键字。确保多个线程在同一时刻只能有一个处于方法或同步块中。

3.wait/notify方法

4.IO通信

简述线程池

没有线程池的情况下,多次创建,销毁线程开销比较大。如果在开辟的线程执行完成当前任务后执行接下来的任务,复用已经创建的线程,降低开销、控制最大并发数。

线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后还会循环获取工作队列中的任务来执行。

将任务派发给线程池时,会出现以下几种情况:

1.核心线程池未满,创建一个新的线程执行任务。

2.如果核心线程池已满,工作队列未满,将线程存储在工作队列。

3.如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务。

4.如果超过最大线程数,按照拒绝策略来处理任务。

 

 

Java中的乐观锁与CAS算法

对于乐观锁,开发者认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。

到了写操作时才进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败;如果没有被修改,那就执行修改操作,返回修改成功。

乐观锁一般都采用Compare And Swap (CAS)算法进行实现。顾名思义,该算法涉及到了两个操作,比较和转换。

CAS算法的思路如下:

1.该算法认为不同线程对变量的操作产生竞争的情况比较少;

2.该算法的核心是对当前变量值E和内存中的变量旧值V进行比较;

3.如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为N;

4.如果不等,就认为在读取值E到比较阶段,有其他线程对变量进行修改过,不进行任何操作。

 

悲观锁就是持悲观态度的锁。就在操作数据时比较悲观,每次去拿数据的时候认为别的线程也会同时修改数据,所以每次在拿数据的时候都会上锁,这样别的线程想拿到这个数据就会阻塞直到它拿到锁。

ABA问题

  1. 线程1读取了数据A
  2. 线程2读取了数据A
  3. 线程2通过CAS比较,发现值是A没错,可以把数据A改成数据B
  4. 线程3读取了数据B
  5. 线程3通过CAS比较,发现数据是B没错,可以把数据B改成了数据A
  6. 线程1通过CAS比较,发现数据还是A没变,就写成了自己要改的值

CAS算法是基于值来做比较的,如果当前有两个线程,一个线程将变量从A到B,再有B改回A,当前线程开始执行CAS算法时,就很容易认为值没有变化,误认为读取数据到执行CAS算法的期间,没有线程修改过数据。

juc包提供了一个AtomicStampedReference,即在原始的版本下加入版本号戳,解决ABA问题。

Synchronized底层实现原理

Java对象底层都关联一个monitor,使用synchronized时JVM会根据使用环境找到对象的monitor,根据monitor的状态进行加解锁的判断。如果成功加锁就成为该monitor的唯一持有者,monitor在被释放前不能再被其他线程获取。

synchronized在JVM编译后会产生monitorenter和monitorexit这两个字节码指令,获取和释放monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前的实例对象;对于静态同步方法,锁是当前类的Class对象;对于同步方法块,锁是synchronized括号里的对象。

执行monitorenter指令时,首次尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加1,执行monitorexit指令时将锁计数器减1.一旦计数器为0锁随即就被释放。

 

Synchronized关键字使用方法

1.直接修饰某个实例方法

2.直接修饰某个静态方法

3.修饰代码块

简述Java偏向锁、轻量级锁、重量级锁

偏向锁

该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。

偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。

如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

偏向锁并不会主动释放,这样每次偏向锁进行的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进行同步操作。

其申请流程为: 1. 首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进入轻量级锁判断逻辑。 否则继续下一步判断; 2. 判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,继续下一步的判断, 如果不一致,跳转到步骤4; 3. 判断是否需要重偏向。如果不用的话,直接获得偏向锁; 4. 利用 CAS 算法将对象的 Mark Word 进行更改,使线程 ID 部分换成本线程 ID。如果更换成功,则 重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。

轻量级锁

当锁处于偏向锁的时候,而又被另一个线程所企图抢占时,偏向锁就会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁线程,以便提高性能。

轻量级锁是为了在没有竞争的前提下减少重量级锁出现并导致的性能消耗。

其申请流程为: 1. 如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前 Mark Word 的拷贝。 2. 虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针 3. 如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。 4. 如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指 向当前线程的栈帧 5. 如果指向当前线程的栈帧,说明当前线程已经拥有了锁,直接进入同步块继续执行 6. 如果不是则说明锁对象已经被其他线程抢占。 7. 如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为 10,此时Mark Word 存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。

重量级锁

重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也就叫做同步锁,这个锁 对象 Mark Word 再次发生变化,会指向一个监视器(Monitor)对象,该监视器对象用集合的形 式,来登记和管理排队的线程。

Java自旋锁、自适应自旋锁、锁粗化、锁消除

自旋锁:线程获取锁失败后,可以采用这样的策略,可以不放弃CPU,不停的重试获取锁。

自适应自旋锁:自旋次数不在人为设定,通常由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。

锁粗化:扩大加锁范围,避免反复的加锁和解锁。

锁消除:是一个更为彻底的优化,在编译时,Java编译器对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。

AQS

它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。

Sync queue:同步队列,是一个双向链表。包括head节点和tail节点。head节点主要用作后续的调度。 Condition queue:非必须,单向链表。当程序中存在cindition的时候才会存在此列表。

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

子类通过继承同步器并实现它的抽象方法getState、setState和compareAndSetState对同步状态进行更改。

 

AQS获取独占锁/释放锁独占锁原理

获取:(acquire) 1. 调用 tryAcquire 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter 方法加入到同步队列的尾部,在队列中自旋。 2. 调用 acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞。 释放:(release) 1. 调用 tryRelease 方法释放同步状态 2. 调用 unparkSuccessor 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。

 

AQS获取共享锁/释放共享锁原理

获取锁(acquireShared) 1. 调用 tryAcquireShared 方法尝试获取同步状态,返回值不小于 0 表示能获取同步状态。 释放(releaseShared) 1. 释放,并唤醒后续处于等待状态的节点。

 Unsafe类

Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重。

另外,Unsafe 提供的这些功能的实现需要依赖本地方法(Native Method)。你可以将本地方法看作是 Java 中使用其他编程语言编写的方法。本地方法使用 native 关键字修饰,Java 代码中只是声明方法头,具体的实现则交给 本地代码。

功能:
(1)内存操作

在 Java 中是不允许直接对内存进行操作的,对象内存的分配和回收都是由 JVM 自己实现的。但是在 Unsafe 中,提供的接口可以直接进行内存操作。

需要注意,通过这种方式分配的内存属于 堆外内存 ,是无法进行垃圾回收的,需要我们把这些内存当做一种资源去手动调用freeMemory方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在try中执行对内存的操作,最终在finally块中进行内存的释放。

为什么要使用堆外内存?

  • 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
  • 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。

(2)内存屏障

在介绍内存屏障前,需要知道编译器和 CPU 会在保证程序输出结果一致的情况下,会对代码进行重排序,从指令优化角度提升性能。而指令重排序可能会带来一个不好的结果,导致 CPU 的高速缓存和内存中数据的不一致,而内存屏障(Memory Barrier)就是通过组织屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。

(3)对象操作

(4)数组操作

(5)CSA操作

(6)线程调度

(7)Class操作

(8)系统信息

 

标签:面试题,Java,对象,线程,内存,方法,Class
From: https://www.cnblogs.com/baifeili/p/16491785.html

相关文章

  • JVM面试题
    JVM 深色为所有线程共享数据区;浅色为线程隔离区。简述JVM内存模型线程私有的运行时数据区:程序计数器、Java虚拟机栈、本地方法栈。线程共享的运行时数据区:Java堆、方......
  • JavaScript冒泡排序+Vue可视化冒泡动画
    冒泡排序(BubbleSort)算是前端最简单的算法,也是最经典的排序算法了。网上JavaScript版本的冒泡排序很多,今天用Vue实现一个动态的可视化冒泡排序。01、JavaScript冒泡排序......
  • Ubuntu 安装配置 Java 环境
    下载Java官网https://www.oracle.com/java/technologies/downloads/https://www.oracle.com/cn/java/technologies/downloads/国内镜像http://www.codebaoku.com/jd......
  • 饮料换购【第六届蓝桥杯省赛C++A/C组,第六届蓝桥杯省赛JAVAB组】
    饮料换购乐羊羊饮料厂正在举办一次促销优惠活动。乐羊羊C型饮料,凭3个瓶盖可以再换一瓶C型饮料,并且可以一直循环下去(但不允许暂借或赊账)。请你计算一下,如果小明不浪费瓶......
  • 【Java面试指北】反射(1) 初识反射
    如果你被问到:什么是反射?为什么需要反射、以及反射的应用?你会如何回答呢?本篇会带大家初识反射,了解反射概念和基本应用。反射的原理以及深入源码的探究将会在后面几篇介绍。......
  • 买不到的数目【第四届蓝桥杯省赛C++A组,第四届蓝桥杯省赛JAVAC组】
    买不到的数目小明开了一家糖果店。他别出心裁:把水果糖包成4颗一包和7颗一包的两种。糖果不能拆包卖。小朋友来买糖的时候,他就用这两种包装来组合。当然有些糖果数目是......
  • Java实现异步编程的8种方式
    一、......
  • java命令--jstat 工具使用
    jstat(JVMStatisticsMonitoringTool)是用于监控虚拟机各种运行状态信息的命令行工具。他可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在......
  • java程序内存泄漏的定位与分析
    1、为什么会发生内存泄漏​​Java​​ 如何检测内在泄漏呢?我们需要一些工具进行检测,并发现内存泄漏问题,不然很容易发生down机问题。编写java程序最为方便的地方就是我们不......
  • Java中的异常处理详解
    Java异常处理的五个关键字:try、catch、finally、throw、throws......