Java面试总结
Java
1.JDK
- 1.
Java
开发工具集:包括JRE
和命令行开发工具(编译,运行,调试Java
程序所需要的基本工具)
2.JRE
- 1.
Java
运行环境:提供Java
虚拟机和运行Java
应用程序所必需的类库
3.JVM
- 1.运行
Java
字节码的Java
虚拟机,源码先通过编译器编译成二进制字节码文件,然后JVM
把每条要执行的字节码交给Java
解释器翻译成对应的机器码,最后由解释器执行- 2.
JVM
屏蔽了底层操作系统的差异,使Java
程序只需生成在Java
虚拟机上运行的字节码,就可在多种平台上运行,为跨平台提供了支持
4.Java中的标识符
- 1.
Java
中需要定义符号用来标记名称,这些符号被称为标识符- 2.标识符可以由字母,数字,下划线(
_
)和美元符号($
)或人民币符号(¥
)组成,但不能以数字开头,也不能是Java
中的关键字或者保留字
5.访问控制修饰符
修饰符 本类 同包 不同包子类 不同包非子类 public 可以 可以 可以 可以 protected 可以 可以 可以 default 可以 可以 private 可以
- 1.决定内容的
访问权限
- 1.
类的访问控制
:只有public
和default
可以修饰类
,public
类可以在任何地方访问,default
类只能在同一个包内访问- 2.
成员的访问控制
:以上4个访问修饰符
都可以修饰属性
,方法
,构造方法
- 3.
局部变量的访问控制
:以上4个访问修饰符
都不能修饰局部变量
,只有final
可以修饰局部变量- 4.
继承中的访问控制
:子类可以访问父类的public
,default
成员(除非在同一包内)和protected
成员,但不能访问private
成员- 5.
接口的访问控制
:接口的成员默认是public
,不能用private
或protected
修饰- 2.
jdk9
之前default
不允许显式声明,之后允许在接口
中显式声明default
- 3.子类重写方法的访问权限不能比父类更严格(父类是
default
,子类重写该方法不能是private
)
6.static
- 1.
修饰属性
- 1.
类变量/静态变量
:该变量不独属于某个对象,被整个类所有的对象所共有(而不是每个对象都有一个该变量)- 2.静态变量存在
方法区
并只存在一份,静态变量在类加载时初始化,且只初始化一次,静态成员的生命周期与类的生命周期相同(类加载时初始化,类卸载时销毁)- 3.静态属性可以直接通过
类名.属性名
的方式访问- 4.类名.属性名会直接去
方法区
查找静态变量- 5.
static
不能修饰局部变量
,只能修饰成员变量
- 6.静态成员变量虽然独立于对象,但所有的静态方法和静态变量都可通过对象访问
- 2.
修饰方法
- 1.静态方法:静态方法属于类,而不是类的实例,静态方法中无法访问非静态的内容(因为非静态成员依赖于实例),如果需要访问非静态成员,必须先创建类的实例
- 2.通过
类名.方法名()
的方式访问- 3.静态方法仍然可以通过
对象名.方法名()
的方式访问- 4.
static
无法修饰构造方法- 5.静态方法内部无法使用
this
和super
关键字- 6.静态内容可以直接从方法区中访问可能会导致
类的实例对象还未创建
也不会存在this
和super
,有可能会产生冲突- 3.
修饰代码块
- 1.静态代码块:随着类加载而加载,静态代码块在类加载时执行,且只执行一次,一般为静态变量赋初始值,可以提升程序性能
- 2.因此通常会将一些只需要进行一次的初始化操作都放在
static
代码块中- 3.静态初始化块可置于类中的任何地方,类中可有多个静态初始化块
- 4.类初次被加载时,会按照静态初始化块的顺序来执行每个块,并且只会执行一次
- 4.
修饰类
- 1.静态内部类:可以直接通过外部类访问,而不需要外部类的实例
- 2.静态内部类不能直接访问外部类的非静态成员
- 3.静态内部类可以有静态成员(变量、方法等)
1.静态方法和实例方法的区别
- 1.静态方法属于类,实例方法属于对象
- 2.静态方法可以直接通过类名调用,实例方法需要通过对象调用
- 3.静态方法不能访问非静态成员,实例方法可以访问静态和非静态成员
2.成员变量和静态变量的区别
- 1.生命周期不同
- 1.成员变量随着对象的创建而存在,随着对象的回收而销毁
- 2.静态变量在类加载的时候创建,随着类的卸载而消失
- 2.调用方式不同
- 1.成员变量只能被对象调用
- 2.静态变量能被对象调用,也能被类名调用
- 3.别名不一样
- 1.成员变量叫实例变量
- 2.静态变量叫类变量
- 4.数据存储不一样
- 1.成员变量储存在堆内存的对象中,也叫对象的特有数据
- 2.静态变量储存在方法区的静态区,所以也叫对象的共享数据
3.静态代码块的作用
- 1.静态代码块用于初始化静态变量
- 2.静态代码块在类加载时执行,且只执行一次
4.静态代码块和构造函数的执行流程
public class Test extends Base{ static{ System.out.println("test static"); } public Test(){ System.out.println("test constructor"); } public static void main(String[] args) { new Test(); } } class Base{ static{ System.out.println("base static"); } public Base(){ System.out.println("base constructor"); } } // 执行结果 1.base static 2.test static 3.base constructor 4.test constructor
- 1.执行流程说明
- 1.首先找到程序入口
main
方法,但执行main
方法之前需要先加载Test
类- 2.加载
Test
类时发现Test
类继承Base
类,因此先加载Base
类- 3.加载
Base
类时发现Base
类存在static
块,因此先执行static
块输出base static
- 4.
Base
类加载完后然后加载Test
类发现Test
类也存在static
块,因此执行Test
类中的static
块输出test static
- 5.
Base
类和Test
类加载完成后,接着执行main
方法,调用子类构造器之前会先调用父类构造器,调用Test
的父类构造器Base()
输出base constructor
,最后再调用子类构造器输出test constructor
5.静态代码块和属性的执行流程
public class Test { Person person = new Person("Test"); static{ System.out.println("test static"); } public Test() { System.out.println("test constructor"); } public static void main(String[] args) { new MyClass(); } } class Person{ static{ System.out.println("person static"); } public Person(String str) { System.out.println("person "+str); } } class MyClass extends Test { Person person = new Person("MyClass"); static{ System.out.println("myclass static"); } public MyClass() { System.out.println("myclass constructor"); } } // 执行结果 1.test static 2.myclass static 3.person static 4.person Test 5.test constructor 6.person MyClass 7.myclass constructor
- 1.执行流程说明
- 1.首先找到程序入口
main
方法,但执行main
方法之前需要先加载Test
类- 2.加载
Test
类时发现Test
类存在static
块,因此先执行static
块输出test static
- 3.然后执行
new MyClass()
,执行此代码前先加载MyClass
类,发现MyClass
类继承Test
类,而是要先加载Test
类,Test
类已加载过- 4.因此直接加载
MyClass
类,发现MyClass
类有static
块,于是先执行static
块,输出myclass static
- 5.接着调用
MyClass
类的构造器生成对象,生成对象前需先初始化父类Test
的成员变量,执行Person person = new Person("Test")
代码- 6.此时
Person
类没有加载,因此需要先加载Person
类,加载时发现Person
类有static
块,所有先执行static
块,输出person static
- 7.然后执行
Person
构造器输出person Test
- 8.接着调用父类
Test
构造器完成父类Test
的初始化,输出test constructor
- 9.然后再初始化
MyClass
类成员变量,执行Person
构造器输出person MyClass
- 10.最后调用
MyClass
类构造器完成MyClass
类的初始化,输出myclass constructor
6.静态代码块的位置
public class Test { static{ System.out.println("test static 1"); } public static void main(String[] args) { } static{ System.out.println("test static 2"); } } test static 1 test static 2
- 1.
static
块可以出现类中的任何地方(只要不是方法内部,记住任何方法内部都不行),并且按照static
块的出现顺序执行
7.静态方法的执行流程
public class TestStatic { static int j=1; public static void main(String args[]){ int f2=f2(1); System.out.println("f2的最终值="+f2); } static int f2(int n){ n += j; j++; System.out.println("j="+j); if (n >= 5) return n; return n + f2(n); } } // 执行结果 1.j=2 2.j=3 3.j=4 4.f2的最终值=13
7.final
- 1.
修饰变量
- 1.常量,
final
变量一旦初始化后不能被修改,常量不存在默认值,一般情况下在创建时必须进行初始化- 2.只有一种情况例外:要求该变量必须在构造方法中被初始化,并且不能有空参数的构造方法,这样可让每个实例都有一个不同的变量,并且这个变量在每个实例中只会被初始化一次,于是这个变量在单个实例里就是常量
- 3.实例变量:必须在声明时或构造方法中初始化
- 4.局部变量:
final
可以修饰局部变量
(局部变量只能用final
修饰),可以在声明时或后续代码中初始化,但只能赋值一次- 5.静态变量:
final
通常和static
一起使用创建类常量,类常量必须在声明时或静态代码块中初始化- 6.
final
可以修饰引用,表现为堆地址不可修改,但对象内容可以修改
- 7.
final
和abstract
这两个关键字是互斥的,不能同时出现- 8.接口中声明的所有变量本身是
final
的- 2.修饰方法
- 1.将方法变成最终的方法,即可以被子类继承,但不可以被子类重写,防止该方法的内容被修改
- 2.
final
方法不能被重写,但可以被重载(即定义同名但参数不同的方法)- 3.
final
方法比非final
快一些,final
方法比非final
方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定,final
方法调用时使用的是invokespecial
指令- 3.
修饰类
- 1.该类不可以被继承
- 2.不可变类有很多好处,譬如它们的对象是只读的,可以在多线程环境下安全的共享,不用额外的同步开销等等
- 3.匿名类中所有变量都必须是
final
变量- 4.匿名类中如果访问外部方法的局部变量,那么这些局部变量必须是
final
或有效 final(effectively final)
,且匿名类中也不能修改其值,这是由Java
语言设计决定的,主要是为了保证数据的一致性
和线程安全性
- 5.匿名类可以修改外部类的实例变量,因为这些变量的生命周期与外部类实例绑定,而不是与方法的局部变量绑定
- 6.数据一致性
- 1.匿名类的生命周期可能比外部方法的生命周期更长,如果匿名类访问的外部方法的局部变量不是
final
,那么在外部方法执行完毕后,局部变量的值可能会被修改,导致匿名类中访问的值不一致- 2.通过将局部变量声明为
final
,可以确保匿名类中访问的值始终是外部方法执行时的值,避免数据不一致的问题- 7.线程安全性
- 1.匿名类可能在一个线程中运行,而外部方法可能在另一个线程中运行,如果局部变量不是
final
,多个线程可能会同时修改该变量,导致线程安全问题- 2.通过将局部变量声明为
final
,可以确保变量的值在匿名类中不会被修改,从而保证线程安全
1.final与finally的区别
- 1.
Java
中final
和finally
是两个完全不同的关键字,分别用于不同的场景
- 1.
final
是一个修饰符,可以用于变量、方法或类- 2.
finally
是异常处理机制的一部分,用于定义一段代码块,无论是否发生异常,这段代码都会执行- 3.
finally
块通常与try-catch
块一起使用,用于释放资源或执行清理操作(即使try
或catch
块中有return
语句,finally
块中的代码仍然会执行)
2.final与finalize()的区别
- 1.
Java
中final
和finalize()
是两个完全不同的概念
- 1.
final
是一个关键字,用于修饰变量、方法或类,表示不可变性、不可重写或不可继承- 2.
finalize()
是Object
类中定义的一个方法(protected
),用于在对象被垃圾回收器回收之前执行一些清理操作- 3.
finalize()
的执行时机不确定,因为垃圾回收的时间是由JVM
决定的,当垃圾回收器确定某个对象不再被引用时,会在回收该对象之前调用其finalize()
方法- 4.不推荐依赖
finalize()
来释放资源,因为它的调用是不可预测的,更好的方式是使用try-finally
或try-with-resources
来确保资源释放
3.final与静态绑定的关系
- 1.静态绑定(早期绑定):指在编译时就能确定方法或变量的调用,静态绑定适用于以下情况
- 1.调用
static
方法(static
方法是与类绑定的,而不是与对象绑定的,因此调用static
方法时,编译器可以直接根据类名确定调用哪个方法,而不需要关心对象的实际类型,且static
方法不能被重写,因此不存在多态性)- 2.调用
final
方法- 3.调用
private
方法(因为private
方法不能被重写)- 4.访问变量(包括实例变量和静态变量)
- 2.动态绑定(晚期绑定):指在运行时根据对象的实际类型来确定调用哪个方法
- 3.当一个方法被声明为
final
时,它不能被子类重写,因此编译器在编译时就可以确定调用的是哪个方法,而不需要在运行时动态查找- 4.
final
变量在编译时就会被绑定,因为它的值在初始化后不能被修改,编译器可以直接将final
变量的值嵌入到代码中,而不需要在运行时查找- 5.且
final
变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销- 6.如果一个类被声明为
final
,它不能被继承,因此所有对final
类的方法调用都是在编译时确定的,因为不存在子类重写方法的可能性- 7.静态绑定的优势
- 1.
性能更高
:由于方法或变量的调用在编译时就已经确定,不需要在运行时进行额外的查找,因此执行效率更高- 2.
安全性更高
:final
方法和类不能被重写或继承,避免了子类修改行为带来的潜在风险
4.final提高性能说明
public class Main { public static void main(String[] args) { String a = "xiaomeng2"; final String b = "xiaomeng"; String d = "xiaomeng"; String c = b + 2; String e = d + 2; System.out.println((a == c)); System.out.println((a == e)); } } 这段代码的输出结果是什么呢? 答案是: true 和 false 原因: 变量a指的是字符串常量池中的 xiaomeng2; 变量 b 是 final 修饰的,变量 b 的值在编译时候就已经确定了它的确定值,换句话说就是提前知道了变量 b 的内容到底是个啥,相当于一个编译期常量; 变量 c 是 b + 2得到的,由于 b 是一个常量,所以在使用 b 的时候直接相当于使用 b 的原始值(xiaomeng)来进行计算,所以 c 生成的也是一个常量,a 是常量,c 也是常量,都是 xiaomeng2 而 Java 中常量池中只生成唯一的一个 xiaomeng2 字符串,所以 a 和 c 是相等的! d 是指向常量池中 xiaomeng,但由于 d 不是 final 修饰,也就是说在使用 d 的时候不会提前知道 d 的值是什么,所以在计算 e 的时候就不一样了,e的话由于使用的是 d 的引用计算,变量d的访问却需要在运行时通过链接来进行,所以这种计算会在堆上生成 xiaomeng2 ,所以最终 e 指向的是堆上的 xiaomeng2 , 所以 a 和 e 不相等。 总得来说就是:a、c是常量池的xiaomeng2,e是堆上的xiaomeng2
- 1.
final
关键字在某些情况下可以提高性能,但这种性能提升通常是微小的,且依赖于具体的JVM
实现和优化策略
- 1.编译时优化:对
final
变量,编译器知道它的值在初始化后不会改变,因此可以进行一些优化
- 1.内联(
Inline
)优化:编译器可能会将final
变量的值直接嵌入到代码中,而不是在运行时从内存中读取- 2.常量折叠(
Constant Folding
):如果final
变量是一个编译时常量(final int x = 10
),编译器会直接使用它的值进行计算,而不需要在运行时读取变量
量- 2.方法调用优化:对于
final
方法,由于它不能被重写,编译器可以使用静态绑定来确定调用哪个方法,而不需要在运行时进行动态查找,这可以减少方法调用的开销- 3.类加载优化:对于
final
类,由于它不能被继承,JVM
在加载类时可以避免一些与继承相关的检查,从而加快类加载速度- 2.
final
变量某些情况下会被缓存
- 1.编译时常量:如果一个
final
变量是编译时常量(final int x = 10
),编译器会将其值直接嵌入到代码中,而不是在运行时从内存中读取,这种情况下可以认为final
变量的值被缓存
- 2.运行时常量:如果一个
final
变量的值在运行时才能确定(例通过构造函数或方法初始化),JVM
不会缓存它的值,每次访问该变量时仍然需要从内存中读取
- 3.
final
关键字的主要作用是确保不可变性、安全性和代码清晰性,而不是为了性能优化
5.final 变量和普通变量的区别?
- 1.
final
变量只能赋值一次,普通变量可以多次赋值- 2.
final
变量在多线程环境下是线程安全的
6.final 方法能否被重载
- 1.
final
方法不能被重写,但可以被重载(即定义同名但参数不同的方法)
8.abstract
/** * 车子类 */ public abstract class Car { Car(){ System.out.println("抽象方法无参构造函数"); } Car(String a){ System.out.println("抽象有参构造方法"); } public abstract void run(); public void stop(){ System.out.println("抽象类的实例方法"); } } /** * 自行车 */ class Bicycle extends Car{ @Override public void run() { System.out.println("跑"); } }
- 1.
abstract
是Java
中的一个关键字,用于定义抽象类或抽象方法,不能用于变量,其主要作用是提供一种机制,允许定义不完整的类或方法,强制子类去实现或扩展- 2.抽象类可以包含抽象方法和非抽象方法
- 3.抽象类不能实例化对象,但是抽象类有构造函数(因为子类创建对象时会先初始化父类的属性和方法),声明抽象类的唯一目的是为了将来对该类进行继承扩充
- 4.抽象类通常用于定义一种通用的模板或规范,强制子类实现特定的行为
- 5.一个类不能同时被
abstract
和final
修饰,如果一个类包含抽象方法,那么该类一定要声明为抽象类,否则将出现编译错误- 6.抽象方法是一种没有任何实现的方法(没有方法体,只有方法签名),该方法的具体实现由子类提供
- 7.抽象方法不能用
private
修饰,因为抽象方法必须被子类实现(覆写),而子类来不能访问private
权限,所以就会产生矛盾- 8.抽象方法不能被声明成
final
(抽象方法需要被继承重写)和static
(抽象方法没有方法体,只有被继承重写才有意义)- 9.抽象方法必须存在于抽象类或接口中,任何继承抽象类的子类必须实现父类的所有抽象方法,除非该子类也是抽象类
- 10.如果一个类包含若干个抽象方法,那么该类必须声明为抽象类。抽象类可以不包含抽象方法
- 11.构造方法不能被声明为
abstract
,因为构造方法的作用是初始化对象,而抽象类不能被实例化
1.抽象类和接口的区别
- 1.抽象类可以包含抽象方法和非抽象方法
- 2.接口只能包含抽象方法(
Java 8
之前)或默认方法(Java 8 之后
)- 3.抽象类可以有实例变量,而接口只能有常量(
public static final
)- 4.一个类只能继承一个抽象类,但可以实现多个接口
2.抽象类可以有构造方法
- 1.抽象类可以有构造方法,但不能直接实例化,构造方法通常用于初始化抽象类的状态或供子类调用
3.抽象类不能被 final 修饰
- 1.
final
类不能被继承,而抽象类必须被继承才能使用
4.抽象方法不能被 private 或 static 修饰
- 1.抽象方法需要被子类重写,而
private
和static
方法不能被子类重写
5.抽象类的作用
- 1.抽象类用于定义一种通用的模板或规范,强制子类实现特定的行为
- 2.可以将通用的逻辑放在抽象类中,减少代码重复
6.抽象类可以有 main 方法
- 1.抽象类可以有
main
方法,因为main
方法是静态的,与类的实例化无关
7.抽象类可以实现接口
- 1.抽象类可以实现接口,并且可以选择实现接口中的部分或全部方法
8.抽象类可以继承具体类
- 1.抽象类可以继承具体类,并且可以重写父类的方法或添加新的抽象方法
9.native
- 1.用来声明
本地方法
,该方法的实现由非Java
语言实现- 2.一般用于
java
与外环境交互或与操作系统交互- 3.
native
可以与所有其它的java
标识符连用,但是abstract
除外, 因为native
暗示这些方法是有代码实现的,只不过这些实现体是非java
的- 4.
native
方法的优点
- 1.可以访问底层系统功能
- 2.可以提高性能
- 3.可以复用现有代码
- 5.
native
方法的缺点
- 1.破坏了 Java 的跨平台性
- 2.增加了代码的复杂性
- 3.容易引入内存泄漏和崩溃问题
- 6.
native
方法通过JNI
(Java Native Interface
)实现,JNI
是Java
提供的一种机制,用于在Java
代码中调用本地代码或在本地代码中调用Java
代码- 7.
native
方法可以是实例方法,也可以是静态方法,且native
方法可以通过JNI
抛出Java
异常
10.synchronized
- 1.
synchronized
是Java
中的关键字,用于实现多线程同步,它可以确保多个线程在访问共享资源时,同一时刻只有一个线程能够执行特定的代码块或方法,从而避免数据竞争
和不一致性
问题- 2.当一个线程进入
synchronized
块或方法时会获得独占锁,这会阻止其他线程同时进入相同的synchronized
块或方法,从而确保了共享资源的互斥访问- 3.
synchronized
关键字在Java
中有三种主要的使用方式
- 1.修饰非静态方法
public synchronized void synchronizedInstanceMethod() { // 同步的代码块 }
- 1.当非静态方法使用
synchronized
关键字修饰时,会将该方法变成同步方法,相当于对当前实例对象(this
)加锁,this
作为对象监视器- 2.因此只有一个线程可同时执行该实例方法,以确保对该实例的互斥访问
- 3.当前类创建多个实例对象时,
synchronized
会独立控制每个实例对象的同步- 2.修饰静态方法
public static synchronized void synchronizedInstanceMethod() { // 同步的代码块 }
- 1.当静态方法使用
synchronized
关键字修饰时,会将该方法变为同步静态方法,相当于对当前类的Class对象
加锁,当前类的Class对象
作为对象监视器(锁定的是类的Class对象
,因此会阻止不同实例以及静态方法之间的并发执行,因为它们共享相同的Class对象
)- 2.因此只有一个线程可同时执行该静态方法,以确保对该类的互斥访问
- 3.当前类创建多个实例对象时,所以实例对应同一个静态方法,所以
synchronized
控制所有实例对象的同步- 3.修饰代码块
// 方式一:当使用 synchronized(class) 时,锁定的是整个类的对象,而不是实例对象 // 即无论多少实例对象存在,它们都会竞争同一个锁 Object lock = new Object(); synchronized (lock) { // 同步的代码块 } // 方式二:当使用 synchronized(this) 时,锁定的是当前实例对象(this) // 即同一实例的不同方法调用会相互排斥,但不同实例之间的方法调用不会相互排斥 synchronized (this) { // 同步的代码块 }
- 1.使用
synchronized
关键字创建同步代码块,这样可指定要加锁的对象,括号中的对象是对象监视器- 2.该方式允许更细粒度的控制,可选择对某个特定对象进行同步,而不是整个方法或类
- 4.
synchronized
只能用于方法(普通方法,静态方法)或代码块,不能用于
- 1.变量
- 2.构造方法:为构造方法本身是线程安全的(每个线程都会创建自己的对象实例)
- 3.接口方法:接口中的方法不能被
synchronized
修饰,因为接口方法默认是抽象的- 4.
Lambda
表达式- 5.
synchronized
底层原理(具体参考高并发 3.锁及其原理
)1.synchronized 通过监视器锁来实现线程同步 2.每个 Java 对象都有一个监视器锁 3.线程在获取了对象的监视器锁后,可以执行被修饰的代码 4.线程在释放了对象的监视器锁后,其他线程可以尝试获取监视器锁 5.Monitor 的结构: 1.Owner:当前持有锁的线程 2.EntryList:等待获取锁的线程队列 3.WaitSet:调用 wait() 方法后进入等待状态的线程队列 6.锁的状态: 1.无锁状态:对象未被任何线程锁定 2.偏向锁:只有一个线程访问锁时,JVM 会优化为偏向锁,避免 CAS 操作 1.轻量级锁:多个线程竞争锁时,JVM 会升级为轻量级锁,使用 CAS 操作 1.重量级锁:当竞争激烈时,JVM 会升级为重量级锁,线程进入阻塞状态 5.对象头与锁:Java 对象在内存中的布局分为三部分: 1.对象头(Header):包含锁信息、GC 信息等 2.实例数据(Instance Data):对象的字段数据 3.对齐填充(Padding):为了内存对齐而填充的字节 6.synchronized 的锁信息存储在对象头中,对象头的主要组成部分包括: 1.Mark Word:存储对象的哈希码、锁状态、GC 分代年龄等信息 1.Klass Pointer:指向对象所属类的元数据 7.Mark Word中锁的状态通过不同的标志位来表示 1.无锁状态:锁标志位为 01 2.偏向锁:锁标志位为 01,并记录偏向线程 ID 3.轻量级锁:锁标志位为 00 4.重量级锁:锁标志位为 10 8.锁的升级过程:JVM 对 synchronized 的锁进行了优化,锁的状态会根据竞争情况逐步升级 1.偏向锁 1.适用场景:只有一个线程访问锁 2.原理:在对象头中记录偏向线程 ID,后续该线程可以直接获取锁,无需 CAS 操作 3.优点:减少锁竞争的开销 2.轻量级锁 1.适用场景:多个线程交替访问锁,但没有竞争 2.原理:使用 CAS 操作将对象头的 Mark Word 替换为指向线程栈中锁记录的指针 3.优点:避免线程阻塞,减少上下文切换 3.重量级锁 1.适用场景:多个线程竞争锁 2.原理:将对象头的 Mark Word 替换为指向 Monitor 的指针,线程进入阻塞状态 3.优点:解决高并发竞争问题 9.synchronized 的底层实现 1.字节码层面:当方法或代码块被 synchronized 修饰时,JVM 会在字节码中插入 monitorenter 和 monitorexit 指令 1.monitorenter:获取对象的 Monitor 2.monitorexit:释放对象的 Monitor 2.JVM 层面 1.JVM 会根据锁的状态和竞争情况,选择合适的锁机制(偏向锁、轻量级锁、重量级锁)。 2.如果锁竞争激烈,JVM 会将锁升级为重量级锁,线程进入阻塞状态,等待锁释放 10.synchronized 的优化:为了提升 synchronized 的性能,JVM 引入了以下优化技术 1.锁消除(Lock Elimination):JVM 在编译时通过逃逸分析,判断锁对象是否只被一个线程访问。如果是,则消除锁 2.锁粗化(Lock Coarsening):如果多个连续的锁操作作用于同一个对象,JVM 会将多个锁合并为一个锁,减少锁的获取和释放次数 3.自适应自旋(Adaptive Spinning):在轻量级锁竞争时,线程会自旋等待锁释放。JVM 会根据历史数据动态调整自旋次数
- 1.
synchronized
底层是基于JVM
的指令和对象的监视器(monitor
)来实现的,每个Java
对象都与一个Monitor
相关联,Monitor
是JVM
实现的一种同步机制,用于控制线程对共享资源的访问- 2.
synchronized
可以修饰方法或者代码块,用来保证线程的同步和安全- 3.当一个线程要执行一个被
synchronized
修饰的方法或代码块时,需要先获取该方法或代码块所属对象的监视器- 4.如果获取成功则该线程可执行同步代码,并且监视器的计数器加一
- 5.如果获取失败则该线程就会阻塞,直到监视器被释放
- 6.当一个线程执行完同步代码后会释放监视器,并且监视器的计数器减一
- 7.如果计数器为零则说明没有线程持有该监视器,其他线程可竞争获取该监视器
- 8.
synchronized
修饰方法时,字节码层面会有一个ACC_SYNCHRONIZED
标志,用来表示该方法是同步的- 9.
synchronized
修饰代码块时,字节码层面会有monitorenter
和monitorexit
两个指令,分别用来进入和退出监视器- 6.
JDK
底层优化
- 1.
synchronized
在JDK1.6
之后进行了优化,引入了偏向锁,轻量级锁,自旋锁等概念,用来提高性能和减少阻塞开销- 2.偏向锁:会将锁定的对象与线程相关联,当一个线程获得锁时,会标记对象为已偏向该线程,以后再次进入同步块时,不需要竞争锁而是直接获得,可减少无竞争情况下的锁开销
- 3.轻量级锁:低竞争情况下,
JDK
使用轻量级锁来减小锁开销,轻量级锁采用自旋方式来等待锁的释放,而不是进入阻塞状态- 4.自旋锁:当轻量级锁尝试获取锁失败时,
JDK
可选择使用自旋锁,自旋锁不会使线程进入阻塞状态,而是一直尝试获取锁,通常在短时间内完成- 5.适应性自旋:
JDK
中的锁可根据历史性能数据来调整自旋等待的次数,以达到更好的性能
1.synchronized概念及其作用
- 1.
synchronized
是Java
中的关键字,用于实现多线程同步,主要作用是确保在同一时刻只有一个线程可以访问被synchronized
修饰的代码块或方法,以避免多线程之间的竞态条件和数据不一致问题
2.synchronized用法
- 1.同步非静态实例方法:
synchronized
修饰非静态方法,锁定的是实例对象- 2.同步静态方法:
synchronized
修饰静态方法,锁定的是类对象- 3.同步代码块:
synchronized
修饰代码块,可以手动指定锁对象,灵活控制同步范围
3.锁对象定义
- 1.锁对象是在
synchronized
代码块或方法中用于实现同步的对象- 2.对于同步实例方法,锁对象是实例对象本身(
this
)- 3.对于同步静态方法,锁对象是类的
Class
对象- 4.对于同步代码块,可以手动指定锁对象
4.synchronized的底层原理
- 1.
synchronized
的底层原理是基于Monitor
实现的,锁信息存储在对象头
中,JVM
会根据锁的竞争情况,选择偏向锁、轻量级锁或重量级锁
5.synchronized的锁升级过程
- 1.锁的升级过程包括:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
6.synchronized和 ReentrantLock 的区别是什么?
- 1.
synchronized
是JVM
实现的隐式锁,ReentrantLock
是JDK
实现的显式锁- 2.
ReentrantLock
提供了更灵活的锁机制,如可中断锁、公平锁、尝试获取锁等
7.synchronized是否可重入
- 1.
synchronized
是可重入的,同一个线程可以多次获取同一个锁
8.synchronized的性能
- 1.早期版本的
Java
中,synchronized
的性能较差,但在 JVM 的不断优化下,性能已经大幅提升
9.synchronized能否用于静态方法和实例方法同时使用
- 1.可以,静态方法的锁是类的
Class
对象,而实例方法的锁是当前对象实例,两者互不干扰
10.synchronized的锁消除和锁粗化
- 1.锁消除:
JVM
通过逃逸分析消除不必要的锁,锁消除是一种优化,它通过静态分析来检测某些锁是不必要的,然后将其删除,以提高程序的性能,通常发生在编译器级别- 2.锁粗化:
JVM
将多个连续的锁操作合并为一个锁,锁粗化是一种优化,它将多个连续的synchronized
块合并成一个更大的同步块,减少了锁操作的开销,这可以避免频繁地获取和释放锁,提高性能
11.自旋锁定义
- 1.自旋锁是一种轻量级锁,它在尝试获取锁时不会立即阻塞线程,而是会在循环中不断尝试获取锁,这可以减少线程阻塞和恢复的开销,但如果锁竞争激烈,自旋锁可能会浪费大量
CPU
时间
12.重入锁定义
- 1.重入锁是指当一个线程持有锁时,它可以多次进入被锁定的代码块或方法而不会被阻塞,
Java
中的synchronized
是可重入锁的一个例子,这意味着同一线程可以多次获取同一个锁,而不会造成死锁
13.偏向锁定义
- 1.偏向锁是一种优化,用于减少锁竞争的开销,它允许线程在没有竞争的情况下快速获得锁,而不必像重量级锁那样进入阻塞状态,只有在多线程竞争锁时,偏向锁才会升级为重量级锁
14.synchronized与volatile区别
- 1.
synchronized
用于实现多线程同步,确保线程之间的可见性和原子性- 2.
volatile
用于确保变量的可见性,但不能实现原子性
11.transient
- 1.
transient
是Java
中的一个关键字,用于修饰类的成员变量- 2.作用是阻止变量被序列化,当一个
对象
被序列化时,被transient
修饰的变量
不会被包含在序列化的数据中- 3.如果某些变量包含敏感信息(如密码、密钥等)或者是临时数据(如缓存、计算结果等),可以使用
transient
修饰避免序列化,如果某些变量引用了不可序列化
的对象(如线程、文件句柄
等),可以使用transient
修饰,避免序列化时抛出异常- 4.优点
- 1.保护敏感信息:避免敏感数据在序列化时被泄露
- 2.减少序列化数据量:避免序列化不必要的临时数据,减少序列化后的数据大小
- 3.避免序列化异常:避免序列化不可序列化的对象
- 5.缺点
- 1.数据丢失:被
transient
修饰的变量在反序列化后会丢失,需要手动初始化- 2.增加代码复杂性:需要在反序列化时手动恢复
transient
变量的值
1.transient 的作用
- 1.
transient
用于修饰类的成员变量,阻止变量被序列化
2.transient 的使用场景
- 1保护敏感信息(如密码)
- 2避免序列化临时数据(如缓存)
- 3避免序列化不可序列化的对象(如线程)
3.transient 变量在反序列化后的值
- 1.对于基本数据类型,默认值为
0、false
等- 2.对于引用类型,默认值为
null
4.恢复 transient 变量
- 1.通过实现
readObject
和writeObject
方法,在反序列化时手动恢复transient
变量的值
5.transient 能否用于静态变量
- 1.可以,但没有意义,静态变量属于类而不是对象,不会被序列化
12.volatile
- 1.
volatile
是Java
中的一个关键字,用于修饰变量,它的主要作用是确保多线程环境下的可见性
和有序性
,但不能保证原子性
- 1.可见性
- 1.普通变量在多线程环境下可能会被缓存到线程的本地内存中,导致其他线程看不到最新的值
- 2.
volatile
修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值,而且当成员变量发生变化时,会强制线程将变化值回写到共享内存- 3.这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值,
volatile
确保当一个线程修改了变量的值后,其他线程能够立即看到修改后的值- 2.有序性
- 1.普通变量可能会被
JVM
或CPU
进行指令重排序优化,导致程序行为不符合预期- 2.
volatile
禁止指令重排序,确保变量的读写操作按照代码的顺序执行- 3.不保证原子性:
volatile
不能保证复合操作(如i++
)的原子性,如果需要原子性操作应使用synchronized
或原子类- 2.
volatile
的应用场景
- 1.状态标记:假设一个线程调用
run()
方法,另一个线程调用stop()
方法,正常情况下当第一行中循环开始后调用第二行将active
设置为false
循环不会停止,但是以上代码中使用了volatile
修饰active
所以该循环会停止
- 2:单写多读:当一个变量只被一个线程修改,其他线程只读取时,可以使用
volatile
- 3.双重检查锁定:单例模式中可使用
volatile
确保实例的可见性
1.volatile的作用
- 1.
volatile
确保变量的可见性和有序性,但不能保证原子性
2.
volatile
和synchronized
的区别
- 1.
volatile
仅保证可见性和有序性,不保证原子性;synchronized
保证可见性、有序性和原子性- 2.
volatile
性能较高,适用于简单的同步场景;synchronized
性能较低,适用于复杂的同步场景
3.volatile 能否用于数组或对象
- 1.可以,但
volatile
仅保证数组引用或对象引用的可见性,不保证数组元素或对象字段的可见性
4.volatile的实现原理
- 1.
volatile
通过内存屏障(Memory Barrier
)实现可见性和有序性- 2.写操作:在写
volatile
变量后插入写屏障,确保写操作立即刷新到主内存- 3.读操作:在读
volatile
变量前插入读屏障,确保从主内存中读取最新值
5.volatile能否解决线程安全问题
- 1.不能完全解决,
volatile
仅适用于简单的同步场景,无法解决复合操作的线程安全问题
13.Java中的关键字
- 1.关键字是编程语言里事先定义好并赋予了特殊含义的单词
abstract assert boolean break byte case catch char class const continue default do double else enum extends final finally float for goto if implements import import instanceof int interface long new package private protected public return short static stricttfp super switch synchronized this throw throws transient try void volatile while - 语法说明
- 1.所有的关键字都是小写
- 2.程序中的标识符不能以关键字命名
- 3.
const
和goto
是保留关键字,虽然在Java
中还没有任何意义,但是在程序中不能作为自定义的标识符- 4.
true
,false
,null
不属于关键字,它们是一个单独标识类型,不能直接使用
1.常量和静态常量的区别
- 1.常量是通过
final
关键字定义的变量,其值不能被修改- 2.静态常量是通过
static final
关键字定义的变量,属于类而不是实例,且只有一份副本
2.常量池的作用是
- 1.常量池用于存储编译期生成的字面量和符号引用,支持动态链接和内存优化
3.字符串常量池是什么
- 1.字符串常量池是运行时常量池的一部分,专门用于存储字符串字面量
- 2.通过
String.intern()
方法可以将字符串添加到常量池中
4.String.intern() 方法的作用
- 1.
String.intern()
方法用于将字符串添加到字符串常量池中,并返回常量池中的引用
5.常量池在JVM中的位置
- 1.常量池存储在方法区(
Method Area
)中
6.常量池和字符串常量池的区别
- 1.常量池包含类、方法、字段等的符号引用和字面量
- 2.字符串常量池是常量池的一部分,专门用于存储字符串字面量
7.优化字符串的内存占用
- 1.使用
String.intern()
方法将字符串添加到字符串常量池中,减少重复字符串的内存占用