面向对象特性
方法重载与重写的区别
普通类和抽象类
接口和抽象类有什么区别?
8.Java的基本数据类型、包装类及自动装箱拆箱
-
byte,8位
-
char,16位
-
short,16位
-
int,32位
简单 类型 boolean byte char short Int long float double 二进 制位 数 1 8 16 16 32 64 32 64 包装 类 Boolean Byte Character Short Integer Long Float Double
自动装箱和拆箱
-
装箱:将基础类型转化为包装类型。
-
拆箱:将包装类型转化为基础类型。
-
当基础类型与它们的包装类有如下几种情况时,编译器会自动帮我们进行装箱或拆箱:
- 赋值操作(装箱或拆箱)
- 进行加减乘除混合运算 (拆箱)
- 进行>,<,==比较运算(拆箱)
- 调用equals进行比较(装箱)
- ArrayList、HashMap等集合类添加基础类型数据时(装箱)
示例代码:
ini
复制代码
Integer x = 1; // 装箱 调⽤ Integer.valueOf(1)
int y = x; // 拆箱 调⽤了 X.intValue()
反射
深浅拷贝
浅拷贝是指只会拷贝一个基本数据类型的值,以及实例对象的引用地址。浅拷贝出来的对象指向同一地址
深拷贝是指不仅会拷贝一个基本数据类型的值,也会复制实例对象的引用地址指向的对象,深拷贝出来的对象,并不指向同一个对象。
反射
在Java中,反射是指在运行时检查和操作类、接口、字段、方法的能力。通过反射,可以在运行时获取类的信息,创建类的实例,调用类的方法,访问和修改类的字段。
获取类的方式:对象.getClass(), 类.class(),Class.forname()
2、泛型的作用
Java中的泛型可以让我们在编写代码时更加灵活和安全。具体来说,泛型的作用包括:
- 类型安全:泛型可以让我们在编译期就检查类型的正确性,避免了运行时出现类型转换错误的情况。
- 代码重用:通过使用泛型,我们可以编写通用的代码,可以用于不同类型的数据。
- 简化代码:泛型可以减少代码中的类型转换,使代码更加简洁、易读。
- 集合框架:Java中的集合框架都是使用泛型实现的,可以让我们在使用集合时更加方便和安全。
总之,泛型是Java中非常重要的一个特性,可以让我们编写出更加灵活、安全、简洁的代码。
代码示例:
public class Stack<T> {
private List<T> elements = new ArrayList<T>();
public void push(T element) {
elements.add(element);
}
public T pop() {
if (elements.isEmpty()) {
throw new NoSuchElementException("Stack is empty");
}
return elements.remove(elements.size() - 1);
}
public boolean isEmpty() {
return elements.isEmpty();
}
}
Stack<Integer> intStack = new Stack<Integer>();
intStack.push(1);
intStack.push(2);
intStack.push(3);
System.out.println(intStack.pop()); // 输出 3
Stack<String> stringStack = new Stack<String>();
stringStack.push("hello");
stringStack.push("world");
System.out.println(stringStack.pop()); // 输出 "world"
在Java中什么是泛型擦除
Java中的泛型擦除是指在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型编写的代码在运行时会被转换成对应的非泛型代码,因此在使用泛型时需要注意这一点。
自动拆装箱
如果引用类型在-128-127之间,比较的是同一个地址,不在的话比较的是两个地址
Integer.ValueOf() Integer对象.intValue()
Object类中的方法
Java的Object类中有以下方法:
clone():创建并返回此对象的一个副本。
equals(Object obj):判断该对象是否与另一个对象相等。
finalize():垃圾回收器在回收对象之前调用该方法。
getClass():返回此对象的运行时类。
hashCode():返回此对象的哈希码值。
notify():唤醒在此对象监视器上等待的单个线程。
notifyAll():唤醒在此对象监视器上等待的所有线程。
toString():返回该对象的字符串表示形式。
wait():导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法。
这些方法分别用于对象的克隆、比较、垃圾回收、获取运行时类、哈希码、线程同步等操作。其中,equals()和hashCode()方法在Java中被广泛使用,用于判断两个对象是否相等。toString()方法也很常用,用于将对象转换成字符串形式。wait()、notify()和notifyAll()方法则用于线程同步。
Socket?
Socket是一种通信机制,用于在不同主机之间进行数据传输。在Socket通信中,一个主机作为服务器,另一个主机作为客户端,通过Socket建立连接,进行数据传输。
在Java中,可以使用Java Socket API来实现Socket通信。Java Socket API提供了两种Socket类型:ServerSocket和Socket。ServerSocket用于创建服务器端Socket,Socket用于创建客户端Socket。在Socket通信中,服务器端Socket监听一个端口,等待客户端Socket的连接请求;客户端Socket通过服务器端Socket的IP地址和端口号发起连接请求,连接成功后,双方就可以通过Socket进行数据传输了。
Java Socket API还提供了一组基本的输入输出流,用于在Socket之间传输数据。通过输入输出流,可以将数据写入Socket的输出流中,或从Socket的输入流中读取数据。Java Socket API还提供了一些高级功能,如非阻塞I/O、多路复用等,可以提高Socket通信的性能和可靠性。
需要注意的是,Socket通信是一种基于TCP协议的可靠的数据传输机制,它可以保证数据的可靠性和一致性。但是,Socket通信的缺点是需要建立连接,因此在连接建立和断开时会有一定的开销。对于实时通信等对延迟要求较高的场景,可能不适合使用Socket通信。
Websocket?
WebSocket是一种在单个TCP连接上进行全双工通信的协议。它允许在客户端和服务器之间进行实时的双向数据传输。相比传统的HTTP协议,WebSocket具有更低的延迟和更高的带宽利用率,因此被广泛应用于实时通信、在线游戏、视频流等场景。
WebSocket协议是基于HTTP协议的,它通过HTTP协议的握手过程建立连接,然后使用自定义的协议进行数据传输。WebSocket协议的握手过程类似于HTTP协议,但是需要在请求头中添加一个Upgrade字段,表示要升级到WebSocket协议。服务器在接收到这个请求后,会返回一个包含协议版本号的响应头,表示连接已经成功建立。之后,客户端和服务器就可以通过WebSocket协议进行实时的双向数据传输了。
在Java中,可以使用Spring框架提供的WebSocket支持来实现WebSocket协议的应用程序。Spring WebSocket提供了一个简单的编程模型和一组API,可以轻松地构建实时通信应用程序。同时,Spring WebSocket还提供了与Spring Security集成的支持,可以保证WebSocket连接的安全性。
异常体系?
Java的异常体系是一个非常重要的概念,它是Java语言中用于处理程序错误的机制。Java中的异常被分为两类:受检异常和非受检异常。
- 受检异常
受检异常是在编译时就能够被检测到的异常,需要在方法中声明并处理。受检异常通常表示一些外部因素的错误,如I/O错误、网络错误等。受检异常必须在方法签名中声明,以便调用者能够知道方法可能抛出哪些异常,从而进行相应的处理。受检异常都是Exception类或其子类的对象。
- 非受检异常
非受检异常是在运行时才能够被检测到的异常,不需要在方法中声明,但需要在程序中进行处理。非受检异常通常表示程序内部的错误,如空指针异常、数组下标越界异常等。非受检异常都是RuntimeException类或其子类的对象。
Java中的异常体系由Throwable类作为根类,它是所有异常类的父类。Throwable类派生出两个子类:Exception和Error。
- Exception
Exception是受检异常的父类,它派生出许多子类,如IOException、ClassNotFoundException等。Exception类及其子类表示程序中的一些可预见的异常情况,需要在程序中进行处理。
- Error
Error是非受检异常的父类,它派生出许多子类,如OutOfMemoryError、StackOverflowError等。Error类及其子类表示程序中的一些不可恢复的错误,通常无法在程序中进行处理。
Java中的异常体系还包括一些其他的异常类和接口,如RuntimeException、Throwable、StackTraceElement等。这些类和接口都是为了增强Java的异常处理能力而存在的。
有关关键字
Java中的异常处理器包括try、catch、finally和throw关键字。
try块用于包含可能会抛出异常的代码块,catch块用于捕获并处理异常,finally块用于包含一些必须执行的代码,无论是否发生异常都会执行。throw关键字用于手动抛出异常。
Java中还有一种特殊的异常机制,即断言机制。断言机制用于在程序中加入一些断言语句,用于在程序运行时检查程序是否满足一些前置条件。
throw关键字是Java中的一个关键字,用于手动抛出异常。当程序中发生一些异常情况时,可以使用throw关键字来主动抛出一个异常对象,以便让程序中的异常处理器来处理这个异常。
具体来说,throw可以用于两种情况:
-
抛出Java中已有的异常:Java中已有许多预定义异常类,可以通过throw关键字来抛出这些异常类的对象。例如,可以使用throw new NullPointerException()来抛出一个空指针异常对象。
-
抛出自定义异常:在Java中,也可以自定义异常类,然后使用throw关键字来抛出这些自定义异常类的对象。自定义异常类需要继承自Exception或其子类,以便在程序中进行处理。
使用throw关键字可以使程序在发现错误时及时抛出异常,从而避免程序继续执行下去,导致更严重的错误发生。同时,throw关键字也可以使程序更加健壮和可靠,增强程序的错误处理能力。
需要注意的是,使用throw关键字抛出异常后,程序会立即停止执行,并且异常对象会被传递到调用栈中,直到被捕获和处理。因此,在使用throw关键字时,需要确保异常对象能够被正确地捕获并处理,否则程序可能会因为未处理的异常而终止。
抛出异常是什么意思?
抛出异常指的是在程序执行过程中,当某个异常情况出现时,程序会创建一个异常对象,并将其抛出到调用栈中,等待被捕获和处理。
在Java中,当程序出现异常情况时,会自动创建一个异常对象,该对象包含有关异常的信息(如异常类型、异常原因等),然后将其抛出到调用栈中。调用栈是一个存储方法调用信息的数据结构,用于记录程序执行过程中的方法调用关系。当异常对象被抛出到调用栈中时,它会沿着调用栈向上传递,直到被捕获和处理,或者如果没有找到异常处理器,则程序会终止运行。
抛出异常的目的是为了让程序能够在出现异常情况时及时停止执行,并将错误信息传递到调用栈中,以便能够被捕获和处理。如果程序没有及时处理异常,可能会导致程序继续执行下去,导致更严重的错误发生。
在Java中,可以使用try-catch语句来捕获和处理异常,以确保程序在出现异常情况时能够及时停止执行,并给出相应的错误提示。
IO流
什么是?
Java中的IO流是用于在程序中读取和写入数据的机制。IO流可以从不同的来源读取数据,比如文件、网络连接等,并将数据写入到不同的目的地,比如文件、数据库等。Java中的IO流分为两种类型:字节流和字符流。
字节流:字节流以字节为单位读取和写入数据,适用于处理二进制数据。Java中的InputStream和OutputStream是字节流的两个基本类。
字符流:字符流以字符为单位读取和写入数据,适用于处理文本数据。Java中的Reader和Writer是字符流的两个基本类。
Java中的IO流还分为输入流和输出流。输入流用于读取数据,输出流用于写入数据。常见的输入流和输出流包括:
输入流:
- FileInputStream:从文件中读取数据。
- ByteArrayInputStream:从字节数组中读取数据。
- ObjectInputStream:从对象序列化的数据中读取数据。
- SocketInputStream:从网络连接中读取数据。
输出流:
- FileOutputStream:将数据写入到文件中。
- ByteArrayOutputStream:将数据写入到字节数组中。
- ObjectOutputStream:将数据以对象序列化的形式写入到输出流中。
- SocketOutputStream:将数据写入到网络连接中。
Java中的IO流还支持缓冲,以提高读取和写入的效率。缓冲输入流和缓冲输出流分别对应于字节流和字符流。缓冲输入流用于读取数据时,将数据缓存在内存中,以减少对硬盘的访问次数;缓冲输出流用于写入数据时,将数据缓存在内存中,以减少对磁盘的写入次数,从而提高写入效率。
Java中的IO流还支持过滤器,以提供更高级的数据处理功能。过滤器是一种IO流,它可以对数据进行过滤、转换、组合等操作。常见的过滤器包括:
- BufferedInputStream/BufferedOutputStream:提供缓冲功能。
- DataInputStream/DataOutputStream:提供基本数据类型的读写功能。
- InputStreamReader/OutputStreamWriter:将字节流转换为字符流。
- GZIPInputStream/GZIPOutputStream:提供压缩和解压缩功能。
Java中的IO流是一个非常重要的概念,它是Java程序中处理数据的核心机制之一。了解IO流的基本概念和使用方法,可以帮助我们更好地理解和编写Java程序。
Java中的访问修饰符?
Java中的访问修饰符用于控制类、接口、变量、方法等成员的可见性和访问范围,共有四种访问修饰符:public、protected、default(也称为package-private)和private。
-
public:公共访问修饰符,表示该成员可以被任何类访问,无访问限制。
-
protected:受保护的访问修饰符,表示该成员可以被其所在类、同一包内的其他类以及该类的子类访问。
-
default(package-private):默认访问修饰符,表示该成员可以被其所在类、同一包内的其他类访问,但不能被其他包中的类访问。
-
private:私有访问修饰符,表示该成员只能被其所在类访问,其他任何类都无法访问。
需要注意的是,类的访问修饰符只有public和default两种。如果一个类使用public修饰,则该类可以被任何类访问。如果使用default修饰,则该类只能被同一包内的其他类访问。
访问修饰符的使用可以提高代码的安全性和可维护性,同时也能够更好地控制代码的访问范围,避免不必要的外部访问和修改。
结合继承关系再分析?
在继承关系中,访问修饰符的作用更加明显。子类可以访问父类中被protected和public修饰的成员,但不能访问父类中被private修饰的成员。而对于默认访问修饰符,子类只能访问和父类在同一包中的成员。
另外,子类中的成员访问修饰符也要符合继承关系中的规则,即子类中的成员访问修饰符不能比父类中的更严格。例如,如果父类中的成员访问修饰符是protected,则子类中的成员访问修饰符可以是protected或public,但不能是private或default。
继承关系中的访问修饰符可以帮助我们更好地控制代码的访问范围和安全性,同时也能提高代码的可维护性。在使用继承时,我们应该合理地使用访问修饰符,避免过度开放和限制,使代码具有更好的灵活性和扩展性。
- public修饰符
public修饰符是最开放的修饰符,它可以被任何类访问。如果一个类的成员变量或方法被public修饰符修饰,那么它可以被任何其他类的对象访问,甚至是在不同的包中。
代码示例:
public class A {
public int num; // public成员变量
public void method() { // public方法
System.out.println("This is a public method.");
}
}
public class B {
public static void main(String[] args) {
A a = new A();
a.num = 1; // 可以访问public成员变量
a.method(); // 可以访问public方法
}
}
2. protected修饰符
protected修饰符可以被同一包内的其他类访问,也可以被不同包中的子类访问。如果一个类的成员变量或方法被protected修饰符修饰,那么它只能被同一包内的其他类或不同包中的子类访问。
代码示例:
public class A {
protected int num; // protected成员变量
protected void method() { // protected方法
System.out.println("This is a protected method.");
}
}
public class B {
public static void main(String[] args) {
A a = new A();
a.num = 1; // 可以访问protected成员变量
a.method(); // 可以访问protected方法
}
}
public class C extends A { // 子类继承父类A
public static void main(String[] args) {
C c = new C();
c.num = 1; // 可以访问父类中的protected成员变量
c.method(); // 可以访问父类中的protected方法
}
}
3. default修饰符
默认修饰符也称为包访问权限,它只能被同一包内的其他类访问。如果一个类的成员变量或方法没有使用任何访问修饰符,那么它就是默认访问权限。
代码示例:
public class A {
int num; // 默认访问权限的成员变量
void method() { // 默认访问权限的方法
System.out.println("This is a default method.");
}
}
public class B {
public static void main(String[] args) {
A a = new A();
a.num = 1; // 可以访问默认访问权限的成员变量
a.method(); // 可以访问默认访问权限的方法
}
}
4. private修饰符
private修饰符是最严格的修饰符,它只能被定义该成员变量或方法所在的类访问。如果一个类的成员变量或方法被private修饰符修饰,那么它只能被该类内部的其他成员方法访问。
代码示例:
public class A {
private int num; // private成员变量
private void method() { // private方法
System.out.println("This is a private method.");
}
public void setNum(int num) { // 设置private成员变量的方法
this.num = num;
}
public void printNum() { // 访问private成员变量的方法
System.out.println("num = " + num);
}
}
public class B {
public static void main(String[] args) {
A a = new A();
a.setNum(1); // 可以通过public方法设置private成员变量
a.printNum(); // 可以通过public方法访问private成员变量
// a.num = 1; // 编译错误,不能直接访问private成员变量
// a.method(); // 编译错误,不能直接访问private方法
}
}
详细说明Java中的多态? Java的多态是指同一个方法在不同的对象上有不同的实现方式。Java中的多态主要通过继承和接口实现。具体来说,当子类继承父类或实现接口时,可以重写父类或接口中的方法,从而实现不同的行为。在运行时,根据实际调用的对象和方法,动态选择正确的方法实现。
多态的实现需要满足以下三个条件:
-
继承或实现:子类继承父类或实现接口。
-
方法重写:子类重写父类或接口中的方法。
-
父类引用指向子类对象:父类类型的变量引用子类类型的对象。
代码示例:
public class Animal {
public void eat() {
System.out.println("Animal is eating.");
}
}
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog is eating.");
}
public void bark() {
System.out.println("Dog is barking.");
}
}
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("Cat is eating.");
}
public void meow() {
System.out.println("Cat is meowing.");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog(); // 父类引用指向子类对象
Animal animal2 = new Cat(); // 父类引用指向子类对象
animal1.eat(); // 调用子类重写的方法
animal2.eat(); // 调用子类重写的方法
// animal1.bark(); // 编译错误,不能调用子类独有的方法
// animal2.meow(); // 编译错误,不能调用子类独有的方法
Dog dog = (Dog) animal1; // 父类引用强制转换为子类类型
dog.bark(); // 可以调用子类独有的方法
Cat cat = (Cat) animal2; // 父类引用强制转换为子类类型
cat.meow(); // 可以调用子类独有的方法
}
}
在上面的例子中,Animal是父类,Dog和Cat是子类。Animal类中有一个eat()方法,在Dog和Cat中都被重写了。在Main类中,分别用Animal类型的变量引用Dog和Cat类型的对象,然后调用eat()方法,由于多态的作用,会动态选择正确的方法实现。但是,由于父类类型的变量不能调用子类独有的方法,所以不能直接调用bark()和meow()方法。需要将父类引用强制转换为子类类型,才能调用子类独有的方法。需要注意的是,如果强制转换的类型不正确,会引发ClassCastException异常。
详细说明Java中的static关键字,以及它的作用,并给出代码示例
Java中的static是一个关键字,用于修饰类的成员变量、方法和代码块。static修饰的成员变量和方法是属于类的,而不是属于类的实例。也就是说,无论创建多少个类的实例,静态成员变量和方法只有一份,它们都共享同一个内存空间。另外,静态代码块是在类被加载时执行的,它只执行一次。
static关键字的作用主要有以下几点:
-
静态变量:静态变量可以被类的所有实例共享,可以用来记录全局信息,如计数器、常量等。
-
静态方法:静态方法可以直接通过类名调用,不需要创建对象,通常用于工具类或辅助类中。静态方法不能访问非静态成员变量和方法,因为它们只能访问属于类的成员。
-
静态代码块:静态代码块会在类被加载时执行,通常用于初始化静态变量或执行一些静态操作。
代码示例:
public class Example {
private static int count; // 静态成员变量
private int id; // 非静态成员变量
static { // 静态代码块
count = 0;
}
public Example() { // 构造方法
count++;
id = count;
}
public static int getCount() { // 静态方法
return count;
}
public int getId() { // 非静态方法
return id;
}
}
public class Main {
public static void main(String[] args) {
Example e1 = new Example();
Example e2 = new Example();
System.out.println("count = " + Example.getCount()); // 调用静态方法
System.out.println("e1.id = " + e1.getId()); // 调用非静态方法
System.out.println("e2.id = " + e2.getId()); // 调用非静态方法
}
}
在上面的例子中,Example类中定义了一个静态成员变量count和一个非静态成员变量id,还有一个静态代码块和一个构造方法。静态代码块初始化了count变量为0,构造方法在创建对象时,将count增加1,并将id赋值为count的值。Example类还定义了一个静态方法getCount(),返回count的值。在Main类中,创建了两个Example类的实例e1和e2,然后通过Example类的静态方法getCount()获取count的值,通过Example类的非静态方法getId()获取每个实例的id值。由于count是静态成员变量,它的值在所有实例中都是相同的,而id是非静态成员变量,每个实例的id值都不同。
ConcurrentHashMap原理
HashTable的每个方法都加sync,所以性能差。而ConcurrentHashMap是用分段锁或Sync和CAS保证的
扩容机制
详细介绍一下Java中的ThreadLocal
Java中的ThreadLocal是一个线程本地变量,它可以在每个线程中存储不同的值,而不会相互干扰。ThreadLocal是一个线程级别的数据存储,可以用于存储线程的上下文信息,比如用户会话信息、数据库连接、事务上下文等。
ThreadLocal的使用非常简单,只需要创建一个ThreadLocal对象,然后通过它的get()和set()方法来访问线程本地变量。每个线程对应一个ThreadLocalMap对象,ThreadLocalMap对象中存储了该线程的所有ThreadLocal变量及其对应的值。当线程结束时,ThreadLocalMap对象会被回收,从而避免了内存泄漏问题。
下面是一个简单的使用ThreadLocal的示例:
public class MyThreadLocal {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void set(String value) {
threadLocal.set(value);
}
public static String get() {
return threadLocal.get();
}
public static void remove() {
threadLocal.remove();
}
}
public class MyRunnable implements Runnable {
@Override
public void run() {
MyThreadLocal.set("Hello, ThreadLocal!");
System.out.println("ThreadLocal value in thread " + Thread.currentThread().getName() + ": " + MyThreadLocal.get());
MyThreadLocal.remove();
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread1 = new Thread(runnable, "Thread 1");
Thread thread2 = new Thread(runnable, "Thread 2");
thread1.start();
thread2.start();
}
}
在上面的示例中,我们创建了一个MyThreadLocal类来封装ThreadLocal的使用,然后在MyRunnable中使用MyThreadLocal来存储线程本地变量。在Main类中,我们创建了两个线程来执行MyRunnable,并分别输出了它们的ThreadLocal值。
需要注意的是,由于ThreadLocal是一个线程级别的数据存储,因此在多线程环境下需要注意对它的访问。特别是在使用线程池等共享线程资源的情况下,需要注意清理ThreadLocal的值,以避免出现数据混乱或内存泄漏等问题。
进程线程
为什么使用多线程?
多线程问题
线程生命周期和状态
线程上下文切换
sleep()和wait()方法,join()和yield()方法
volatile关键字
synchronized关键字
线程池
AQS
AQS(AbstractQueuedSynchronizer)是JUC(Java Util Concurrency)中的一个重要组件,它是一个基于FIFO队列的同步器,可以用来实现各种同步工具,如ReentrantLock、Semaphore、CountDownLatch等。AQS的底层原理是通过一个双向链表(CLH队列)来维护等待线程,同时使用一个volatile变量(state)来表示同步状态。
AQS的核心思想是将所有需要同步的操作都封装在一个共享资源(如锁)中,通过AQS提供的模板方法来实现同步逻辑。当多个线程同时访问共享资源时,AQS会将其中一个线程(通常是第一个尝试获取锁的线程)设置为独占模式,其他线程则进入等待队列。在独占模式下,线程可以修改共享资源的状态,直到释放锁为止。当独占模式的线程释放锁时,AQS会从等待队列中唤醒一个线程,使其进入独占模式,并继续执行操作。
下面是一个简单的使用AQS实现锁的示例:
public class MyLock {
private final Sync sync;
public MyLock() {
sync = new Sync();
}
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
}
public class Main {
private static final MyLock lock = new MyLock();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
System.out.println("Thread 1 acquired lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("Thread 1 released lock");
}
}).start();
new Thread(() -> {
lock.lock();
System.out.println("Thread 2 acquired lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("Thread 2 released lock");
}
}).start();
}
}
在上面的示例中,我们定义了一个MyLock类,它使用AQS来实现锁的功能。在MyLock中,我们定义了一个Sync内部类,它继承了AbstractQueuedSynchronizer,并实现了tryAcquire、tryRelease和isHeldExclusively三个方法,分别用于获取锁、释放锁和判断当前线程是否持有锁。在Main类中,我们创建了两个线程来演示锁的使用,其中一个线程获取锁后睡眠1秒钟,另一个线程在它释放锁之后才能获取锁。
需要注意的是,AQS是一个高度抽象的同步框架,要想正确地使用它,需要了解其底层原理以及各种同步工具的使用方式。同时,在使用AQS时需要注意避免死锁、饥饿等问题,保证程序的正确性和性能。