Java,集合,JUC面试题
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;
在方法区的常量池中找,有无world:
若没有,则在常量池中开辟空间存储world,并将该空间的地址值赋给堆中存储对象的空间;
若有,则直接将world所在空间的地址值给堆中存储对象的空间。
对于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的启动,把序列化的对象,通过反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间(永久性保存对象)。
②序列化成字节流形式的对象可以进行网络传输(二进制形式),方便了网络传输。
③通过序列化可以在进程间传递对象。
实际开发中有哪些用到序列化和反序列化的场景?
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化。
- 将对象存储到缓存数据库(如 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及其子类:线程安全版队列
集合篇
Java中的集合类主要由Collection和Map这两个接口派生而出,其中Collection接口又派生出三个子接口,分别是Set、List、Queue。所有的Java集合类,都是Set、List、Queue、Map这四个接口的实现类。
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
Map继承树
一些特殊序列的插入可能导致平衡二叉树的极端情况,用红黑树可以避免一边倒的情况。


Java中的容器,线程安全和线程不安全的分别有哪些
java.util包下的集合类大部分都是线程不安全的,例如我们常用的HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap,这些都是线程不安全的,但他们的优点是性能好。如果我们需要使用线程安全的集合类,则可以使用Collections工具类提供的synchronizedXxx()方法,将这些集合类包装成线程安全的集合类。
java.util包下也有线程安全的集合类,例如Vector、Hashtable(Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰)。这些集合类都是比较古老的API,虽然实现了线程安全,但性能很差。所以即使是使用线程安全的集合类,也建议使用不安全的集合类包装成线程安全集合类的方式,而不是直接使用这些古老的API。
从Java5开始,Java在java.util.concurrent包下提供了大量支持高效并发访问的集合类,他们既能保证良好的访问性能,又能保证线程安全。这些集合类可以分为两部分,他们的特征如下:
(1)以Concurrent开头的集合类
以Concurrent开头的集合类代表了支持并发访问的集合,他们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。
(2)以CopyOnWrite开头的集合类
以CopyOnWrite开头的集合类采用了复制底层数组的方式来实现写操作。当线程对此集合类执行读取操作时,线程将会直接读取集合本身,无需加锁与阻塞。当对线程进行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作,由于对集合的写入操作都是对数组的副本执行操作,因此他是线程安全的。
描述以下Map put的过程
(1)首次扩容
先判断数组是否为空,若数组为空则进行第一次扩容(resize)
(2)计算索引
通过hash算法,计算键值对在数组中的索引
(3)插入数据

JDK7和JDK8中的HashMap有什么区别?
HashMap为什么用红黑树而不用B树?
B/B+树多用于外存,B/B+树也被称为一个磁盘友好的数据结构。
介绍以下HashMap的扩容机制?
JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
为什么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,那么说明两个对象相同,否则不相同。
树有关知识
红黑树是为了弥补二叉树插入形成单边树,虽然是树的结构但是是链表的存储方式,降低效率。
1、红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。
2、平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
B树是因为平衡二叉树存储的信息有限,当数据量巨大时(如外存),我们不再局限于每个结点只存储一个数据的方式。
B树的出现是为了弥补不同的存储级别之间的访问速度上的巨大差异,实现高效的 I/O。平衡二叉树的查找效率是非常高的,并可以通过降低树的深度来提高查找的效率。但是当数据量非常大,树的存储的元素数量是有限的,这样会导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下。另外数据量过大会导致内存空间不够容纳平衡二叉树所有结点的情况。
JUC
为什么wait()方法不定义在Thread中?
wait是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁,每个对象object都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入WAITING状态,自然要操作对应的对象object而非当前线程。
可以直接调用Thread类的run方法吗?
new一个Thread,线程进入了新建状态。调用start()方法,会启动一个线程并是线程进入了就绪状态,当分配到时间片后就可以开始运作了。start()会执行线程的相应工作,然后自动执行run()方法的内容,这是真正的多线程工作。但是,直接执行run()方法,会把run()方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
调用start()方法可以启动线程并使线程进入就绪状态,直接执行run()方法的话不会以多线程的方式执行。
volatile关键字
在Java中,volatile关键字可以保证变量的可见性,如果我们将变量声明为volatile,这就指示JVM,这个变量是共享且不稳定的,每次使用它都到内存中进行读取;volatile还有一个重要的作用就是防止JVM的指令重排序。如果我们将变量声明为volatile,在对这个变量进行读写操作的时候,会通过插入特定的内存屏障的方式来禁止指令重排。
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两个都能保证。
乐观锁和悲观锁
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题,所以每次获取资源操作的时候都会上锁。
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交的时候去验证对应的资源是否被其他线程修改了。
乐观锁一般都采用Compare And Swap (CAS)算法进行实现。顾名思义,该算法涉及到了两个操作,比较和转换。
CAS算法的思路如下:
1.该算法认为不同线程对变量的操作产生竞争的情况比较少;
2.该算法的核心是对当前变量值E和内存中的变量旧值V进行比较;
3.如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为N;
4.如果不等,就认为在读取值E到比较阶段,有其他线程对变量进行修改过,不进行任何操作。
ABA问题
- 线程1读取了数据A
- 线程2读取了数据A
- 线程2通过CAS比较,发现值是A没错,可以把数据A改成数据B
- 线程3读取了数据B
- 线程3通过CAS比较,发现数据是B没错,可以把数据B改成了数据A
- 线程1通过CAS比较,发现数据还是A没变,就写成了自己要改的值
CAS算法是基于值来做比较的,如果当前有两个线程,一个线程将变量从A到B,再有B改回A,当前线程开始执行CAS算法时,就很容易认为值没有变化,误认为读取数据到执行CAS算法的期间,没有线程修改过数据。
juc包提供了一个AtomicStampedReference,即在原始的版本下加入版本号戳,解决ABA问题。
Synchronized关键字
synchronzied是Java中的一个关键字,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
Java对象底层都关联一个monitor,使用synchronized时JVM会根据使用环境找到对象的monitor,根据monitor的状态进行加解锁的判断。如果成功加锁就成为该monitor的唯一持有者,monitor在被释放前不能再被其他线程获取。
实现原理:
Java对象底层都关联一个monitor,使用synchronized时JVM会根据使用环境找到对象的monitor,根据monitor的状态进行加解锁的判断。如果成功加锁就成为该monitor的唯一持有者,monitor在被释放前不能再被其他线程获取。
synchronized在JVM编译后会产生monitorenter和monitorexit这两个字节码指令,获取和释放monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前的实例对象;对于静态同步方法,锁是当前类的Class对象;对于同步方法块,锁是synchronized括号里的对象。
执行monitorenter指令时,首次尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加1,执行monitorexit指令时将锁计数器减1.一旦计数器为0锁随即就被释放。
Synchronized关键字使用方法
1.直接修饰某个实例方法
2.直接修饰某个静态方法
3.修饰代码块
Synchronized关键字和volatile关键字是两个互补的存在。volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码快;volatile关键字主要解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
Java线程的实现方式
1.实现Runnable接口
2.继承Thread类
3.实现Callable接口
简述线程通信的方式
1.volatile关键字修饰变量,保证所有线程对变量访问的可见性。
2.synchronized关键字。确保多个线程在同一时刻只能有一个处于方法或同步块中。
3.wait/notify方法
4.IO通信
简述线程池
使用线程池的好处:
1.降低资源消耗。通过重复利用自己创建的线程降低线程创建和销毁造成的消耗;
2.提高相应速度。当任务到达时,任务不可以需要等到线程创建就能立即执行。
3.提高线程可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行同一的分配,调优和监控。
没有线程池的情况下,多次创建,销毁线程开销比较大。如果在开辟的线程执行完成当前任务后执行接下来的任务,复用已经创建的线程,降低开销、控制最大并发数。
线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后还会循环获取工作队列中的任务来执行。
将任务派发给线程池时,会出现以下几种情况:
1.核心线程池未满,创建一个新的线程执行任务。
2.如果核心线程池已满,工作队列未满,将线程存储在工作队列。
3.如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务。
4.如果超过最大线程数,按照拒绝策略来处理任务。
ReentrantLock
reentrantLock实现了Lock接口,是一个可重入且独占式的锁,和synchronized关键字类似。不过,reentrantLock更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。reentrantLock默认使用非公平锁,reentrantLock的底层通过AQS来实现的。
公平锁:锁被释放以后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
非公平锁:锁被释放之后,后申请的线程可能会先获得锁,是随机或者按照其他优先级顺序的。性能更好,但可能会导致某些线程永远无法获得锁。
synchronized和reenactmentLock的区别
1.两者都是可重入锁,可重入锁是指一个线程获得某个对象的锁,在还没释放这个锁的同时还可以继续获得这个锁。
2.syn依赖于JVM,reen依赖于API
3.reenactment比syn多一些更高级的功能
可中断锁和不可中断锁
可中断锁:获取锁的过程中可以被中断,不需要等到获取锁之后才能进行其他逻辑。reenactmentLock属于可中断锁
不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他逻辑处理。syn属于不可中断锁。
简述Java偏向锁、轻量级锁、重量级锁
偏向锁
该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
偏向锁并不会主动释放,这样每次偏向锁进行的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进行同步操作。
轻量级锁
当锁处于偏向锁的时候,而又被另一个线程所企图抢占时,偏向锁就会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁线程,以便提高性能。
轻量级锁是为了在没有竞争的前提下减少重量级锁出现并导致的性能消耗。
重量级锁
重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也就叫做同步锁,这个锁 对象 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获取独占锁/释放锁独占锁原理
AQS获取共享锁/释放共享锁原理
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)