1. 多线程
1.1 基本概念
1.1.1 进程与线程的对比
进程和线程都是计算机中用于并发执行的基本单元,但它们之间有一些核心区别:
-
定义和概念
-
进程(Process): 进程是操作系统分配资源的基本单位。每个进程都有自己的内存空间、数据栈以及其他用于跟踪执行的辅助数据。进程之间相互独立,操作系统负责管理和调度不同的进程。
-
线程(Thread): 线程是进程内部的执行单元,是处理器调度和执行的基本单位。一个进程中可以包含多个线程,它们共享进程的资源,如内存和文件句柄,但每个线程有自己的执行堆栈和程序计数器。
-
-
资源分配和共享
-
进程: 进程拥有完全独立的地址空间,一个进程崩溃不会直接影响其他进程。进程间的资源共享较为复杂,需要进程间通信机制,如管道、信号、套接字等。
-
线程: 线程在同一进程内共享地址空间和资源,线程间的通信更为简单,可以直接读写进程数据段(如全局变量)来进行通信,但这也使得线程间的数据同步变得关键。
-
-
开销和性能
-
进程: 进程的创建、切换和管理的开销比线程大,因为进程需要更多的资源和独立的地址空间。
-
线程: 线程的创建和切换开销小于进程,因为线程共享大部分资源。线程可以提高程序的响应性和资源利用率,尤其是在多核处理器上。
-
-
通信和同步
-
进程: 进程间通信(IPC)需要特定的机制,因为进程彼此独立。
-
线程: 线程间可以直接通信,因为它们共享相同的进程内存空间。但这也需要同步机制,如互斥锁(Mutex)和信号量(Semaphore),以避免竞争条件和数据不一致。
-
-
独立性
-
进程: 更加独立,适用于需要隔离的应用场景。
-
线程: 依赖于进程,更适合执行共享资源的并发任务。
-
总之,进程和线程都是实现任务并发执行的方式,但线程是更轻量级的,适合需要高效资源共享和通信的场景,而进程则提供了更强的隔离和独立性。
1.1.2 如何创建线程
在 Java 中创建线程主要有两种方式:继承 Thread
类和实现 Runnable
接口。还有一种更高级的方式,使用 Callable
和 Future
接口,以及通过线程池创建线程。
-
继承
Thread
类这是创建线程的最基本方式。你需要创建一个继承自
Thread
类的子类,并重写其run()
方法。然后你可以创建该子类的实例,并调用其start()
方法来启动线程。class MyThread extends Thread { public void run() { System.out.println("Thread is running."); } public static void main(String args[]) { MyThread t1 = new MyThread(); t1.start(); } }
-
实现
Runnable
接口实现
Runnable
接口是另一种创建线程的方式。你需要实现Runnable
接口,并实现run()
方法。然后,将Runnable
对象作为参数传递给Thread
类的构造器,最后调用Thread
对象的start()
方法。class MyRunnable implements Runnable { public void run() { System.out.println("Runnable thread is running."); } public static void main(String args[]) { MyRunnable myRunnable = new MyRunnable(); Thread t1 = new Thread(myRunnable); t1.start(); } }
-
使用
Callable
和Future
Callable
接口类似于Runnable
,但它可以返回一个结果,并且能抛出异常。Future
可以用于获取Callable
任务的结果。import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; class MyCallable implements Callable<Integer> { public Integer call() throws Exception { // Perform tasks return 123; } public static void main(String args[]) throws Exception { FutureTask<Integer> future = new FutureTask<>(new MyCallable()); Thread t1 = new Thread(future); t1.start(); Integer result = future.get(); // 获取结果 } }
-
线程池
使用线程池是执行多线程任务的高效方式。线程池在程序启动时创建一定数量的线程,并在队列中等待任务。这避免了为每个任务创建新线程的开销。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class MyRunnable implements Runnable { public void run() { System.out.println("Pool thread is running."); } public static void main(String args[]) { ExecutorService executor = Executors.newFixedThreadPool(5); executor.execute(new MyRunnable()); executor.shutdown(); } }
总结
- 继承
Thread
类: 简单,但不灵活,因为 Java 不支持多重继承。 - 实现
Runnable
接口: 更灵活,允许你的类继承其他类。 - 使用
Callable
和Future
: 更灵活,可以返回结果和处理异常。 - 线程池: 最高效,用于管理大量线程,减少创建和销毁线程的开销。
1.1.3 线程的状态
在Java中,线程(Thread)在其生命周期中可以处于不同的状态。Java的线程状态由java.lang.Thread.State
枚举定义,这些状态反映了线程在任何给定时刻的活动状态。了解这些状态对于理解和管理多线程程序非常重要。
线程的状态
-
NEW(新建)
- 当线程对象被创建后,但在调用
start()
方法之前,线程处于这个状态。 - 示例:
Thread thread = new Thread();
- 当线程对象被创建后,但在调用
-
RUNNABLE(可运行)
- 在调用线程的
start()
方法之后,线程进入可运行状态。 - 在这个状态下,线程可能正在执行,也可能正在等待操作系统分配执行时间。
- 在Java中,
RUNNABLE
状态包括了传统操作系统线程的“就绪(Ready)”和“运行(Running)”两个状态。
- 在调用线程的
-
BLOCKED(阻塞)
- 当线程试图获取一个锁(synchronized锁),而该锁被其他线程持有,则该线程进入阻塞状态。
- 当持有锁的线程释放锁后,阻塞的线程将变成可运行状态。
-
WAITING(等待)
- 当线程等待另一个线程执行特定操作时,它进入等待状态。
- 进入这个状态的常见方法是调用
Object.wait()
、Thread.join()
或LockSupport.park()
。 - 线程需要被显式地唤醒,通常是由其他线程调用
notify
或notifyAll
方法。
-
TIMED_WAITING(计时等待)
- 类似于等待状态,但有一个最大等待时间。
- 进入这个状态的常见方法是调用
Thread.sleep(long millis)
、Object.wait(long timeout)
、Thread.join(long millis)
或LockSupport.parkNanos()
/parkUntil()
。 - 当等待时间到达或线程被提前唤醒时,线程将变为可运行状态。
-
TERMINATED(终止)
- 当线程的
run()
方法执行完毕后,线程进入终止状态。 - 一旦线程终止,它不能再次启动。
- 当线程的
示例代码
public class ThreadStateExample {
public static void main(String[] args) throws InterruptedException {
// 新建状态
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000); // 让线程暂停一会儿,模拟一些工作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println("State after creation: " + thread.getState());
// 启动线程
thread.start();
System.out.println("State after calling start(): " + thread.getState());
// 等待线程结束
thread.join();
System.out.println("State after completion: " + thread.getState());
}
}
这个示例简单地展示了线程状态的变化。实际的多线程程序可能会更加复杂,线程状态的变化会受到多种因素的影响。理解这些状态及其转换对于编写正确和高效的并发程序至关重要。
1.2 实现线程同步的方案
在 Java 中实现线程同步,主要目的是为了防止多个线程同时访问共享资源而导致的数据不一致性和竞争条件。有几种常用的线程同步机制:
1. 同步方法(Synchronized Method)
使用 synchronized
关键字修饰方法。当线程访问同步方法时,它会锁定该方法所属的对象,防止其他线程同时访问相同对象的任何其他同步方法。
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
2. 同步块(Synchronized Block)
在方法内部,可以通过同步代码块(synchronized block)来同步部分代码。你可以指定一个对象作为锁,只有获得该锁的线程才能执行同步块中的代码。
public class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
}
3. volatile
关键字
volatile
关键字用于标记一个 Java 变量为“存储在主内存”中。它确保了变量的读取和写入都是直接操作在主内存,而不是线程的工作内存。这有助于确保变量的可见性,但不处理并发和同步。
public class Counter {
private volatile int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
4. ReentrantLock
ReentrantLock
是 java.util.concurrent.locks
包中的一个类,提供了比同步方法和同步块更灵活的锁定机制。它允许更细粒度的锁控制。
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
5. 使用 java.util.concurrent
包
Java 的 java.util.concurrent
包提供了许多并发工具类,如 AtomicInteger
, ConcurrentHashMap
等,它们内部实现了线程安全的操作。
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
总结
选择合适的线程同步机制取决于具体场景。如果是简单的操作,使用 synchronized
方法或块通常就足够了。如果需要更高级的特性(如尝试锁定、定时锁定等),ReentrantLock
是更好的选择。而 java.util.concurrent
包中的类则适用于更复杂的并发场景和数据结构。在实现线程同步时,重要的是要确保既保护共享数据的一致性,又尽量减少对性能的影响。
1.3 线程池
1.3.1 线程池的作用
线程池在 Java 中是一种非常有效的执行多线程任务的方式。它主要的作用和优势包括:
-
资源重用
线程池中的线程在执行完任务后不会被销毁,而是可以被再次利用来执行新的任务。这种重用避免了频繁创建和销毁线程的开销,特别是在大量短生命周期的异步任务处理场景中。
-
提高响应速度
由于线程已经预先创建,当新任务到来时,无需等待新线程的创建即可立即执行。这对于系统的响应时间是一个显著的改进。
-
资源控制
线程池允许管理资源的使用,包括线程的数量和使用率。通过配置最大线程数,可以防止因为线程数过多导致的内存消耗过大或 CPU 过度使用。
-
灵活管理
线程池提供了多种参数设置,如核心线程数、最大线程数、存活时间、工作队列等,允许根据具体的应用需求灵活地管理线程。
-
提高系统稳定性
通过对线程数量的限制,线程池可以防止因为线程数量无限制增长而导致的系统资源耗尽问题,从而提高系统的整体稳定性。
-
提供更强大的功能
Java 的线程池通过
ExecutorService
接口提供了诸如任务调度、线程池管理、任务结果追踪等高级功能。
示例:使用线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务给线程池
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker);
}
// 关闭线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("所有任务已完成");
}
}
class WorkerThread implements Runnable {
private String command;
public WorkerThread(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始. Command = " + command);
processCommand();
System.out.println(Thread.currentThread().getName() + " 结束.");
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
在这个例子中,线程池用于执行一系列的工作线程,每个线程执行一个简单的打印任务。
总的来说,线程池是管理和执行多线程任务的理想选择,特别是在需要高效并发执行大量任务的应用程序中。通过线程池,可以显著提高性能、增加资源控制和提升系统稳定性。
1.3.2 线程池的参数
Java 线程池(通常是通过 java.util.concurrent.ThreadPoolExecutor
类实现)的行为可以通过多个参数进行调整和控制。这些参数对于理解和正确使用线程池至关重要。
主要线程池参数
-
核心线程数(Core Pool Size)
- 线程池中的基本线程数,即使它们处于空闲状态,这些线程也不会被终止。
-
最大线程数(Maximum Pool Size)
- 线程池允许的最大线程数。如果阻塞队列满了,并且已创建的线程数小于最大线程数,线程池会创建新的线程来处理任务。
-
工作队列(Work Queue)
- 用于在执行任务之前保存任务的阻塞队列。常用的有
LinkedBlockingQueue
、ArrayBlockingQueue
等。
- 用于在执行任务之前保存任务的阻塞队列。常用的有
-
线程保活时间(Keep-Alive Time)
- 如果当前线程数量超过核心线程数,空闲线程等待新任务的最长时间。超过这个时间,多余的线程将被终止。
-
线程保活时间单位(Time Unit)
- 线程包活时间的单位,包括秒、毫秒。
-
线程工厂(Thread Factory)
- 用于创建新线程的工厂。可以用来自定义线程的名称、优先级等属性。
-
拒绝策略(Rejected Execution Handler)
- 当线程池和工作队列都满了,无法处理更多任务时,所采取的策略。常见的拒绝策略包括
AbortPolicy
(抛出异常)、CallerRunsPolicy
(在调用者线程中运行任务)、DiscardPolicy
(放弃任务)和DiscardOldestPolicy
(放弃队列中最老的任务)。
- 当线程池和工作队列都满了,无法处理更多任务时,所采取的策略。常见的拒绝策略包括
示例:创建线程池
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 5000;
TimeUnit unit = TimeUnit.MILLISECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler);
// 添加任务到线程池
for (int i = 0; i < 15; i++) {
executor.execute(new WorkerThread("" + i));
}
executor.shutdown();
}
}
class WorkerThread implements Runnable {
private String command;
public WorkerThread(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Command = " + command);
processCommand();
System.out.println(Thread.currentThread().getName() + " End.");
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
在这个示例中,创建了一个自定义的线程池,并配置了核心线程数、最大线程数、保活时间等参数。这些参数决定了线程池的行为,包括线程的创建、执行和终止。
正确设置线程池参数对于提高应用程序性能和资源利用率至关重要。不同的任务类型和负载条件可能需要不同的线程池配置。
1.3.3 线程池的类型
Java 提供了几种类型的线程池,主要通过 java.util.concurrent.Executors
类的静态工厂方法来创建。每种类型的线程池都适用于不同的应用场景:
- 固定大小线程池(FixedThreadPool)
- 特点: 线程池中的线程数量固定,所有线程都是在开始时一次性创建的。
- 适用场景: 适用于负载相对平稳的场景,或者任务数量预先已知时。
- 创建方式:
Executors.newFixedThreadPool(int nThreads)
- 缓存线程池(CachedThreadPool)
- 特点: 线程池中的线程数量不固定,根据需要创建新线程,如果线程空闲超过一定时间(默认60秒),则被回收。
- 适用场景: 适用于执行许多短期异步任务的程序,或者负载较轻的服务器。
- 创建方式:
Executors.newCachedThreadPool()
- 单线程线程池(SingleThreadExecutor)
- 特点: 只有一个核心线程的线程池,确保所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
- 适用场景: 适用于需要顺序执行任务的场景,同时确保不会有多个线程活动。
- 创建方式:
Executors.newSingleThreadExecutor()
- 定时线程池(ScheduledThreadPool)
- 特点: 支持定时以及周期性任务执行的线程池。
- 适用场景: 适用于需要多个后台线程执行周期任务,同时作为定时器。
- 创建方式:
Executors.newScheduledThreadPool(int corePoolSize)
- 单线程定时线程池(SingleThreadScheduledExecutor)
- 特点: 类似于
ScheduledThreadPool
,但它只有一个线程用于执行定时任务。 - 适用场景: 适用于需要单个后台线程执行周期任务,同时作为定时器。
- 创建方式:
Executors.newSingleThreadScheduledExecutor()
- 工作窃取线程池(WorkStealingPool)
- 特点: 基于
ForkJoinPool
,这种线程池使用多个队列减少竞争,工作线程可以从其他队列“窃取”任务来执行。 - 适用场景: 适合于很多小任务的执行,尤其是CPU密集型任务。
- 创建方式:
Executors.newWorkStealingPool(int parallelism)
(在 Java 8 中引入)
示例代码
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
ExecutorService workStealingPool = Executors.newWorkStealingPool(4);
选择哪种类型的线程池取决于具体的应用需求,例如任务的性质(CPU密集型、IO密集型)、任务的执行频率以及对线程数量的控制需求等。正确选择线程池可以提高程序的性能、响应速度和资源利用率。
1.3.4 阿里规约(禁止使用Executors工具类来创建线程)
阿里巴巴Java开发手册中建议不要使用 Executors
工具类直接创建线程池,主要是出于以下几个考虑:
-
默认配置问题
Executors
类提供的工厂方法创建的线程池可能不适合生产环境,因为它们使用了一些对于高负载系统不够安全的默认配置。-
固定大小(FixedThreadPool)和单线程执行器(SingleThreadExecutor): 使用了无界的任务队列,意味着如果任务提交的速度超过了处理的速度,队列可能会迅速膨胀,从而导致内存耗尽。
-
无界缓存线程池(CachedThreadPool)和调度线程池(ScheduledThreadPool): 允许创建数量几乎无限的线程,如果任务提交的速度超过线程处理速度,可能会导致创建大量线程,同样会造成系统资源枯竭。
-
-
资源控制
在生产环境中,我们通常希望对系统资源(如线程数量、任务队列大小等)有更精确的控制。
Executors
工具类提供的线程池很难满足这些定制化的需求。 -
性能和稳定性
由于上述的默认配置问题,使用
Executors
创建的线程池可能会在高负载下表现出不稳定的性能,特别是在资源紧张的生产环境中。过多的线程创建和任务积压可能导致系统崩溃或响应缓慢。 -
可定制性
直接使用
ThreadPoolExecutor
构造函数创建线程池,可以显式地指定核心线程数、最大线程数、队列类型、线程工厂、拒绝策略等,从而提供更灵活的线程池配置,更适合不同的业务需求。
结论
总的来说,阿里巴巴Java开发手册中禁止使用 Executors
创建线程池的建议,主要是为了避免因其默认配置带来的资源耗尽风险,并鼓励开发者根据具体业务需求进行线程池的定制,以实现更高效、稳定和可靠的多线程处理。
1.3.5 Tomcat线程池
与普通线程池有区别:超出核心线程数的任务直接开启非核心线程,达到最大线程数后,才进入等待队列。
主打的是放更多的任务进来。
1.4 线程安全
1.4.1 什么是线程安全
线程安全是多线程编程中的一个重要概念,它涉及到在并发环境下对共享资源的访问控制。当多个线程同时访问某个资源(如数据结构、文件等),而不需要通过外部同步手段来防止数据竞争或保证数据一致性时,我们说这个资源或操作是线程安全的。
核心要素
-
原子性(Atomicity): 确保当一个线程正在执行操作时,不会被其他线程中断,直到操作完成。
-
可见性(Visibility): 确保一个线程对共享变量的修改能够及时地被其他线程看到,避免读取到过时的值。
-
有序性(Ordering): 确保程序执行的顺序按照代码的先后顺序进行,防止指令重排。
线程安全问题
在多线程环境中,如果没有适当的同步措施,可能会遇到以下问题:
-
数据竞争(Race Condition): 当多个线程同时访问和修改同一个资源时,最终结果依赖于线程执行的顺序,可能导致数据不一致。
-
死锁(Deadlock): 多个线程因争夺资源而无限期地相互等待。
-
活锁(Livelock)和饥饿(Starvation): 线程虽然没有被阻塞,但无法继续执行,或者某些线程无法获取足够资源执行。
实现线程安全
实现线程安全的方法包括:
-
同步控制(Synchronization): 使用同步块或方法来控制对共享资源的访问。
-
使用线程安全的数据结构: 如
java.util.concurrent
包中的并发集合。 -
使用不可变对象(Immutable Objects): 不可变对象自然是线程安全的,因为它们的状态不能被修改。
-
使用线程局部变量(Thread Local Variables): 确保数据只由单个线程访问和修改。
-
锁机制: 包括重入锁(ReentrantLock)、读写锁(ReadWriteLock)等。
例子
在 Java 中,synchronized
关键字和 java.util.concurrent
包中的类(如 ConcurrentHashMap
, AtomicInteger
)是实现线程安全的常用方式。
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
在这个例子中,increment
方法是线程安全的,因为 synchronized
关键字确保了同一时刻只有一个线程能执行该方法。
总之,线程安全是确保数据在多线程环境中保持一致性和完整性的关键。在设计多线程程序时,考虑线程安全非常重要,以避免潜在的并发问题。
1.4.2 锁
理论
- 悲观锁与乐观锁:
- 悲观锁是一种传统的锁策略,它假设最坏的情况:即在任何时间点,都可能有多个线程试图修改同一数据,因此在数据被访问时需要进行锁定以防止其他线程的访问。悲观锁 更适合写操作频繁的场景,能有效防止数据冲突,但可能导致资源等待和死锁。
- 乐观锁是一种更加现代的锁策略,它假设多个线程之间的冲突是少数情况,只在数据实际更新时才检查是否有冲突。乐观锁 更适合读操作频繁的场景,性能更好,但在数据冲突较多的环境中可能需要重复尝试。
- 自旋锁:当一个线程尝试获取一个自旋锁而该锁已经被其他线程占用时,该线程会在一个循环中不断检查锁是否可用,而不是立即进入休眠状态。这意味着线程会"自旋"(即持续忙等),直到锁变为可用状态。线程在等待锁的过程中保持激活状态,并持续检查锁的状态,这就是所谓的“忙等”或“自旋”。是一种避免频繁操作系统内核态用户态的切换而产生的锁优化思想,由于线程不会进入阻塞状态,因此不涉及上下文切换的开销。在等待锁的过程中,自旋锁会消耗CPU资源。
- 读写锁:
- 读锁:当没有线程持有写锁时,可以由多个读线程同时持有。这意味着读操作可以并行执行,前提是没有写操作。
- 写锁:写锁是排他的。当一个线程持有写锁时,其他线程(无论是读线程还是写线程)都不能访问该资源。写操作会阻塞所有其他的读写操作。
- 适用于读操作多于写操作的数据结构,如缓存系统、配置文件等。在需要同时读取和修改数据的场景中,可以通过读写锁来提高性能。
- 轻/重量级锁:
- 轻量级锁:在线程尝试获取锁时,如果锁未被占用,虚拟机会在对象头上设置一个标志位来表示锁的占用,而不是立即挂起线程。如果锁已被占用,虚拟机会尝试自旋(Spin),在一定次数内循环检查锁是否可以被获取,以避免线程阻塞。轻量级锁减少了线程状态变更的开销,特别是在锁竞争不激烈的情况下。适用于锁竞争不激烈,并且锁持有时间短的情况。
- 重量级锁:当一个线程尝试获取一个已被其他线程持有的锁时,它将被阻塞并进入等待状态。这涉及到操作系统的线程调度和上下文切换。由于涉及到操作系统层面的线程阻塞和唤醒,重量级锁的开销相对较大。在竞争不激烈的情况下,或者锁持有时间相对较长的情况下,重量级锁是合适的。
- 轻量级锁和重量级锁是 JVM 在不同竞争情况下对锁的优化方式。在竞争激烈的环境中,重量级锁因为线程阻塞和唤醒的开销可能导致性能下降;而在竞争不激烈的环境中,轻量级锁和偏向锁提供了更高的性能。理解这些锁的工作原理和适用场景对于编写高效的并发程序非常重要。
- 非/公平锁:
- 公平锁:公平锁遵循严格的先进先出(FIFO)原则,即等待时间最长的线程会被最先允许获取锁。当一个线程请求锁时,如果锁已经被其他线程持有,该线程会被放入一个等待队列中,按照请求的先后顺序等待释放锁的机会。公平锁确保所有等待线程都有机会获取锁,但可能会导致一些线程等待时间较长,可能会出现线程饥饿(Starvation)的情况。
- 非公平锁:非公平锁没有严格的先进先出原则,线程请求锁时,如果锁已经被其他线程持有,那么它有时可以直接获取锁,而不需要等待。非公平锁通常比公平锁具有更低的开销,因为它不需要维护一个等待队列和执行额外的线程切换操作。非公平锁可能会导致某些线程频繁地获取锁,而其他线程很少有机会获得锁,可能会出现不公平的情况。
- 可重入锁/非可重入锁:
- 可重入锁:可重入锁允许同一个线程多次获取同一个锁,而不会导致死锁。当一个线程已经持有该锁时,它可以再次请求该锁,而不会被阻塞,而是会继续执行,每次成功获取锁都会引入一个计数器,线程在释放锁时需要相应地减少计数器。
- 非可重入锁:非可重入锁不允许同一个线程多次获取同一个锁,如果一个线程已经持有该锁,再次请求锁会导致死锁或者阻塞。非可重入锁通常不太常见,因为它在实际应用中会导致编程困难和潜在的问题。
实现
- CAS: Compare-And-Swap 的缩写,它是一种并发编程中常用的原子操作,用于实现多线程之间对共享数据的安全访问。利用CPU的原子性指令,执行一个操作传入原始值及新的值,先比较原始值与传入的值是否一致,如果一致,说明没有被人修改过过,因此就将新的值与传入的值交换,完成新的值设置进去;如果不一致,就说明该值已经被修改过了,因此就直接修改失败(乐观锁、轻量级锁)。性能较高。
- Synchronized:监视器锁,基于操作系统级别的Monitor实现。是可重入锁,Java内置锁,支持Java编译器优化的锁膨胀、锁消除、锁合并。
- 优化:锁升级。无锁->偏向锁->轻量级锁->重量级锁。具体流程如下:
下面是 synchronized 锁升级过程的简要介绍:
1. 无锁状态:
- 初始时,对象的头信息中没有锁记录。
- 如果一个线程尝试获取锁,并且该锁当前没有被其他线程占用,那么这个线程会成功获取锁,对象头的信息会记录这个线程获取了锁。
2. 偏向锁状态(Bias Locking):
- 当只有一个线程访问共享资源时,偏向锁可以提高性能。
- 当一个线程首次尝试获取锁时,偏向锁会记录该线程的 ID,并尝试将锁的标志设置为偏向锁。
- 在后续的访问中,如果同一个线程再次尝试获取锁,它可以直接获取锁,而不需要竞争。
- 如果有其他线程尝试获取锁,偏向锁就会升级为轻量级锁。
3. 轻量级锁状态(Lightweight Locking):
- 当多个线程争夺同一个锁时,锁会升级为轻量级锁。
- 轻量级锁使用 CAS 操作来尝试获取锁,如果成功,则线程可以继续执行,如果失败,则锁会升级为重量级锁。
- 轻量级锁的主要目的是减小锁竞争的性能开销。
4. 重量级锁状态(Heavyweight Locking):
- 如果轻量级锁的 CAS 操作依然无法成功获取锁,锁就会升级为重量级锁。
- 重量级锁会使其他线程进入阻塞状态,直到拥有锁的线程释放锁。
锁升级过程是为了在多线程环境中平衡性能和线程安全。偏向锁和轻量级锁主要用于减小锁的开销,以提高单线程访问的性能。
而当多个线程竞争锁时,锁升级为重量级锁,以确保线程的安全性,但会带来性能开销。
需要注意的是,锁升级的过程是自动发生的,开发者一般不需要显式地管理锁的状态。
Java 虚拟机会根据线程的竞争情况和访问模式自动选择合适的锁状态。
这种锁升级机制使得 synchronized 在不同情况下能够提供合适的性能和线程安全性。
-
ReentrantLock:ReentrantLock 是 Java 中的一个重要的锁实现,它是基于可重入原理的锁,提供了更多的灵活性和控制,相对于 synchronized 关键字,它更加强大。具有可重入性、手动锁定和解锁、条件变量、公平锁和非公平锁、支持中断、性能和灵活性。
-
Synchronized和ReentrantLock的区别:
- Synchronized是Java中的关键字,ReentrantLock是基于JUC并发包中的AQS实现的
- 两者都支持可重入
- Synchronized仅支持非公平锁,ReentrantLock都支持
- Synchronized不支持条件,ReentrantLock支持Condition条件变量
1.4.3 ThreadLocal
ThreadLocal
是 Java 中的一个类,用于在多线程环境中实现线程本地变量。线程本地变量是指每个线程都拥有自己独立的变量副本,不同线程之间互不干扰。ThreadLocal
主要用于将某个对象与当前线程关联起来,以便在整个线程的生命周期内对这个对象进行访问。
以下是关于 ThreadLocal
的一些重要信息:
-
创建 ThreadLocal 对象:
- 可以通过直接实例化
ThreadLocal
类或使用其子类InheritableThreadLocal
来创建一个ThreadLocal
对象。
- 可以通过直接实例化
-
设置和获取值:
- 使用
set(T value)
方法来设置当前线程的局部变量的值。 - 使用
get()
方法来获取当前线程的局部变量的值。
- 使用
-
线程间隔离:
- 每个线程都有自己的
ThreadLocal
实例,因此对于同一个ThreadLocal
对象,不同线程之间的数据是相互隔离的,不会互相影响。
- 每个线程都有自己的
-
使用场景:
ThreadLocal
适用于需要在线程之间保存数据,但不希望将数据暴露给其他线程的情况。常见的用例包括线程安全的日期格式化、数据库连接、用户身份验证等。
-
内存泄漏风险:
- 使用不当会导致内存泄漏。如果将一个长生命周期的对象绑定到
ThreadLocal
并忘记解除绑定,这个对象将一直存在于线程的局部变量中,无法被垃圾回收。
- 使用不当会导致内存泄漏。如果将一个长生命周期的对象绑定到
-
清理 ThreadLocal:
- 为了避免内存泄漏,应该在不再需要的时候调用
remove()
方法来清理ThreadLocal
绑定的对象。
- 为了避免内存泄漏,应该在不再需要的时候调用
以下是一个简单的示例,演示了如何使用 ThreadLocal
:
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
threadLocal.set(42);
Runnable task = () -> {
int value = threadLocal.get();
System.out.println("Thread-local value: " + value);
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
在上面的示例中,ThreadLocal
被用来存储整数值,并且每个线程都有自己独立的值。两个线程分别访问 ThreadLocal
存储的值,互不干扰。这是 ThreadLocal
的典型用法之一。
1.4.4 Object(扩展)
1.5 JUC(扩展)
2. JVM
2.1 内存模型
2.1.1 线程私有
- 虚拟机栈:虚拟机栈(Java Virtual Machine Stack),通常简称为栈,是 Java 虚拟机中的一个关键组成部分之一,用于管理线程的方法调用和局部变量。每个线程在运行过程中都会拥有自己的虚拟机栈,用来保存方法调用的信息和局部变量。
- 本地方法栈:本地方法栈(Native Method Stack)是 Java 虚拟机中的一个重要组成部分,它用于执行本地方法(Native Method),也就是用本地语言(通常是C、C++等)编写的方法或函数。本地方法是通过 Java 的本地接口(Java Native Interface,JNI)来调用的,允许 Java 代码与底层的本地代码进行交互。
- 程序计数器:程序计数器(Program Counter),通常缩写为 PC,是计算机体系结构中的一个寄存器,用于存储正在执行的程序的地址或下一条要执行的指令的地址。在 Java 虚拟机中,程序计数器具有特殊的含义和作用。
2.1.2 线程共享
- 堆:Java 堆(Java Heap)是 Java 虚拟机(JVM)中的一个重要组成部分,它是用来存储对象实例的内存区域。Java 堆是 Java 程序中最大的内存区域之一,也是垃圾回收的主要工作区域之一。
- 年轻代:
- 伊甸园区
- 幸存者1区(from)
- 幸存者2区 (to)
- 老年代:
- 年轻代:
- 方法区(永久代、元空间):方法区(Method Area),在 Java 8 及之前的版本中也称为永久代(Permanent Generation),是 Java 虚拟机(JVM)中的一个重要组成部分,用于存储类的信息、静态变量、常量池、方法字节码等数据。方法区通常是在堆内存之外分配的,它也是 Java 内存管理的一部分。
2.2 垃圾回收
2.2.1 GC分类
- FGC:FGC(Full Garbage Collection)是指全垃圾回收,也称为全堆垃圾回收或整堆垃圾回收。它是一种垃圾回收过程,用于回收整个堆内存中的所有不再被引用的对象,以释放内存空间。通常是包括新生代和老年代的整堆内存的回收。
- YGC:YGC(Young Generation Garbage Collection)是指年轻代垃圾回收,它是 Java 堆内存中新生代(Young Generation)部分的垃圾回收过程。新生代是堆内存的一部分,用于存储新创建的对象。YGC主要负责回收新生代中不再被引用的对象,以释放内存空间,使得新对象能够继续分配在新生代中。
2.2.2 垃圾收集算法
- 标记-清除:在标记阶段,垃圾回收器会标记出所有还存活的对象。在清除阶段,回收器会删除所有未标记的对象,释放内存空间。缺点:会产生内存碎片,且回收过程较为耗时。
- 复制:复制算法将堆内存分为两个区域:一个存放活跃对象(From区),另一个用于存放复制后的对象(To区)。当From区满时,回收器将活跃对象复制到To区,同时清空From区。优点:快速,避免了内存碎片问题。适用于新生代的垃圾回收。
- 标记-整理:标记-整理算法结合了标记-清除和复制算法的优点。在标记阶段,垃圾回收器标记出所有还存活的对象。然后,它会将所有存活对象压缩到一端,清除掉未标记的对象,从而消除内存碎片。优点:避免了内存碎片问题,相对高效。适用于老年代的垃圾回收。
- 分代收集:分代回收算法根据对象的生命周期将堆内存划分为不同的代,通常包括新生代、老年代和永久代/元空间。新生代中的对象生命周期短暂,采用复制算法。老年代中的对象生命周期较长,采用标记-整理算法。通过将不同代的垃圾回收算法结合使用,提高了垃圾回收效率。
- 并发CMS(Concurrent Mark-Sweep)标记:CMS垃圾回收算法是一种低停顿时间的算法,它使用多线程并发标记和清除未使用的对象。适用于要求低停顿时间的应用程序,但可能会产生内存碎片。
2.2.3 垃圾收集器(拓展)
- CMS:CMS收集器是一种低停顿时间的垃圾收集器,适用于需要快速响应的应用程序。它使用标记-清除算法,并且在标记和清除阶段尽量与应用程序并发执行,以减少停顿时间。由于不进行复制和整理操作,可能会产生内存碎片。
- G1:G1收集器是一种面向服务端应用的垃圾收集器,旨在提供低停顿时间和高吞吐量。它将堆划分为多个区域,并使用标记-整理算法。它可以根据应用程序需求调整垃圾回收优先级。G1收集器通常比CMS更稳定,不容易出现Full GC。
- ZGC:ZGC是一种低停顿时间的垃圾收集器,旨在实现几乎无停顿的垃圾回收。它使用标记-整理算法,并且在标记和清除阶段进行并发执行,以减小停顿时间。ZGC适用于需要极低停顿时间的应用程序,但它在吞吐量上可能不如其他收集器高。
3. 异常体系
Java中的异常体系是一种用于处理程序中出现错误或异常情况的机制。异常是指程序在执行过程中遇到的不正常情况,如运行时错误、用户输入错误、资源不足等。Java的异常体系有助于提高代码的健壮性,使程序能够更好地处理异常情况,而不会导致程序崩溃。
Java中的异常体系包括以下重要概念和组件:
-
Throwable类:Throwable是所有异常的根类,它有两个子类:Error和Exception。Error表示严重的系统错误,通常无法通过代码来处理,例如OutOfMemoryError。Exception表示可捕获和处理的异常,分为受检查异常(Checked Exception)和运行时异常(Runtime Exception)两种。
-
Exception类:Exception类是受检查异常的根类,它的子类包括IOException、SQLException等。受检查异常在方法签名中必须声明或捕获,否则编译器会报错。
-
RuntimeException类:RuntimeException类是运行时异常的根类,它的子类包括NullPointerException、ArrayIndexOutOfBoundsException等。运行时异常通常是由程序逻辑错误导致的,编译器不要求显式捕获或声明。
-
try-catch块:try-catch块用于捕获和处理异常。在try块中编写可能引发异常的代码,然后在catch块中捕获并处理异常。一个try块可以包含多个catch块,每个catch块捕获不同类型的异常。
-
finally块:finally块用于包含在try-catch块之后,它中的代码无论是否发生异常都会被执行。通常用于清理资源、关闭文件或执行必要的清理操作。
-
throw关键字:throw关键字用于手动抛出异常。开发人员可以使用throw来抛出自定义异常或重新抛出已捕获的异常。
-
throws关键字:throws关键字用于在方法声明中指定可能抛出的受检查异常。调用该方法的代码必须显式处理这些异常,或者再次声明抛出。
-
自定义异常:开发人员可以创建自定义异常类,通过继承Exception或RuntimeException来定义自己的异常类型,以更好地满足特定应用程序的需求。
异常处理的目标是优雅地处理异常情况,而不是简单地终止程序。通过合理使用try-catch块、throws声明和自定义异常,可以使程序更加可维护和稳定。捕获和处理异常有助于提高代码的可靠性,减少应用程序崩溃的风险。
4. 新特性
4.1 Lambda表达式(8)
Lambda表达式是Java 8引入的一项重要特性,它允许您以一种更简洁和函数式的方式来表示匿名函数(函数没有名字的方法)。Lambda表达式的引入使得在Java中编写更具可读性和简洁性的代码变得更容易。
Lambda表达式的基本语法如下:
(parameter) -> expression
其中,(parameter)
定义了参数列表,->
表示 Lambda 表达式的箭头,expression
是 Lambda 表达式的主体,它可以是一个表达式或一个代码块。
Lambda表达式的主要特点和用途包括:
-
简化匿名类:Lambda表达式可以替代一些需要创建匿名内部类的情况,使代码更简洁。
// 使用匿名内部类 Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Hello, World!"); } }; // 使用Lambda表达式 Runnable runnable = () -> System.out.println("Hello, World!");
-
函数式编程:Lambda表达式支持函数式编程,使得函数可以作为参数传递给方法,或者将函数作为返回值,从而实现更高级的抽象。
// 使用Lambda表达式作为参数传递给方法 List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.forEach(name -> System.out.println(name)); // 使用Lambda表达式返回一个函数 Function<Integer, Integer> square = x -> x * x;
-
简化集合操作:Lambda表达式可以用于简化集合的操作,例如过滤、映射、排序等。
// 使用Lambda表达式过滤集合 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> evenNumbers = numbers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList()); // 使用Lambda表达式映射集合 List<String> words = Arrays.asList("apple", "banana", "cherry"); List<Integer> wordLengths = words.stream().map(s -> s.length()).collect(Collectors.toList());
-
闭包:Lambda表达式可以访问其外部范围的变量,这使得它们可以像闭包一样捕获外部状态。
int x = 10; Function<Integer, Integer> addX = y -> x + y; int result = addX.apply(5); // 结果是15,Lambda捕获了外部变量x
需要注意的是,Lambda表达式引入了新的函数式编程概念,但不是所有情况都适合使用Lambda。它适用于简化代码、提高可读性以及实现函数式编程的场景。同时,Lambda表达式也需要理解Java的函数式接口(Functional Interface)概念,函数式接口是只包含一个抽象方法的接口,Lambda表达式可以赋值给这种接口的变量。
4.2 Stream常用方法(8)
Java 8引入的Stream API是一个强大的用于处理集合数据的工具,它提供了一种更流畅和函数式的方式来进行数据处理和操作。Stream API支持链式调用,允许您在不修改原始数据的情况下对数据进行各种转换、筛选和聚合操作。
以下是一些关键概念和用法示例,以帮助您了解Java 8的Stream API:
-
创建Stream:
-
从集合创建Stream:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); Stream<String> nameStream = names.stream();
-
使用Stream.of()创建Stream:
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);
-
使用Stream.generate()或Stream.iterate()创建无限流:
Stream<Integer> infiniteStream = Stream.generate(() -> 1); Stream<Integer> naturalNumbers = Stream.iterate(1, n -> n + 1);
-
-
中间操作:中间操作是对Stream进行转换和处理的操作,它们不会立即执行,而是返回一个新的Stream。
filter(Predicate<T> predicate)
:根据条件筛选元素。map(Function<T, R> mapper)
:对每个元素应用映射函数。distinct()
:去除重复元素。sorted()
:对元素进行排序。limit(long maxSize)
:截取Stream的前N个元素。skip(long n)
:跳过Stream的前N个元素。
-
终端操作:终端操作是对Stream执行最终操作,触发Stream的处理和计算。
forEach(Consumer<T> action)
:对每个元素执行操作。toArray()
:将Stream转换为数组。collect(Collector<T, A, R> collector)
:将Stream元素收集到集合中。min(Comparator<T> comparator)
和max(Comparator<T> comparator)
:查找最小和最大值。count()
:计算元素个数。anyMatch(Predicate<T> predicate)
、allMatch(Predicate<T> predicate)
和noneMatch(Predicate<T> predicate)
:检查元素是否满足条件。findFirst()
和findAny()
:查找第一个或任意一个元素。
-
并行处理:Stream API支持并行处理,可以通过
parallel()
方法将Stream转换为并行流,以提高性能。Stream<String> names = Arrays.asList("Alice", "Bob", "Charlie").stream(); Stream<String> parallelNames = names.parallel(); // 转换为并行流
-
自定义Collector:您可以使用
Collectors
工具类创建自定义的收集器,以满足特定需求。List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); String concatenated = names.stream().collect(Collectors.joining(", "));
Stream API是Java 8引入的重要功能之一,它可以大大简化对集合数据的处理和操作,提高了代码的可读性和表达性。了解如何使用Stream API对数据进行筛选、转换、聚合和并行处理将有助于更有效地编写Java代码。
4.3 新的日期API(8)
Java 8引入了新的日期和时间API,以替代旧的java.util.Date
和java.util.Calendar
类,这个新的API被称为"java.time"或"新日期时间API"。这个新的日期时间API提供了更多功能,更容易使用的日期和时间处理方法,同时修复了旧API中的许多设计缺陷和问题。以下是Java 8中的新日期类和一些重要的用法示例:
-
LocalDate:表示日期,不包含时间信息。
LocalDate date = LocalDate.now(); // 获取当前日期 LocalDate customDate = LocalDate.of(2022, 1, 15); // 创建自定义日期
-
LocalTime:表示时间,不包含日期信息。
LocalTime time = LocalTime.now(); // 获取当前时间 LocalTime customTime = LocalTime.of(14, 30); // 创建自定义时间
-
LocalDateTime:表示日期和时间,不包含时区信息。
LocalDateTime dateTime = LocalDateTime.now(); // 获取当前日期和时间 LocalDateTime customDateTime = LocalDateTime.of(2022, 1, 15, 14, 30); // 创建自定义日期和时间
-
ZonedDateTime:表示带时区的日期和时间。
ZoneId zoneId = ZoneId.of("America/New_York"); ZonedDateTime zonedDateTime = ZonedDateTime.now(zoneId); // 获取指定时区的当前日期和时间
-
Duration:表示时间段,用于计算两个时间点之间的时间差。
LocalDateTime start = LocalDateTime.of(2022, 1, 15, 14, 30); LocalDateTime end = LocalDateTime.of(2022, 1, 16, 16, 45); Duration duration = Duration.between(start, end);
-
Period:表示日期间隔,用于计算两个日期之间的日期差。
LocalDate startDate = LocalDate.of(2022, 1, 15); LocalDate endDate = LocalDate.of(2022, 1, 20); Period period = Period.between(startDate, endDate);
-
DateTimeFormatter:用于格式化和解析日期时间对象。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String formattedDateTime = dateTime.format(formatter); LocalDateTime parsedDateTime = LocalDateTime.parse("2022-01-15 14:30:00", formatter);
-
TemporalAdjusters:提供了各种日期调整策略,例如获取下一个周一或下一个月的第一天。
LocalDate nextMonday = date.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); LocalDate firstDayOfMonth = date.with(TemporalAdjusters.firstDayOfMonth());
新日期时间API使日期和时间处理更加直观和易用,并提供了更多灵活的操作方式。它还更好地处理了时区和夏令时等问题,使得在不同地区的应用程序中处理日期和时间更为方便。因此,推荐在Java 8及更高版本中使用新日期时间API来处理日期和时间。
4.4 Switch语法优化(12)
Java 12引入了一个新的语言特性,称为"Switch表达式"或"增强型Switch语句",旨在提高Switch语句的可读性和灵活性,并允许它用作表达式而不仅仅是语句。这个特性在Java 12中是一个预览功能,需要在编译时启用。
传统的Switch语句在Java中是一种流程控制结构,它通常用于根据不同的情况执行不同的代码块。传统的Switch语句看起来像这样:
int dayOfWeek = 3;
String dayName;
switch (dayOfWeek) {
case 1:
dayName = "Monday";
break;
case 2:
dayName = "Tuesday";
break;
// ...
default:
dayName = "Unknown";
}
在Java 12中,引入了Switch表达式,它允许将Switch语句用作表达式,并更加紧凑和清晰地编写类似的代码。Switch表达式的语法看起来像这样:
int dayOfWeek = 3;
String dayName = switch (dayOfWeek) {
case 1 -> "Monday";
case 2 -> "Tuesday";
// ...
default -> "Unknown";
};
Switch表达式中的箭头->
用于指定每个情况下的返回值,而不需要显式地使用break
来终止每个情况。此外,Switch表达式支持使用yield
关键字来返回值,允许在一个分支中进行多个计算并返回最终结果:
int dayOfWeek = 3;
String dayName = switch (dayOfWeek) {
case 1, 2 -> "Monday or Tuesday";
case 3, 4 -> "Wednesday or Thursday";
default -> {
String result = "Unknown";
yield result;
}
};
Switch表达式还提供了更丰富的功能,如支持模式匹配、类型判断和空安全性等,使得在编写更复杂的Switch逻辑时更加方便。
需要注意的是,Switch表达式在Java 12中是一个预览功能,如果要使用它,需要在编译时启用预览功能,可以使用--enable-preview
编译选项,以及在运行时使用--enable-preview
命令行选项。此外,Switch表达式在Java 13和以后的版本中已成为正式功能,并在这些版本中无需额外的启用选项。
4.5 模块化(9)
Java 9引入了一项重大的变化,即模块化系统(Module System)。模块化系统旨在帮助开发人员更好地管理和组织Java应用程序的代码,以及改进Java平台的可伸缩性、可维护性和安全性。
以下是关于Java 9的模块化系统的重要概念和特点:
-
模块(Module):模块是Java 9中的新概念,它是一种组织代码的方式,可以包含类、接口、资源文件等。每个模块都有一个唯一的名称,例如
java.base
是Java SE平台的基本模块。 -
模块路径(Module Path):模块路径是指编译器和运行时查找模块的路径,可以包含多个模块。与传统的类路径不同,模块路径明确了每个模块的依赖关系,使得模块之间的关系更加清晰。
-
模块描述文件(Module Descriptor):每个模块都需要一个
module-info.java
文件,它包含了模块的元数据信息,包括模块名称、依赖关系、导出的包等。module com.example.mymodule { requires java.base; exports com.example.mypackage; }
-
依赖性管理:模块化系统引入了
requires
关键字,用于声明一个模块对其他模块的依赖关系。这有助于更好地控制应用程序的依赖性,并减少了类路径冲突和版本冲突的问题。 -
可重用性和可隔离性:模块化系统鼓励开发人员将代码组织成独立的模块,使得模块可以在不同的项目中重用,并且不容易受到其他模块的影响。
-
模块化JDK:Java 9将JDK本身模块化,将核心库和工具模块化成多个独立的模块。这有助于减小JRE的大小,并提高了Java平台的可维护性。
-
命名空间隔离:模块化系统引入了命名空间隔离,不同模块之间的类和资源在命名空间上是隔离的,避免了类冲突。
-
弃用和移除:Java 9通过模块化系统更容易地标记弃用的API,并允许将不推荐使用的API从模块中移除。
尽管Java 9的模块化系统引入了一些重要的变化,但它仍然兼容旧的非模块化代码,使得现有的Java应用程序可以平稳过渡。模块化系统提供了更好的代码组织和依赖管理,有助于提高应用程序的可维护性和安全性。然而,它也需要开发人员学习新的概念和技能,以充分利用这一特性。
4.6 ZGC(11)
Java 11引入了一种名为Z Garbage Collector(ZGC)的新型垃圾收集器,它是Java虚拟机(JVM)中的一项重要改进。ZGC旨在提供低停顿时间和高吞吐量的垃圾收集,并且适用于大型内存堆和高并发应用程序。
以下是Java 11的ZGC的一些重要特点和特性:
-
低停顿时间:ZGC的主要目标之一是降低垃圾回收导致的停顿时间。它采用了一种并发的算法,允许垃圾收集器与应用程序线程并发执行,从而最大程度地减小了停顿时间。
-
可预测的性能:ZGC的性能稳定且可预测,大多数垃圾收集暂停时间都非常短。这对于需要低延迟的应用程序非常有用,如金融交易系统和实时游戏。
-
大内存支持:ZGC设计用于处理大内存堆,可以支持数百GB甚至TB级别的堆大小。这对于大规模的数据处理和高性能计算应用程序非常重要。
-
透明:ZGC是一种"低干扰"的收集器,几乎不会导致长时间的停顿。这意味着应用程序几乎不需要额外的调整就可以开始使用ZGC。
-
垃圾回收线程数自动管理:ZGC会自动管理垃圾回收线程的数量,以适应应用程序的需求。这减少了需要手动调整线程数的工作。
-
全局并发阶段:ZGC的垃圾收集过程中有一个全局的并发阶段,这是该收集器的核心特性之一。这个阶段涵盖了整个收集过程,包括标记、压缩、处理引用等。
-
实验性特性:需要注意的是,Java 11中的ZGC在发布时是一项实验性特性。尽管它在许多情况下表现良好,但在特定情况下可能会有性能问题,因此建议在生产环境中进行充分测试。
总的来说,Java 11的ZGC是一个面向低停顿时间和大内存应用程序的重要垃圾收集器。它的引入有助于提高Java应用程序的可用性和性能,并为需要低延迟的应用提供了更好的支持。但需要注意的是,使用ZGC时,仍然需要监测和调整应用程序的性能,以确保它符合预期。