一、Java基础语法
1.1 基础类型
| 类型 | 占用空间大小 | 取值范围 |
| byte | 1字节 | -128 ~ 127 |
| short | 2字节 | -32768 ~ 32767 |
| int | 4字节 | -2,147,483,648 ~ 2,147,483,647 |
| long | 8字节 | -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 |
| float | 4字节 | 约 ±3.40282347E+38F(有效数字为6~7位) |
| double | 8字节 | 约 ±1.79769313486231570E+308(有效数字为15位左右) |
| boolean |
1.2 流程控制
1.2.1 顺序结构
代码按照一定的顺序依次执行,没有条件或循环限制。语法形式如下:
语句1;
语句2;
……
语句n;
1.2.2 分支结构
根据条件的不同,选择执行不同的代码块,主要包括if-else和switch-case两种语法结构。
if-else语句的语法格式如下:
if (条件) {
// 如果条件成立,执行这里的代码块
} else {
// 否则执行这里的代码块
}
switch-case语句的语法格式如下:
switch (表达式) {
case 常量1:
// 执行这里的代码块
break;
case 常量2:
// 执行这里的代码块
break;
……
default:
// 执行这里的代码块
break;
}
1.2.3 循环结构
根据条件判断,反复执行同一段代码,主要包括for循环、while循环和do-while循环三种语法结构。
for循环的语法格式如下:
for (初始化; 判断条件; 修改条件) {
// 循环体语句
}
while循环的语法格式如下:
while (循环条件) {
// 循环体语句
}
do-while循环的语法格式如下:
do {
// 循环体语句
} while (循环条件);
1.2.4 递归结构
指函数(或方法)本身调用自己的一种技巧。递归解决问题时,将一个大问题拆分成若干个相似的小问题,并通过调用函数本身解决小问题,最终得到原问题的解。
通常,递归解决问题可以让代码更加简洁优雅,但是需要注意的是,递归可能会存在栈溢出等风险
public class Fibonacci {
public static int fib(int n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
public static void main(String[] args) {
int n = 10;
for (int i = 0; i < n; i++) {
System.out.print(fib(i) + " ");
}
}
}
1.2.5 流程关键词
Java中的continue、break和return是三个关键字,用于控制程序的流程。
continue:在循环语句(for、while、do-while)中,跳过当前迭代并继续下一次迭代。break:在循环语句和开关语句(switch)中,立即终止循环或开关,并跳出语句块。return:在方法中,返回指定类型的值并结束方法的执行。
1.3 抽象类
Java中的抽象类是一种不能直接实例化对象的类,它仅用于被其他类继承和扩展。在抽象类中,可以定义一些抽象方法,这些方法只有名称、返回类型和参数列表,没有具体的实现内容,并且必须由子类进行实现。如果一个类包含了至少一个抽象方法,那么这个类就必须声明为抽象类。
抽象类的主要作用是在面向对象编程中,为具有相似特征的一组类建立一个公共的抽象父类,以避免代码重复,提高代码的可维护性和可扩展性。另外,Java中的接口也是一种类似于抽象类的机制,不过接口中的所有方法都是抽象方法,而且一个类可以实现多个接口,但只能继承一个父类。
以下是一个简单的抽象类的示例:
public abstract class Animal {
public abstract void eat();
public abstract void sleep();
}
public class Cat extends Animal {
public void eat() {
System.out.println("Cat is eating.");
}
public void sleep() {
System.out.println("Cat is sleeping.");
}
}
1.4 接口
Java中的接口是一种定义行为规范的抽象类型,它可以用来描述类应该具有哪些行为,但不关心这些行为是如何实现的。可以将接口看作是一种约定,即实现类必须满足接口中所定义的方法。在Java语言中,接口是通过interface关键字来定义的。
以下是一个简单的接口的示例:
public interface Shape {
void draw();
double getArea();
}
在以上代码中,我们定义了一个接口Shape,它包含两个方法draw()和getArea(),这些方法没有具体的实现代码。
接口的主要作用是为了实现多态性,让不同的实现类具有相同的行为规范,以便于统一处理。另外,接口也可以被用来解耦合,将实现类与调用者之间进行解耦合,增强系统的可维护性和可扩展性。在Java中,一个类可以实现多个接口,从而具有多种行为规范。
1.5 枚举
Java中的枚举(Enum)是一种数据类型,用于表示一组固定常量,常常用于程序中需要使用特定常量的场景。它可以让代码更加简洁、清晰和易读。在Java中,枚举是通过enum关键字来定义的。
以下是一个简单的枚举的示例:
public enum Direction {
NORTH,
SOUTH,
EAST,
WEST
}
在以上代码中,我们定义了一个枚举类型Direction,它包含了四个枚举常量:NORTH、SOUTH、EAST、WEST。
枚举类型可以被看做是一种特殊的类,它可以包含属性、方法和构造函数,并且枚举常量本身也可以拥有自己的属性和方法。Java中的枚举还支持实现接口和继承其他类,从而具有更加强大的扩展性。
使用枚举可以减少代码中的硬编码,提高程序的可读性和可维护性。枚举常量的值是固定的,所以不会发生改变,可以在程序中随时引用和使用。另外,枚举还可以用于switch语句中,让程序更加简洁明了。
1.6 范型
Java泛型(Generics)是JDK 5中引入的一个新特性,它可以是Java的类和方法变得更加灵活,增强代码的可复用和可读性,同时确保了类型安全和编译时检查。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。我们可以在定义类、接口、方法时,使用“<>”括起来的泛型参数列表来定义泛型类型。
泛型在Java中的好处是显而易见的:既可以提高代码的可重用性,又可以避免类型转换带来的不安全性问题。此外,Java中的泛型还支持通配符、上下界、类型推断等特性,让程序员可以更加灵活地使用泛型。
1.6.1 泛型类
public class Box<T> {
private T data;
public void setData(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
在以上代码中,我们定义了一个泛型类Box,它有一个泛型参数T,可以代表任何具体类型。该类有两个方法setData()和getData(),分别用于设置和获取data属性的值。
1.6.2 泛型接口
public interface MyList<T> {
void add(T value);
T get(int index);
int size();
}
1.6.3 方法泛型
public class MyUtils {
public static <T> T createObject(Class<T> cls) throws Exception {
return cls.getDeclaredConstructor().newInstance();
}
}
在以上代码中,我们定义了一个静态方法createObject(),使用类型参数T来表示返回值的类型。该方法接受一个Class<T>类型的参数cls,用于获取需要创建的对象的类型。在方法内部,我们通过反射机制创建一个指定类型的对象,并将其返回。
二、面向对象
2.1 方法
在Java中,方法是面向对象编程的基础,封装了类中的行为逻辑并对外提供了接口。
面向对象编程中的方法和面向过程编程中的函数有些类似,它们都是一段可执行的代码块,可以实现某种功能。但是,方法不仅仅包含代码,还具有访问权限、返回值类型和参数列表等属性,使得方法可以更加灵活地处理数据和交互对象。
在Java中,方法的定义格式如下:
修饰符 返回值类型 方法名(参数列表) {
// 方法体
}
其中,修饰符表示访问权限和其他限制条件,返回值类型表示方法的返回结果类型,方法名用于标识方法,参数列表包含了方法需要的输入参数,方法体是具体的代码实现。
2.2 重载
方法重载是一种常见的技术,它允许在同一类中声明多个方法,方法名相同但参数列表不同,以便实现不同的功能。可以方便地实现不同的功能需求,使代码更加清晰,同时也可以减少代码的重复编写。
Java方法重载的规则如下:
- 方法名必须相同
- 参数列表必须不同(参数类型、参数个数或参数顺序不同)
- 方法返回类型可以相同也可以不同
以下是一个Java方法重载的示例:
public class Calculator {
public int add(int x, int y) {
return x + y;
}
public double add(double x, double y) {
return x + y;
}
public int add(int x, int y, int z) {
return x + y + z;
}
}
2.3 封装
Java封装是指将类的数据和操作处理以及保护起来,隐藏其复杂性,并且仅通过公共接口向其他类提供有限的访问权限。在Java中,我们通过访问修饰符(public、protected、private)来控制数据的访问权限,实现对数据的封装。
通过封装,我们可以控制数据被访问的权限,并改善类与类之间的通信方式。同时,也可以使代码更加易于维护和扩展。
封装的好处在于:
- 防止数据被不恰当地修改
- 隐藏复杂性,使代码具有更高的可维护性和可读性
- 使得代码更加安全,在一定程度上可以避免编程错误
2.4 多态
Java中的多态指的是同一个方法在不同的类中有不同的实现方式。多态性是对象多种表现形式的体现,可以提高代码的重用性和灵活性。
实现多态的方式主要有两种:继承和接口。
2.4.1 继承多态性
是指子类可以覆盖父类中的同名方法,调用时会执行子类中的方法实现。例如:
public class Animal {
public void move() {
System.out.println("Animal is moving...");
}
}
public class Dog extends Animal {
public void move() {
System.out.println("Dog is running on four legs...");
}
}
public class Cat extends Animal {
public void move() {
System.out.println("Cat is jumping and climbing...");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Animal();
Animal animal2 = new Dog();
Animal animal3 = new Cat();
animal1.move(); // Animal is moving...
animal2.move(); // Dog is running on four legs...
animal3.move(); // Cat is jumping and climbing...
}
}
在上述代码中,Dog和Cat类都继承了Animal类,并覆盖了其move()方法。使用父类类型声明的变量(animal2和animal3)可以分别引用子类对象,调用它们各自的move()方法,实现多态。
2.4.2 接口多态性
是指一个类实现了多个接口,并且对这些接口有不同的实现方式。例如:
interface Swim {
void swim();
}
interface Fly {
void fly();
}
class Bird implements Swim, Fly {
public void swim() {
System.out.println("Bird is swimming...");
}
public void fly() {
System.out.println("Bird is flying...");
}
}
class Main {
public static void main(String[] args) {
Bird bird = new Bird();
Swim swim = bird;
Fly fly = bird;
swim.swim(); // Bird is swimming...
fly.fly(); // Bird is flying...
}
}
在上述代码中,Bird类实现了两个接口Swim和Fly,并分别实现了它们的方法。使用接口类型声明的变量(swim和fly)可以引用实现类对象,调用它们各自的方法。
2.5 访问权限
2.5.1 继承中的访问权限
在Java中,父类中的成员变量和方法可以被子类继承并使用,但子类不能访问父类中的private成员变量和方法。具体来说:
- public修饰的成员变量和方法可以被任何类访问;
- protected修饰的成员变量和方法可以被同一个包内的类和不同包中的子类访问;
- 默认或不加任何修饰符的成员变量和方法可以被同一个包内的类访问;
- private修饰的成员变量和方法只能被所在类访问。
- 子类重写父类的方法时,访问权限修饰符必须大于或等于父类的访问权限修饰符。(这是因为Java中的继承关系是is-a的关系,即子类是一个特殊的父类,具有父类所有的属性和方法。如果子类重写的方法的访问权限小于父类的访问权限,那么子类就不能完全替代父类,这样在使用子类对象时就会出现错误)
2.5.2 实现中的访问权限
在Java中,实现接口时需要实现接口中所有的方法,并且这些方法都是public访问权限。因为接口中的方法默认是public abstract类型的,因此子类实现时必须将这些方法声明为public类型,否则会导致编译错误。
三、常用类
Java 常用类是 Java 标准类库中的一部分,是指经常被开发者们使用的文档、集合、时间日期等基础类。这些常用类大多都在 java.util 和 java.lang 包中提供。下面列举了一些常用类及其功能:
- Object 类:位于 java.lang 包中,是所有类的超类。它定义了一些基本方法,如 equals()、hashCode() 和 toString() 等,可以在其他类中继承和重写这些方法。
- String 类:位于 java.lang 包中,用于处理字符串。String 对象是不可变的,即一旦创建就无法更改其内容。可以使用 String 类提供的方法来操作和处理字符串。
- StringBuilder 和 StringBuffer 类:位于 java.lang 包中,用于动态修改字符串中的内容。StringBuilder 是线程不安全的,适用于单线程环境。StringBuffer 是线程安全的,适用于多线程环境。
- ArrayList 和 LinkedList 类:位于 java.util 包中,用于实现列表。ArrayList 是基于数组实现的,支持随机访问,但插入和删除操作效率较低。LinkedList 是基于链表实现的,支持高效的插入和删除操作,但随机访问效率较低。
- HashMap 和 TreeMap 类:位于 java.util 包中,用于实现映射关系。HashMap 是基于哈希表实现的,支持快速访问和插入,但不保证元素的顺序。TreeMap 是基于红黑树实现的,可以对元素进行排序。
- Date 和 Calendar 类:位于 java.util 包中,用于处理日期和时间。Date 类表示日期和时间,Calendar 类可以进行日期和时间的计算、格式化和解析等操作。
除了以上列举的类之外,Java 常用类还包括 Math、Random、Scanner 等多个类,提供了各种基本功能。
四、Java基础进阶
4.1 集合类
| 集合类型 | 特点 | 子类及特点 |
|---|---|---|
| List | 有序集合,可以允许重复元素。 | ArrayList:基于数组实现,支持快速随机访问和遍历。LinkedList:基于链表实现,支持快速插入和删除。Vector:与ArrayList类似,但线程安全。 |
| Set | 无序集合,不允许重复元素。 | HashSet:基于哈希表实现,无序性能高,元素需实现hashCode()和equals()方法。TreeSet:基于红黑树实现,有序且性能高,元素需实现Comparable接口。LinkedHashSet:基于哈希表和链表实现,保留插入顺序。 |
| Map | 映射表结构,通过key-value对存储和操作数据。 | HashMap:基于哈希表实现,无序性能高,键值对需实现hashCode()和equals()方法。TreeMap:基于红黑树实现,有序且性能高,键值对需实现Comparable接口。LinkedHashMap:基于哈希表和链表实现,保留插入顺序。 |
| Queue | 先进先出(FIFO)的集合结构。 | ArrayDeque:基于双端队列实现,不限制容量,支持高效地添加或删除队首或队尾。LinkedList:基于链表实现,支持高效的添加或删除队首或队尾。PriorityQueue:基于堆实现,支持元素的优先级排序并依次出队。 |
| Stack | 后进先出(LIFO)的集合结构。 | Stack:基于Vector实现,线程安全且效率较低,不推荐使用。 |
4.2 注解
Java 注解(Annotation)是 JDK5.0 引入的一种注释机制,可以为 Java 代码提供元数据。它可以作为元数据,不直接影响代码执行,但某些类型的注解可以用于这一目的。Java 注解可以通过反射获取标注内容,在编译器生成类文件时可以被嵌入到字节码中,并且在运行时可以获取到标注内容。
4.2.1 Java 中常用的注解
| 注解 | 描述 |
|---|---|
| @Override | 标识一个方法重写父类中的方法 |
| @Deprecated | 标注过时的方法或类 |
| @SuppressWarnings | 忽略编译器警告 |
| @FunctionalInterface | 标识函数式接口 |
| @SafeVarargs | 标识可变参数是否安全 |
| @Target | 标识注解的作用范围 |
| @Retention | 标识注解的生命周期 |
| @Documented | 标识注解是否包含在 Javadoc 中 |
4.2.3 元注解
| 元注解 | 描述 | 版本 |
|---|---|---|
| @Retention | 标识注解的生命周期:SOURCE、CLASS(默认)、RUNTIME | 1.5 |
| @Target | 标识注解的使用范围,可用于修饰类、方法、属性、参数等 | 1.5 |
| @Documented | 标识注解是否包含在 Javadoc 中 | 1.5 |
| @Inherited | 标识注解是否可以被子类继承 | 1.5 |
| @Repeatable | 标识注解是否可以重复使用 | 1.8 |
4.3 IO流
| IO流种类 | 描述 | 特点 |
|---|---|---|
| 字节流 | 以字节为单位进行输入输出的IO流,用于处理二进制数据。 | 输入流的父类是InputStream,输出流的父类是OutputStream。 |
| 字符流 | 以字符为单位进行输入输出的IO流,用于处理文本数据。 | 输入流的父类是Reader,输出流的父类是Writer,可以使用缓存提高效率。 |
| 缓冲流 | 为其他输入输出流添加缓存功能,可以减少输入输出次数,提高效率。 | 缓冲输入流的父类是BufferedInputStream,缓冲输出流的父类是BufferedOutputStream,缓冲字符输入流的父类是BufferedReader,缓冲字符输出流的父类是BufferedWriter。 |
| 转换流 | 用于在字节流和字符流之间转换数据。 | 字节转字符流的转换流是InputStreamReader和OutputStreamWriter,字符转字节流的转换流是OutputStreamWriter和InputStreamReader。 |
| 对象流 | 用于读写Java对象,可以将Java对象直接输出到文件或网络中。 | 对象输入流的父类是ObjectInputStream,对象输出流的父类是ObjectOutputStream。 |
| 数据流 | 用于读写Java基本数据类型和字符串。 | 数据输入流的父类是DataInputStream,数据输出流的父类是DataOutputStream。 |
4.4 反射
Java反射是一种机制,可以在程序运行时动态地获取类的信息(包括类名、父类、实现的接口、构造方法、成员变量、成员方法等),并可以操作类的属性和方法。其核心是通过类的字节码对象创建出类的实例,从而达到在程序运行期间调用类的方法和访问类的属性的目的。
Java反射机制主要通过以下几个类和接口来实现:
- Class:代表一个类或一个接口,提供了获取类信息的方法。
- Constructor:代表一个构造方法,提供了调用构造方法的方法。
- Method:代表一个方法,提供了调用方法的方法。
- Field:代表一个成员变量,提供了访问和修改成员变量的方法。
使用Java反射机制可以实现很多功能,比如可以动态加载类,创建对象,调用方法,访问和修改成员变量等。但是需要注意的是,使用反射机制会降低程序的性能和安全性,因此应该谨慎使用。
4.5 多线程
4.5.1 继承Thread类
创建线程的步骤如下:
- 定义一个类并继承 Thread 类。
- 重写 Thread 类中的 run() 方法,在该方法中编写需要在新线程中执行的代码。
- 创建该类的对象。
- 调用该对象的 start() 方法,该方法将启动一个新线程,并自动调用 run() 方法。
以下是一个简单的示例代码,展示了如何使用 Thread 类来创建和启动一个新线程:
class MyThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
输出结果为:
Hello from a thread!
4.5.2 实现Runnable接口
实现 Runnable 接口需要完成以下步骤:
- 定义一个类并实现 Runnable 接口。
- 实现 Runnable 接口中的 run() 方法,在该方法中编写需要在新线程中执行的代码。
- 创建该类的对象。
- 将该对象作为参数传递给 Thread 类的构造器,创建一个新的 Thread 对象。
- 调用该 Thread 对象的 start() 方法,该方法将启动一个新线程,并自动调用 Runnable 对象中的 run() 方法。
以下是一个简单的示例代码,展示了如何使用 Runnable 接口来创建和启动一个新线程:
class MyThread implements Runnable {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
}
}
输出结果为:
Hello from a thread!
4.5.3 ThreadPoolExecutor类
Java 中的 ThreadPoolExecutor 类是一个线程池实现类,用于管理和调度线程池中的线程。通过使用 ThreadPoolExecutor 类,可以避免重复创建和销毁线程的开销,从而提高程序的性能。
ThreadPoolExecutor 类具有以下特点:
- 可以动态地调整线程池的大小,以满足不同的任务需求。
- 可以限制线程池中同时执行的最大任务数。
- 可以设置任务队列,存放等待执行的任务,以防止任务过多导致内存溢出或系统崩溃。
- 可以设置线程池的超时时间,当线程池空闲一段时间后,多余的线程会被回收。
使用 ThreadPoolExecutor 类可以大幅提高多线程编程的效率和可靠性,尤其是对于需要频繁创建和销毁线程的应用程序。
下面是一个简单的示例代码,展示了如何使用 ThreadPoolExecutor 类创建一个线程池:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPool {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable worker = new MyRunnable(i);
executor.execute(worker);
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
class MyRunnable implements Runnable {
private int id;
public MyRunnable(int id) {
this.id = id;
}
public void run() {
System.out.println("Thread " + id + " is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread " + id + " is done");
}
}
输出结果为:
Thread 0 is running
Thread 2 is running
Thread 4 is running
Thread 1 is running
Thread 3 is running
Thread 4 is done
Thread 0 is done
Thread 2 is done
Thread 1 is done
Thread 3 is done
Thread 5 is running
Thread 6 is running
Thread 7 is running
Thread 8 is running
Thread 9 is running
Thread 6 is done
Thread 7 is done
Thread 9 is done
Thread 5 is done
Thread 8 is done
Finished all threads
4.5.4 多线程理论
4.5.4.1 生产者与消费者
Java中的生产者与消费者模式是一种常见的线程同步机制,用于解决多线程之间的生产者和消费者问题。在这种模式中,生产者和消费者通过共享一个缓冲区来通信,生产者向缓冲区中存储数据,而消费者从缓冲区中取出数据。具体实现中,可以使用wait()和notify()方法来实现线程的等待和唤醒,从而保证生产者与消费者的协作。
生产者与消费者模式可以用来实现异步消息队列、线程池等多种场景,可以避免生产者与消费者之间的阻塞和空转,提高系统效率。
public class ProducerConsumerExample {
private static final int BUFFER_SIZE = 10; // 缓冲区大小
private static List<Integer> buffer = new ArrayList<>(); // 缓冲区
private static Object lock = new Object(); // 锁对象,用于同步
public static void main(String[] args) {
Thread producer = new Thread(new Producer());
Thread consumer = new Thread(new Consumer());
producer.start();
consumer.start();
}
// 生产者线程
static class Producer implements Runnable {
@Override
public void run() {
try {
int count = 0;
while (true) {
synchronized (lock) {
while (buffer.size() == BUFFER_SIZE) { // 如果缓冲区已满,等待消费者线程取走数据
lock.wait();
}
buffer.add(count);
System.out.println("Produced: " + count);
count++;
lock.notifyAll(); // 通知消费者线程可以开始取走数据了
}
Thread.sleep(500); // 生产者线程休眠一段时间,模拟生产数据的过程
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
// 消费者线程
static class Consumer implements Runnable {
@Override
public void run() {
try {
while (true) {
synchronized (lock) {
while (buffer.isEmpty()) { // 如果缓冲区为空,等待生产者线程生产数据
lock.wait();
}
int value = buffer.remove(0);
System.out.println("Consumed: " + value);
lock.notifyAll(); // 通知生产者线程可以开始生产数据了
}
Thread.sleep(500); // 消费者线程休眠一段时间,模拟处理数据的过程
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
4.5.4.2 读者写者问题
读者写者问题是经典的并发编程问题,它描述了多个读线程和一个写线程同时访问共享数据时可能会出现的问题。在 Java 中,可以使用 Lock 和 Condition 来实现这个问题的解决方案。
下面是一个基于 ReentrantReadWriteLock 的读者写者问题示例代码:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReaderWriter {
private final Lock lock = new ReentrantReadWriteLock().writeLock();
private final Condition condition = lock.newCondition();
private int count;
public void read() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
condition.await();
}
// 执行读操作
System.out.println("Read: " + count);
count--;
condition.signalAll();
} finally {
lock.unlock();
}
}
public void write() throws InterruptedException {
lock.lock();
try {
while (count != 0) {
condition.await();
}
// 执行写操作
System.out.println("Write: " + count);
count++;
condition.signalAll();
} finally {
lock.unlock();
}
}
}
上述代码中,ReaderWriter 类有一个可重入的写锁,读操作需要获取读锁即可,写操作需要获取写锁。在读操作中,若 count 为 0,则等待其他线程执行写操作并唤醒读线程;在写操作中,若 count 不为 0,则等待其他线程执行读操作并唤醒写线程。
4.5.4.3 哲学家进餐问题
哲学家进餐问题是经典的并发编程问题之一,它描述了多个哲学家同时从一张圆桌上取餐和放餐的行为可能会遇到的死锁问题。在 Java 中,可以使用 ReentrantLock 和 Condition 来实现这个问题的解决方案。
下面是一个基于 ReentrantLock 和 Condition 的哲学家进餐问题示例代码:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class DiningPhilosophers {
private final ReentrantLock[] forks;
private final Condition[] conditions;
public DiningPhilosophers(int n) {
this.forks = new ReentrantLock[n];
this.conditions = new Condition[n];
for (int i = 0; i < n; i++) {
forks[i] = new ReentrantLock();
conditions[i] = forks[i].newCondition();
}
}
public void eat(int i) throws InterruptedException {
int left = i;
int right = (i + 1) % forks.length;
if (i % 2 == 0) {
forks[left].lock();
forks[right].lock();
} else {
forks[right].lock();
forks[left].lock();
}
try {
System.out.println("Philosopher " + i + " is eating");
Thread.sleep(1000);
} finally {
forks[left].unlock();
forks[right].unlock();
}
}
public static void main(String[] args) throws InterruptedException {
DiningPhilosophers dining = new DiningPhilosophers(5);
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
final int philosopher = i;
threads[i] = new Thread(() -> {
while (true) {
try {
dining.eat(philosopher);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
}
}
上述代码中,DiningPhilosophers 类有一个 forks 数组和一个 conditions 数组,每个哲学家需要同时获取左边和右边的叉子才能开始吃饭。使用 i % 2 == 0 判断左右手来避免死锁。在 eat() 方法中,如果当前哲学家的左右两边的叉子都可用,则获取叉子并执行吃饭操作,最后释放叉子。
4.5.4.4 理发师问题
理发师问题描述了在某个理发店中有一个理发师和若干顾客,理发师一次只能为一个顾客理发,而顾客需要等待理发师空闲时才能得到服务。在 Java 中,可以使用 Semaphore 来实现这个问题的解决方案。
下面是一个基于 Semaphore 的理发师问题示例代码:
import java.util.concurrent.Semaphore;
public class BarberShop {
private Semaphore customers;
private Semaphore barber;
private Semaphore mutex;
private int waiting;
public BarberShop(int chairs) {
customers = new Semaphore(0);
barber = new Semaphore(0);
mutex = new Semaphore(1);
waiting = 0;
}
public void getHaircut(int i) throws InterruptedException {
mutex.acquire();
if (waiting == customers.availablePermits()) {
System.out.println("Customer " + i + " leaves the shop");
mutex.release();
return;
}
waiting++;
mutex.release();
customers.acquire();
System.out.println("Customer " + i + " is getting a haircut");
Thread.sleep(500);
barber.release();
System.out.println("Customer " + i + " leaves the shop");
}
public void cutHair() throws InterruptedException {
while (true) {
customers.acquire();
mutex.acquire();
waiting--;
mutex.release();
System.out.println("Barber is cutting hair");
Thread.sleep(1000);
barber.acquire();
}
}
public static void main(String[] args) throws InterruptedException {
BarberShop barberShop = new BarberShop(3);
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
final int customer = i;
threads[i] = new Thread(() -> {
try {
barberShop.getHaircut(customer);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threads[i].start();
}
Thread barberThread = new Thread(() -> {
try {
barberShop.cutHair();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
barberThread.start();
for (Thread thread : threads) {
thread.join();
}
barberThread.interrupt();
}
}
上述代码中,BarberShop 类有一个 customers Semaphore 和一个 barber Semaphore,顾客需要获取信号量来等待理发师为其理发,理发师需要获取信号量来等待顾客的到来。在 getHaircut() 方法中,如果没有空余的等待座位,则顾客离开理发店;否则,顾客增加等待计数并等待理发师;当理发师完成为某个顾客理发的过程后,释放信号量以通知下一个顾客前来理发。
4.5.4.5 洗手间问题
va 中的洗手间问题(Bathroom problem)描述了男女两个不同性别的人共用一间洗手间可能会遇到的同步问题。在这个问题中,当一个人正在使用洗手间时,另一个人需要等待直到洗手间空闲才能继续使用。在 Java 中,可以使用 Semaphore 来实现这个问题的解决方案。
下面是一个基于 Semaphore 的洗手间问题示例代码:
import java.util.concurrent.Semaphore;
public class Bathroom {
private Semaphore male;
private Semaphore female;
private Semaphore mutex;
private int maleCount;
private int femaleCount;
public Bathroom() {
male = new Semaphore(1);
female = new Semaphore(1);
mutex = new Semaphore(1);
maleCount = 0;
femaleCount = 0;
}
public void maleUse(int i) throws InterruptedException {
mutex.acquire();
maleCount++;
if (maleCount == 1) {
female.acquire();
}
mutex.release();
male.acquire();
System.out.println("Male " + i + " is using bathroom");
Thread.sleep(1000);
male.release();
mutex.acquire();
maleCount--;
if (maleCount == 0) {
female.release();
}
mutex.release();
}
public void femaleUse(int i) throws InterruptedException {
mutex.acquire();
femaleCount++;
if (femaleCount == 1) {
male.acquire();
}
mutex.release();
female.acquire();
System.out.println("Female " + i + " is using bathroom");
Thread.sleep(1000);
female.release();
mutex.acquire();
femaleCount--;
if (femaleCount == 0) {
male.release();
}
mutex.release();
}
public static void main(String[] args) throws InterruptedException {
Bathroom bathroom = new Bathroom();
Thread[] threads = new Thread[10];
for (int i = 0; i < 5; i++) {
final int male = i;
threads[i] = new Thread(() -> {
try {
bathroom.maleUse(male);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threads[i].start();
final int female = i;
threads[i + 5] = new Thread(() -> {
try {
bathroom.femaleUse(female);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threads[i + 5].start();
}
for (Thread thread : threads) {
thread.join();
}
}
}
上述代码中,Bathroom 类有两个 Semaphore(male 和 female)和一个 mutex Semaphore,用于控制男女使用洗手间的同步。在 maleUse() 方法和 femaleUse() 方法中,首先获取 mutex 信号量来保证每个人进入等待状态前都能更新男女等待数量。如果当前性别没有人在使用洗手间,则获取相应的信号量,并执行使用洗手间的操作。当使用完毕后,释放相应的信号量并更新男女等待数量。
可以在多个线程中通过调用 maleUse() 或 femaleUse() 方法来模拟多个人同时使用洗手间的过程。
本文详细介绍了Java编程的基础知识,包括数据类型、流程控制、抽象类与接口、枚举、泛型,以及面向对象的原理,如方法、重载、封装和多态。此外,还讨论了Java中的常用类、集合框架、注解、反射和多线程概念,如线程池、线程同步问题的解决策略。

被折叠的 条评论
为什么被折叠?



