Java基础
Java 中的 final 关键字有哪些用法?
在 Java 中,final关键字有多重用途,首先,它用于声明变量为常量,一旦被赋值便不可修改,其次 final修饰方法和类表示它们是最终的,不能被子类重写和继承,从而增加了程序的稳定性和安全性。此外,当 final 用于修饰参数时,表示该参数是只读的,不可在方法内部被修改。最后 final 在多线程环境中使用可以确保对象的安全发布,使其在不需要额外同步措施的情况下,能够被其他线程安全的访问,通过使用final关键字,可以提高程序的可读性、可维护性,并增强程序的安全性。
GC 如何判断对象可以被回收?
GC 是 Java 中的垃圾回收器,有引用计数法、可达性分析法和弱引用、软引用和虚引用回收方式。
引用计数法会在对象被引用时计数+1,引用完成时计数-1,当引用计数为 0 时表示可以被回收,但是这种方式不能解决循环引用的问题,实际场景并不会使用。
可达性分析法是主流的垃圾回收算法,它通过对象是否可达来判断对象是否可以被回收,在 Java 中垃圾收集器会从一组名为 GC Roots 的对象开始,递归遍历所有与 GC Roots直接或间接相连的对象,将所有可达的标记为活动对象,没有被标记的对象可以被回收。
强引用、软引用、弱引用、虚引用
强引用(Strong Reference),当我们通过 new 关键字创建一个对象并赋值给一个变量时,该变量持有该对象的强引用,只有在该对象的所有强引用被释放时,对象才会被垃圾收集器回收。
软引用(Soft Reference),当一个对象被软引用所引用时,垃圾收集器会根据堆内存的使用情况决定是否回收这个对象,如果内存不足时,垃圾收集器会回收一些被软引用引用的对象,直到真正的内存不足了,才会回收所有软引用。
弱引用(Weak Reference),当一个对象被弱引用所引用时,垃圾收集器会在下一次垃圾回收时进行回收这个对象,无论堆内存是否足够,它都有可能被回收。
虚引用(Phantom Reference),虚引用任何时候都不会阻止垃圾收集器回收对象,虚引用主要用于跟踪对象被垃圾收集器回收的情况。在实际使用中,通常会与引用队列(ReferenceQueue)一起使用,当对象被回收时,会将其包装为虚引用并加入引用队列,以便进行后续处理。
Java类加载器是如何工作的?
Java 类加载器是Java运行时环境的一部分,负责加载Java类文件到JVM中,并生成对应的Class对象。类加载器通常按照一定的层次结构组织,每个类加载器负责加载特定位置或特定类型的类文件。类加载器的工作过程如下:
首先它会分三步进行,加载、连接、初始化
加载:类加载器首先会通过类的全限定名(Fully Qualified Name)查找对应的字节码文件(.class文件)
连接:连接阶段分为三个步骤:验证(Verification)、准备(Preparation)和解析(Resolution)
验证:确保被加载的类文件符合Java虚拟机规范
准备:为类的静态变量分配内存,并将其初始化为默认值。
解析:将类、接口、字段和方法的符号引用解析为直接引用。
初始化: 在初始化阶段,Java虚拟机会对类进行初始化,包括执行静态变量赋值和静态代码块的执行等操作。如果类具有父类,会先初始化父类。
类加载器按照一定的委托机制组织成树状结构。Java虚拟机自带了多个类加载器,其中最主要的是以下三种:
启动类加载器(Bootstrap Class Loader):是Java虚拟机的一部分,负责加载Java核心类库(如java.lang包下的类)。
它是虚拟机的内置类加载器,通常使用C++实现,不是Java类,因此在Java中无法直接获取对其的引用。
扩展类加载器(Extension Class Loader):也称为平台类加载器(Platform Class Loader),负责加载 Java 扩展类库(如 JRE/lib/ext 目录下的jar包)。
应用程序类加载器(Application Class Loader):也称为系统类加载器(System Class Loader),负责加载应用程序的类,即CLASSPATH所指定的目录和jar包。
B/S架构和C/S架构分别是什么?
B/S架构(Browser/Server Architecture)和C/S架构(Client/Server Architecture)是两种常见的软件系统架构模式,它们在应用程序的组织结构和交互方式上有所不同。
B/S架构(Browser/Server Architecture) :B/S架构是一种基于Web的架构模式,它将应用程序的逻辑分为两个部分:客户端(通常是Web浏览器)和服务器端。
客户端通过Web浏览器向服务器发出请求,服务器接收请求并处理,并将结果返回给客户端,客户端再将结果呈现给用户。
B/S架构的优点包括跨平台性好、易于维护和升级、部署简单等。
C/S架构(Client/Server Architecture) :C/S架构是一种传统的架构模式,它将应用程序的逻辑分为两个部分:客户端和服务器端。
客户端是一个独立的程序,通常是安装在用户计算机上的软件,它向服务器发出请求并处理服务器返回的结果。
C/S架构的优点包括灵活性高、响应速度快、数据处理能力强等。
B/S架构适用于基于Web的应用程序,而C/S架构适用于需要更多客户端控制和数据处理能力的应用程序。
Java中的继承是单继承还是多继承?
在Java中,类的继承是单继承的,即一个类只能直接继承自一个父类。这意味着一个子类只能有一个直接的父类。这种设计是为了避免多继承可能带来的复杂性和潜在的问题,如菱形继承问题(Diamond Problem)。
在Java中,一个类可以继承自另一个类,并且可以实现多个接口。这种方式被称为单继承多实现。通过接口,Java允许类在不同的层次上实现多个类型的行为,同时避免了多继承的相关问题。
ArrayList和LinkedList的区别是什么?
ArrayList和LinkedList都是Java中常用的集合类,它们都实现了List接口,但它们的内部实现和特点有所不同。下面是它们的主要区别:
-
底层数据结构:
ArrayList 底层基于数组实现,它通过动态扩展数组的大小来存储元素。
LinkedList 底层基于双向链表实现,每个元素都包含对前一个和后一个元素的引用。
-
访问效率:
ArrayList 支持随机访问,因为它通过索引可以直接访问数组中的元素,时间复杂度为O(1)。 LinkedList 不支持随机访问,因为它需要从头或尾部开始遍历链表才能访问到指定位置的元素,时间复杂度为O(n)。
-
插入和删除效率:
ArrayList 在中间位置的插入和删除操作需要移动元素,时间复杂度为O(n)。LinkedList 在中间位置的插入和删除操作效率较高,因为只需要改变相邻元素的引用,时间复杂度为O(1)。
-
空间占用:
ArrayList 在添加新元素时可能需要重新分配内存,因为数组大小是固定的,当元素数量超过当前容量时,需要将数组扩展到更大的大小。
LinkedList 在添加新元素时不需要重新分配内存,因为它使用链表结构,每个元素在内存中都是独立分配的。
综上所述,如果需要频繁进行随机访问操作,应该选择 ArrayList。如果需要频繁进行插入和删除操作,应该选择LinkedList
在Java中实现对象克隆(Clone)有两种方式:
-
实现Cloneable接口并重写clone()方法:
首先,要确保被克隆的类实现了Cloneable接口,这是一个标记接口,表示该类的实例可以被克隆。 其次,重写 clone() 方法,该方法是Object类中的一个 protected 方法。在子类中覆盖该方法,使其能够返回当前对象的一个副本。
在 clone() 方法中,通常使用 super.clone() 来获得对象的浅拷贝(Shallow Copy),然后再进行必要的深拷贝(Deep Copy)操作,以确保克隆出来的对象的独立性。
-
使用序列化实现深拷贝:
通过对象的序列化和反序列化来实现对象的深拷贝。这种方式相对简单,但可能会对性能产生一定的影响。
什么是字节码?采用字节码的好处是什么?
字节码是一种中间代码,字节码是一种在编译和执行之间的中间表示形式,它可以被解释器或即时编译器(JIT)在运行时转换为机器码执行。Java源代码经过编译生成字节码文件(.class),可以在不同平台上执行。
采用字节码的好处包括:
- 跨平台性: 字节码是独立于平台的中间表示形式,因此可以在不同操作系统和硬件架构上运行,只要有相应的虚拟机实现即可。
- 安全性: 字节码可以在运行时受到控制,虚拟机可以对其进行检查以确保其安全性
- 代码隐藏: 字节码可以隐藏源代码的逻辑,这对于保护知识产权和防止反编译是很有用的。
- 动态性: 字节码的中间表示形式使得可以在运行时进行动态生成和修改,这为实现动态语言和一些高级功能提供了便利。
字节码提供了一种灵活、跨平台的方式来表示程序,同时还能够在运行时进行优化和控制,使得其具有更好的性能和安全性。
标识符的命名规则是什么?
- 必须以字母、下划线(_)或美元符($)开始。
- 后续字符可以是字母、数字、下划线(_)或美元符($)的任意组合。
- 标识符不能是Java关键字或保留字。
排序都有哪几种方法?
排序算法有很多种,每种都有其独特的优劣点和适用场景。以下是一些常见的排序算法:
- 冒泡排序(Bubble Sort) : 依次比较相邻的元素,如果顺序错误则交换它们,重复这个过程直到排序完成。
- 选择排序(Selection Sort) : 找到未排序部分的最小(或最大)元素,将其放在已排序部分的末尾,重复这个过程直到排序完成。
- 插入排序(Insertion Sort) : 将每个元素插入到已排序部分的正确位置,形成一个有序序列。
什么是双亲委托模型?
双亲委托模型是Java类加载器的一种工作机制。在这种模型中,类加载器之间形成了父子关系,每个类加载器在加载类时都会先委托给其父加载器去尝试加载。只有当父加载器无法加载时,子加载器才会尝试自己加载。这样的层次结构一直持续到顶层的启动类加载器。
双亲委托模型的好处在于保证了类的一致性和安全性。通过这种方式,Java核心类库等关键类都是由启动类加载器加载的,而不是由应用程序自定义的类加载器加载。这样可以防止应用程序在不同的类加载器中加载同名类导致的混乱和冲突。另外,这种模型还能够防止恶意代码替换核心类库,提高了Java程序的安全性。
总的来说,双亲委托模型通过层次化的类加载器结构,实现了对类加载过程的规范和管理,保证了Java程序的稳定性、一致性和安全性。
Java中有没有指针?
Java是一种高级编程语言,它在语言级别上不直接支持指针。指针是一种直接引用内存地址的机制,在低级语言(如C和C++)中常用于对内存的直接操作,但也容易引发内存泄漏、越界访问等问题。
在Java中,虽然没有直接的指针概念,但是有引用(Reference)的概念。在Java中,对象是通过引用来进行操作的,而不是直接通过内存地址。引用指向对象在堆内存中的实际位置,但无法直接访问或操作这个地址。Java中的引用机制使得内存管理更加安全和方便,避免了许多与指针相关的问题。
Java中的异常体系包括哪些?
Java中的异常体系主要包括以下几种类型的异常:
- 检查异常(Checked Exception) :继承自
java.lang.Exception类的异常,编译器要求必须进行处理或声明。例如:IOException、SQLException等 - 运行时异常(Unchecked Exception) :继承自
java.lang.RuntimeException类的异常,编译器不会强制要求进行处理或声明。通常是由程序错误导致的,应该尽可能避免。例如:NullPointerException、ArrayIndexOutOfBoundsException等。 - 错误(Error) :继承自
java.lang.Error类的严重问题,通常表示虚拟机无法处理的错误情况。与异常不同,错误不应该被捕获和处理,而是应该由虚拟机或程序终止处理。例如:OutOfMemoryError、StackOverflowError等。
String、StringBuffer、StringBuilder有什么区别?
String、StringBuffer和StringBuilder是Java中用于处理字符串的类,它们之间有以下区别:
-
可变性:
String是不可变的(immutable),一旦创建了String对象,其内容不能被修改。StringBuffer和StringBuilder是可变的(mutable),可以通过方法改变其内容。
-
线程安全性:
String是线程安全的,因为它是不可变的,多个线程可以同时访问一个String对象而不会产生线程安全问题。StringBuffer是线程安全的,它的方法都是同步的,因此可以安全地在多线程环境中使用。StringBuilder不是线程安全的,它的方法没有同步,所以在多线程环境中使用可能会出现线程安全问题。
-
性能:
- 由于
String是不可变的,对String进行频繁的字符串拼接操作会产生大量的临时对象,降低了性能。 StringBuffer和StringBuilder是可变的,它们提供了修改字符串内容的方法,因此在进行频繁的字符串拼接操作时性能更好。StringBuilder相比StringBuffer在单线程环境下性能更好,因为不需要同步操作。
- 由于
综上所述,String适用于不需要修改字符串内容的场景,StringBuffer适用于多线程环境下需要修改字符串内容的场景,StringBuilder适用于单线程环境下需要修改字符串内容的场景。
equals与==的区别是什么?
equals()方法和==运算符在Java中用于比较对象的值和引用,它们之间的区别如下:
-
equals()方法:equals()方法是一个在Object类中定义的方法,子类可以选择性地重写它来实现自定义的对象相等性比较逻辑。- 默认情况下,
equals()方法在Object类中的实现与==运算符的行为相同,即比较两个对象的引用是否相同。 - 如果一个类重写了
equals()方法,通常会根据对象的内容来决定两个对象是否相等。这样的相等性比较通常会重写hashCode()方法以保持一致性。
-
==运算符:==运算符用于比较两个对象的引用是否相同,即它们是否指向内存中的同一个对象。- 如果比较的是基本数据类型,
==比较的是它们的值;如果比较的是引用类型,==比较的是它们在内存中的地址。
总的来说,equals()方法用于比较对象的内容是否相等,而==运算符用于比较对象的引用是否相同。在大多数情况下,应该使用equals()方法来比较对象的内容,而不是使用==运算符。
什么是JDK?什么是JRE?
JDK(Java Development Kit)是Java开发工具包,JDK包括了Java编译器(javac)、Java运行时环境(JRE)、Java文档生成工具(Javadoc)等一系列工具和库。开发人员可以使用JDK来编写、编译和调试Java程序。
JRE(Java Runtime Environment)是Java运行时环境,JRE包括Java虚拟机(JVM)和Java类库等运行时必需的组件,但不包括开发工具。如果用户想要运行Java程序,但不需要进行开发,只需安装JRE即可。
hashCode与equals有什么关系?
hashCode()方法和equals()方法是Java中Object类的两个重要方法,它们之间有以下关系:
-
一致性:
- 如果两个对象根据
equals()方法比较相等(即a.equals(b)返回true),那么它们的hashCode()方法应返回相同的值。 - 如果两个对象根据
equals()方法比较不相等(即a.equals(b)返回false),那么它们的hashCode()方法不要求返回不同的值,但如果两个对象的hashCode()方法返回不同的值,可以提高哈希表的性能。
- 如果两个对象根据
-
重写规则:
- 如果一个类重写了
equals()方法,通常也应该重写hashCode()方法,以确保满足一致性原则。 - 在重写
hashCode()方法时,通常会根据对象的内容计算哈希码,以确保相等的对象具有相同的哈希码。
- 如果一个类重写了
-
用于哈希表:
hashCode()方法返回对象的哈希码,用于在哈希表等数据结构中确定对象的存储位置。equals()方法用于比较两个对象的内容是否相等。
总的来说,hashCode()方法和equals()方法是一对相关联的方法,它们一起用于确保对象在哈希表等数据结构中的正确行为。在重写equals()方法时,通常也需要重写hashCode()方法以保持一致性。
面向对象和面向过程的区别是什么?
面向对象编程(OOP)和面向过程编程(POP)是两种不同的编程范式,它们之间的区别主要体现在以下几个方面:
-
思维方式:
- 面向对象编程强调的是对象的概念,将问题分解为一系列的对象,通过对象之间的交互来解决问题,更加注重于对象的行为和状态。
- 面向过程编程则更加注重于过程或者函数,将问题分解为一系列的步骤,通过调用函数或者过程来解决问题,更加注重于函数的顺序和逻辑。
-
代码组织方式:
- 在面向对象编程中,代码被组织成对象的集合,每个对象都有自己的属性和方法,对象之间通过消息传递来进行交互。
- 在面向过程编程中,代码被组织成一系列的函数或者过程,每个函数或者过程都是一组操作的集合,通过函数调用来完成任务。
-
可维护性和扩展性:
- 面向对象编程通常具有更好的可维护性和扩展性,因为对象可以隐藏内部实现细节,只提供公共接口供其他对象使用,降低了代码的耦合性。
- 面向过程编程可能会导致代码之间的耦合度较高,如果需要修改某个过程,可能需要修改多个地方的代码。
总的来说,面向对象编程更加灵活、可维护和扩展,适用于复杂的系统和大型项目;而面向过程编程更加直观、简单,适用于小型项目或者一些简单的任务。在实际应用中,可以根据具体的需求和情况选择合适的编程范式。
Java 多线程并发
Thread 和 Runnable 的区别?
Thread 和 Runable 是两种启动线程的方式,它们的区别在于实现方式和对类的继承影响。
Thread:实现方式是继承Thread类,缺点:因为java只能单继承,所以如果一个类已经继承了其他类就不能再继承Thread类,从而影响了扩展性。
Runable:实现 Runable 接口,优点:通过实现接口方式,可以避免 java 单继承限制,从而增加了扩展性。使用场景:当希望类具有更多的灵活性和扩展性,或者当前类已经继承其他类,应该使用Runnable。
通过实现 Runnable 接口,可以将同一个 Runnable 实例传递给多个 Thread 实例。从而实现多个线程共享同一个任务的能力,而 Thread 继承时是做不到的。
Java 线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?
Java 线程锁机制通过对象的监视器(Monitor)来实现,这与 Synchronized 关键字密切相关。锁机制主要包括偏向锁、轻量级锁和重量级锁,这些锁根据竞争的激烈程度进行升级。
偏向锁
特点:偏向锁是为了减少同一个线程多次获取锁的开销。当一个线程获得锁时,锁会偏向该线程,之后该线程在进入和退出同步块时不需要进行任何同步操作。
适用场景:适用于线程之间没有锁竞争的场景。
轻量级锁
特点:轻量级锁使用 CAS(Compare-And-Swap)操作来减少传统重量级锁使用操作系统互斥量带来的性能消耗。当偏向锁被其他线程竞争时,会升级为轻量级锁。
适用场景:适用于锁竞争不激烈的场景。
重量级锁
特点:重量级锁使用操作系统的互斥量(Mutex)进行同步,线程会被阻塞,直到获得锁。它的性能开销较大。
使用场景:适用于锁竞争激烈的场景。
锁的升级过程:
- 偏向锁:初始状态下,偏向锁会偏向第一个访问的线程,减少获取锁的开销。
- 轻量级锁:当有第二个线程尝试竞争偏向锁时,偏向锁会升级为轻量级锁,采用 CAS 操作进行锁的获取和释放。
- 重量级锁:如果轻量级锁的竞争仍然激烈(多次 CAS 操作失败),轻量级锁会升级为重量级锁,线程会被阻塞,直到获得锁。
锁的降级
锁一旦升级,不会主动降级。升级是为了提高在高竞争场景下的性能,而降级可能导致性能下降。
Volatile和Synchronized有什么区别?Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加Volatile?
Volatile 和 Synchronized 的区别?
在 Java 中 volatile 和 synchronized 是两种不同的机制,用于处理并发编程中的可见性和原子性问题。
Volatile:
- 可见性:当一个变量被声明为 volatile 时,任何线程对该变量的修改都会立即被刷新到主存中,从而使其他线程可以立即看到该修改。
- 禁止指令重排:编译器和处理器在对 volatile 变量进行读写操作时,不会对这些操作进行重排序,也就是说,对一个 volatile 变量的写操作在发生前的所有操作一定已经执行完毕,对于读操作只是保证有序性。
- 不能保证原子性:不能保证对该变量操作的原子性。例如:i++ 不是原子操作,使用 Volatile 不能保证线程安全。
Synchronized:
实现线程之间的互斥和同步,确保同一时刻只有一个线程能够执行同步代码块,从而保证线程的安全。
- 互斥性:确保同一时刻只有一个线程可以执行同步方法和同步代码块。
- 可见性:当一个线程退出同步代码块时,保证了对共享变量的修改对后续进入该代码块的其他线程是可见的。
- 原子性:通过互斥机制,synchronized 能够确保对共享变量操作的原子性。
Volatile 能否保证线程安全?
volatile 只能保证变量的可见性,不能保证操作的原子性,因此,对于涉及到复合操作(如自增、自减)的变量,仅靠 volatile 是不能保证线程安全的。
DCL(Double-Check Locking)单例模式中的 volatile
在 DCL 单例模式中,volatile 关键字用于防止指令重排序,从而确保线程安全。
主要是以下两点:
- 防止指令重排序:在没有 volatile 的情况下,编译器和 CPU 可能会对于初始化操作进行重排序(instance = new Singleton()),从而导致其他线程在 instance 没有初始化就看到对象。
- 确保可见性:完成初始化之后,其他线程能够立即看到最新的值。
线程池的复用原理?
线程池中的线程复用原理是通过维护一个线程队列来管理和重复使用一组预先创建的线程,从而减少线程创建和销毁的开销。以下是工作流程。
- 初始化线程池:线程池在创建时,会预先启动一定数量的线程,这些线程会进入等待状态,等待任务的到来。
- 提交任务:当一个新任务被提交到线程池时,线程池会将任务放入一个任务队列中。
- 线程获取任务:线程池中的空闲线程会不断检查任务队列,如果发现有任务,就从队列中取出一个任务来执行,如果队列为空,线程会继续等待。
- 执行任务:线程从任务队列获取到任务后,立即开始执行任务代码,执行完毕后,不会销毁线程,而是返回线程池等待下一个任务。这样就实现了线程复用,避免了频繁的线程创建和销毁,提升了性能。
- 动态调整:根据配置可以动态调整线程的数量,例如当任务增加时,线程池可以创建更多的线程来处理任务,当任务量较少时,可以回收空闲线程来节省资源。
并发编程的三大特性?
并发编程的三大特性是原子性、可见性和有序性。
- 原子性:指一个操作要么全部执行完毕,要么不执行。对于原子操作,线程之间不会产生中断。
- 可见性:指的是一个线程对共享变量的修改,能够立即被其他线程看到,如果没有可见性的保证,线程A对变量的修改,线程B可能永远看不到。
- 有序性:指程序的执行顺序按照代码的先后顺序执行,由于编译器和处理器的优化,程序的实际执行顺序可能与代码的书写顺序不同。
ThreadLocal 内存泄露的原因?如何避免?
ThreadLocal 是java中提供的一种机制,用于在多线程环境中为每个线程提供独立的变量副本。如果不正确的使用 ThreadLocal 可能会导致内存泄漏。
内存泄漏的原因
1. 线程池和线程生命周期
当使用线程池时,线程会被重复使用,如果一个线程持有一个 ThreadLocal变量,而这个变量没有被正确清除,那么该变量的引用会一直存在,导致内存无法被回收。
即使线程结束,引用的对象也可能无法被回收,因为线程池中的线程是复用线程,线程生命周期并没有结束。
2. ThreadLocalMap 的设计
ThreadLocal 使用 ThreadLocalMap 来存储数据。这个ThreadLocalMap 是线程对象的一个属性。
ThreadMap 使用 ThreadLocal 的弱引用作为键,但是值是强引用。如果 ThreadLocal 本身被垃圾回收了,键变成了 null 但是值仍然存在,这就导致了内存泄漏,因为值无法被回收。
如何避免内存泄露
1. 显式清除
当不再需要 ThreadLocal 变量时,显式地调用 ThreadLocal.remove() 方法清除变量,防止内存泄露。
2. 使用弱引用包装值
使用自定义的 ThreadLocal 实现,使其值也使用弱引用,这样即使 ThreadLocal 被回收,其值也可以被及时回收。
3. 使用 InheritableThreadLocal 慎重
InheritableThreadLocal 会导致子线程继承父线程的 ThreadLocal 值,如果不注意管理,容易导致内存泄露。要避免在频繁创建和销毁线程的情况下使用它。
4. 避免在线程池中滥用 ThreadLocal
尽量减少在线程池中使用 ThreadLocal,尤其线程池。因为线程池中的线程不会被频繁销毁,而 ThreadLocal 变量可能会长时间占用内存。
线程的生命周期?线程有几种状态?
当使用 Thread 线程时,生命周期是以下。
- 新建
创建 Thread
- 就绪
当线程启动后,会等待CPU 调度,线程处于就绪状态。
这个状态包括(CPU调度和CPU调度的执行时间)
- 运行
当前线程获取 CPU 时间片开始执行时,线程会处于运行状态。(只是线程处于运行状态,任务并没有被执行)
- 阻塞
当线程等待获取一个排他锁(也叫互斥锁)时,线程处于阻塞状态,等待其他线程唤醒。
- 执行
线程唤醒后,获取锁,进行执行任务。执行完毕后,线程无法再次启动。
有A,B,C三个线程,如何保证三个线程同时执行?如何在并发情况下保证三个线程依次执行?如何保证三个线程有序交错进行?
保证三个线程同时执行
为了保证三个线程尽可能同时启动,我们可以使用 CountDownLatch 来确保三个线程都准备好后同时开始执行。
如何保证三个线程依次执行
为了保证三个线程依次执行,我们可以使用 Thread.join() 方法。join() 方法等待调用它的线程完成后才继续执行。(缺点:会阻塞主线程)
如何保证三个线程有序交错进行?
不能理解
谈谈你对AQS的理解。AQS如何实现可重入锁?
对 AQS 的理解
AQS 是java 中用于构建锁和同步器的基础框架,AQS 提供了一种是实现先进先出等待队列的机制,允许实现者以多种方式管理线程的同步状态。它是构建高效、可扩展的同步器。
AQS 的主要特点包括:
- 同步状态:AQS 使用一个 volatile 的 int 变量来表示同步状态。子类通过继承 AQS 并实现其方法来操作这个同步状态。
- FIFO 等待队列:AQS 维护一个等待线程的FIFO 队列,使用一个链表来保存等待的线程。
- 独占模式和共享模式:AQS 支持两种操作模式:独占模式和共享模式。在独占模式下,一个线程独占资源,在共享模式下,多个线程可以共享资源。
- 模板方法模式:AQS 使用模板方法模式,子类需要实现一些关键方法来定义特定的同步行为,例如:trycquire,tryRelease, tryAcquireShared, tryReleaseShared 等。
AQS 如何实现可重入锁
ReentrantLock 是基于 AQS 实现的可重入锁。可重入锁允许线程多次获取同一个锁,而不会导致死锁。实现这种行为的关键在于 AQS 如何管理锁的获取和释放。
主要原理
同步状态表示锁的持有情况:
当同步状态为 0 时,表示锁未被持有。
当同步状态大于 0 时,表示锁已被持有,状态值表示持有锁的次数。
当前持有锁的线程:
AQS 需要记录当前持有锁的线程,当一个线程再次请求锁时,检查是否与当前持有锁的线程相同。
具体实现步骤
获取锁(tryAcquire 方法)
检查同步状态是否为 0,如果是,则尝试获取锁并设置当前持有锁的线程。
如果同步状态不为 0,检查当前线程是否为持有锁的线程,如果是,则增加同步状态的值(重入次数)。
释放锁(tryRelease 方法):
检查当前线程是否为持有锁的线程。
减少同步状态的值(减少重入次数),如果同步状态减到 0,则完全释放锁并清除持有锁的线程。