基础概念
Java 线程是并发编程的基础,涉及到线程的创建、管理、同步以及通信。理解和掌握线程的使用对于编写高效和响应快速的应用程序至关重要。
1. 线程基础
线程是程序中的执行流。每个Java程序至少有一个线程 — 主线程(main)。通过使用Thread类和Runnable接口,可以在Java中创建和管理线程。
线程创建
有两种主要方式来创建线程:
继承Thread类:创建一个新的类继承Thread类,并重写其run()方法。
实现Runnable接口:创建一个实现了Runnable接口的类,然后将其实例传递给Thread类的构造函数。
// 通过继承Thread类创建线程
class MyThread extends Thread {public void run() {System.out.println("Thread running: " + Thread.currentThread().getName());}
}// 通过实现Runnable接口创建线程
class MyRunnable implements Runnable {public void run() {System.out.println("Runnable running: " + Thread.currentThread().getName());}
}public class ThreadExample {public static void main(String[] args) {MyThread t1 = new MyThread();t1.start();Thread t2 = new Thread(new MyRunnable());t2.start();}
}
2. 线程状态
Java线程可以处于以下状态之一:
新建(New):线程实例已被创建,但尚未开始执行。
可运行(Runnable):线程正在Java虚拟机中执行,但它可能正在等待操作系统的其他资源(如CPU分配)。
阻塞(Blocked):线程被阻止,无法进行,因为它正在等待一个监视器锁。
等待(Waiting):线程通过调用wait()、join()或LockSupport.park()进入等待状态。
计时等待(Timed Waiting):类似于等待状态,但它会在指定的时间自动返回。
终止(Terminated):线程已完成执行或因异常退出。
3. 线程同步
为了防止多个线程访问共享资源而引发数据不一致的问题,需要进行线程同步。
synchronized
synchronized关键字可以用来同步方法或代码块。同步方法或代码块确保一次只有一个线程可以执行它们。
public class Counter {private int count = 0;// 同步方法public synchronized void increment() {count++;}public int getCount() {return count;}
}
4. 线程通信
线程之间的通信常通过等待/通知机制实现 —— 即wait()、notify()和notifyAll()方法。
public class WaitNotifyExample {public static void main(String[] args) {final Object lock = new Object();Thread thread1 = new Thread(() -> {synchronized (lock) {try {System.out.println("Thread 1: Waiting for lock");lock.wait();System.out.println("Thread 1: Woken up");} catch (InterruptedException e) {e.printStackTrace();}}});Thread thread2 = new Thread(() -> {synchronized (lock) {System.out.println("Thread 2: Sleeping for 1 second");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread 2: Notifying");lock.notify();}});thread1.start();thread2.start();}
}
5. 并发工具
Java还提供了如ExecutorService, CountDownLatch, CyclicBarrier, Semaphore, Future, Callable等高级并发工具,这些都是构建复杂的并发应用程序时不可或缺的工具。
进阶概念
1. 线程池(Executor Framework)
线程池是一种基于池化技术的线程管理解决方案。使用线程池可以减少在创建和销毁线程上所花的开销,同时可以提供对并发任务执行的细粒度控制。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class ThreadPoolExample {public static void main(String[] args) {// 创建固定大小的线程池ExecutorService executor = Executors.newFixedThreadPool(4);// 提交任务给线程池for (int i = 0; i < 10; i++) {int taskNumber = i;executor.submit(() -> {System.out.println("Executing Task " + taskNumber + " by " + Thread.currentThread().getName());});}// 关闭线程池executor.shutdown();try {executor.awaitTermination(1, TimeUnit.DAYS);} catch (InterruptedException e) {e.printStackTrace();}}
}
2. 并发集合
Java 提供了多种线程安全的集合类,如 ConcurrentHashMap, CopyOnWriteArrayList 等。这些集合类优化了多线程环境下的性能,减少了锁的竞争。
import java.util.concurrent.ConcurrentHashMap;public class ConcurrentMapExample {public static void main(String[] args) {ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();// 线程安全地更新 ConcurrentHashMapmap.put("key", "initial value");map.compute("key", (k, v) -> v + ", updated value");System.out.println(map.get("key"));}
}
3. 原子变量
Java 的 java.util.concurrent.atomic 包提供了一系列的原子类,用于在无锁的情况下进行线程安全的操作,这些操作包括单一变量的读取、写入、更新等。
import java.util.concurrent.atomic.AtomicInteger;public class AtomicExample {public static void main(String[] args) {AtomicInteger atomicInteger = new AtomicInteger(0);// 原子方式更新整数atomicInteger.incrementAndGet(); // 增加1System.out.println("Current Value: " + atomicInteger.get());}
}
4. CompletableFuture
CompletableFuture 提供了一个非阻塞的方式来处理并发编程。通过CompletableFuture,你可以编写异步的、非阻塞的代码,处理异步的计算结果。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;public class CompletableFutureExample {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new IllegalStateException(e);}return "Result of the asynchronous computation";});// 获取结果,等待异步操作结束String result = completableFuture.get();System.out.println(result);}
}
5. 高级同步技术
学习如 CyclicBarrier, Semaphore, 和 Phaser 等同步辅助类,这些类可以协调多个线程间复杂的交互和同步。
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.BrokenBarrierException;public class CyclicBarrierExample {private static class Task implements Runnable {private CyclicBarrier barrier;public Task(CyclicBarrier barrier) {this.barrier = barrier;}public void run() {try {System.out.println(Thread.currentThread().getName() + " is waiting on barrier");barrier.await();System.out.println(Thread.currentThread().getName() + " has crossed the barrier");} catch (InterruptedException | BrokenBarrierException e) {e.printStackTrace();}}}public static void main(String[] args) {final CyclicBarrier cb = new CyclicBarrier(3, () -> System.out.println("All parties are arrived at barrier, lets play"));Thread t1 = new Thread(new Task(cb), "Thread 1");Thread t2 = new Thread(new Task(cb), "Thread 2");Thread t3 = new Thread(new Task(cb), "Thread 3");t1.start();t2.start();t3.start();}
}
面试相关问题
1. Java中创建线程的方法有哪些?
在Java中,有两种主要的方式来创建线程:
继承Thread类:创建一个类继承Thread类,并重写run()方法。然后创建这个类的实例并调用其start()方法来启动新线程。
实现Runnable接口:创建一个实现了Runnable接口的类,实现run()方法。然后创建这个类的实例,把它作为参数传递给Thread类的构造器,然后调用Thread的start()方法来启动线程。
2. 什么是线程安全?请举例说明。
线程安全意味着在多线程环境中,无论运行时操作系统如何调度这些线程,以及线程如何交替执行,代码都能表现出正确的行为。换句话说,线程安全的代码能够保护数据免受多个线程同时执行时的冲突和错误状态。
例如,java.util.concurrent.ConcurrentHashMap是线程安全的,它可以在不进行额外同步的情况下被多个线程同时访问和修改,而java.util.HashMap就不是线程安全的。
3. 解释synchronized关键字及其工作原理。
synchronized是Java中的一个关键字,用于加锁,以便在同一时刻只允许一个线程访问关键部分代码。它可以用于同步方法和同步块。
当synchronized用于方法时,锁是对当前对象实例加锁。
当synchronized用于静态方法时,锁是对当前类的Class对象加锁。
当synchronized用于代码块时,它必须给定一个对象引用,锁是对这个对象引用加锁。
synchronized确保只有持有对象锁的线程才能执行同步代码,从而防止多个线程同时执行相同代码块造成的数据不一致问题。
4. 什么是死锁?如何避免死锁?
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种僵局,每个线程都在等待其他线程释放它所需的资源。为了避免死锁,可以采用以下措施:
避免多个线程同时持有多个锁:尽量确保每个线程一次只持有一个锁。
锁顺序:规定一个全局的锁的顺序,并且按照这个顺序来获取锁。
使用tryLock():使用带有超时的tryLock()方法,这样线程在等待锁的时候,如果不能立即得到锁,就会放弃、回退、然后重新尝试,从而减少死锁的可能性。
使用死锁检测工具:利用一些工具来检测和恢复死锁。
5. 解释Java中的volatile关键字。
volatile是Java语言提供的最轻量级的同步机制。用volatile声明的变量可以确保每次读取都从主存中进行,每次写入都到主存中,从而保证了该变量的可见性。volatile并不执行互斥访问,但它禁止指令重排序优化,使得赋值操作不会与之前的写操作重排序,也不会与之后的读操作重排序。
总体知识
1. Java内存模型(Java Memory Model, JMM)
理解Java内存模型是至关重要的,因为它定义了线程如何通过内存进行交互。Java内存模型确保不同线程间的可见性和有序性,通过使用volatile、synchronized等关键字实现。
可见性:保证一个线程对共享变量的修改,对于其他线程是可见的。
原子性:操作在其他线程看来是不可分割的。
有序性:保证程序执行的顺序按照代码的先后顺序执行。
Java 内存模型(JMM,Java Memory Model)是理解 Java 多线程编程中线程间通信的核心概念。JMM 定义了线程和主内存之间的抽象关系,并规定了线程如何以及何时可以看到由其他线程修改过的共享变量的值,以及如何同步访问共享变量。
1. JMM的主要目标
JMM的设计目的主要是为了解决可见性、原子性和有序性这三个问题。
可见性:一个线程对共享变量的修改,其他线程能够立即得知这个修改。
原子性:操作在多个线程中不可被中断的执行过程。
有序性:程序执行的顺序按照代码的先后顺序执行。
2. 内存模型的组成
JMM 把变量存储分为主内存和工作内存两种:
主内存(Main Memory):主内存直接与Java堆相关,存储实例字段、静态字段和构成数组对象的元素等。
工作内存(Working Memory):每个线程都有自己的工作内存,它包含了该线程用到的变量的主内存副本拷贝。
3. 内存交互操作
JMM定义了8种操作来完成主内存与工作内存之间的交互:
lock(锁定):作用于主内存的变量,它标记一个变量被一个线程独占状态。
unlock(解锁):作用于主内存的变量,它标记一个变量结束独占状态。
read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
4. 内存可见性保证
Java内存模型通过volatile、synchronized和final等关键字来提供内存可见性保证。
volatile:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个volatile变量的值,这新值对其他线程来说是立即可见的,并禁止指令重排。
synchronized:保证了同一时刻只有一个线程可以执行同步代码段,并且入口前必须获得锁,退出时必须释放锁,从而保证了锁内语句对变量的修改对所有线程都可见。
final:被final修饰的字段,线程安全性得到了保证,对final字段的写入早于对这个字段的首次读取。
5. 指令重排
JMM 允许编译器和处理器对操作指令进行重排序,只要重排序不改变单线程程序的执行结果。这样做是为了提高性能。但在多线程环境下,这种重排序可能会导致严重问题。volatile和synchronized可以在一定程度上约束JVM和Java程序的重排序行为。
2. 锁优化
理解并能实现各种锁优化技术是必需的:
轻量级锁:JVM在运行时对于无竞争的锁进行优化,避免重量级的操作系统互斥元操作。
偏向锁:一种优化技术,假设最后一个获得锁的线程将频繁锁定同一个对象。
锁消除:JVM优化去除不可能共享数据之间的锁操作。
锁粗化:将多个连续的锁扩展为一个更大范围的锁,以减少锁获取的次数。
在Java中,锁是同步不同线程间对共享资源访问的一种机制。为了提高性能,Java虚拟机(JVM)提供了多种锁优化技术。理解这些锁优化方法对于编写高效的多线程代码非常重要。
1. 偏向锁(Biased Locking)
偏向锁是一种锁优化技术,它假设锁会被同一个线程多次重复获取。当锁被一个线程获取之后,JVM会在锁对象的头部标记这个线程的ID,之后这个线程再次请求锁时,JVM只需简单检查这个ID,无需进行完整的锁获取过程。
优点:减少了锁获取的开销,适用于只有单一线程访问同步块的情况。
缺点:如果多个线程交替竞争同一个锁,偏向锁会被撤销,可能增加额外的撤销成本。
2. 轻量级锁(Lightweight Locking)
当偏向锁被撤销,但锁竞争不激烈时,JVM会使用轻量级锁。轻量级锁通过在栈帧中创建锁记录(Lock Record)来存储锁标记。如果没有竞争,线程可以通过CAS(Compare-And-Swap)操作将锁对象的头部指向锁记录来获取锁。
优点:避免了在无竞争情况下的重量级操作系统互斥量的使用。
缺点:在有竞争的情况下,线程需要自旋等待锁释放,可能导致CPU资源浪费。
3. 重量级锁(Heavyweight Locking)
如果轻量级锁的自旋失败,即存在锁竞争,JVM将升级锁为重量级锁。重量级锁依赖于操作系统的互斥量(mutex)来实现线程的阻塞和唤醒,以控制对临界区的访问。
优点:适用于锁竞争激烈的情况,有效避免了线程的无限自旋。
缺点:依赖操作系统功能,涉及到用户态到内核态的切换,性能开销较大。
4. 锁粗化(Lock Coarsening)
如果JVM检测到一系列的连续锁操作(在循环中频繁地加锁和解锁同一个对象),它可能会将锁的范围扩大(粗化),以包括整个操作序列,从而减少锁获取和释放的次数。
优点:减少锁操作的总次数,提高执行效率。
缺点:如果操作序列中包含不需要同步的操作,可能导致不必要的性能损失。
5. 锁消除(Lock Elimination)
JVM通过逃逸分析来检测某些锁操作是否是多余的。如果JVM确定一段代码中的锁对象不可能被其他线程访问(对象的作用域不会逃出当前线程),则JVM可以消除这些锁操作。
优点:减少不必要的锁操作,提高性能。
缺点:依赖JVM的逃逸分析的准确性。
public class LockOptimizationExample {private static final Object lock = new Object();public static void main(String[] args) {// 偏向锁示例(需要在JVM启动参数中开启偏向锁)synchronized (lock) {System.out.println("Hello from biased locking!");}// 轻量级锁和重量级锁的转换new Thread(() -> {synchronized (lock) {try {Thread.sleep(1000); // 模拟长时间操作} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}).start();synchronized (lock) {System.out.println("Hello from main thread!");}}
}
3. 并发工具
掌握Java提供的并发工具类,如java.util.concurrent包中的类,这些工具可以帮助处理复杂的并发场景:
Executors框架:管理线程池和执行异步任务。
并发集合:如ConcurrentHashMap, CopyOnWriteArrayList等,这些集合提供更好的并发性能。
同步器:如CountDownLatch, CyclicBarrier, Semaphore, Phaser等,帮助同步多个线程的操作。
Future和Callable:支持有返回结果的并行计算。
CompletableFuture:提供非阻塞的异步编程方式。
Java并发工具主要来自java.util.concurrent包,这个包提供了一系列用于处理多线程编程的高级同步器和并发集合类。这些工具的设计旨在帮助开发者更高效地管理和控制线程间的并发操作。
1. 执行器框架(Executor Framework)
Java的执行器框架抽象了线程的创建、管理和执行过程,使得开发者可以更专注于任务的执行而非线程管理。核心接口和类包括:
Executor:一个简单的接口,用于执行提交的Runnable任务。
ExecutorService:一个更完整的异步任务执行接口,它继承了Executor,添加了生命周期管理和任务执行结果追踪的功能。
ScheduledExecutorService:一个能处理延迟或定期执行任务的接口。
Executors:一个工具类,提供了构造执行器的工厂方法。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ExecutorExample {public static void main(String[] args) {ExecutorService executor = Executors.newFixedThreadPool(3);// 提交任务给线程池for (int i = 0; i < 5; i++) {int taskId = i;executor.submit(() -> {System.out.println("Executing task " + taskId + " via " + Thread.currentThread().getName());});}executor.shutdown(); // 关闭线程池}
}
2. 并发集合
并发集合是特别设计用于多线程环境的集合,提供了线程安全的集合操作,无需外部同步。
ConcurrentHashMap:一个高效的线程安全的哈希表。
CopyOnWriteArrayList:一个线程安全的List,通过复制底层数组的方式实现修改操作,非常适合读多写少的场景。
BlockingQueue:一个支持两个附加操作的队列,即在队列为空时取元素的线程会等待队列变为非空,队列满时插入元素的线程会等待队列可用。
import java.util.concurrent.ConcurrentHashMap;public class ConcurrentHashMapExample {public static void main(String[] args) {ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();map.put("key", "value");System.out.println(map.get("key"));}
}
3. 同步器
Java提供了多种同步器,这些同步器为多个线程之间的协作提供了控制机制。
CountDownLatch:允许一个或多个线程等待其他线程完成操作。
CyclicBarrier:使一定数量的线程在某个公共点同步。
Semaphore:控制对某组资源的访问权限。
Phaser:一个更灵活的线程同步设备,可以用于执行已经完成的阶段中的重复动作。
import java.util.concurrent.Semaphore;public class SemaphoreExample {public static void main(String[] args) {Semaphore semaphore = new Semaphore(2);Runnable longRunningTask = () -> {boolean permit = false;try {permit = semaphore.tryAcquire();if (permit) {System.out.println("Semaphore acquired");Thread.sleep(5000);} else {System.out.println("Could not acquire semaphore");}} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {if (permit) {semaphore.release();}}};Thread t1 = new Thread(longRunningTask);Thread t2 = new Thread(longRunningTask);t1.start();t2.start();}
}
4. Future和Callable
这些工具用于处理那些需要返回结果的任务。Callable是类似于Runnable的接口,不过它可以返回一个结果并能抛出异常。
import java.util.concurrent.*;public class CallableFutureExample {public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executor = Executors.newSingleThreadExecutor();Callable<Integer> task = () -> {TimeUnit.SECONDS.sleep(2);return 123;};Future<Integer> future = executor.submit(task);System.out.println("future done? " + future.isDone());Integer result = future.get(); // 阻塞直到任务完成System.out.println("future done? " + future.isDone());System.out.println("result: " + result);executor.shutdown();}
}
4. 线程安全策略
熟练使用不同的线程安全策略,例如:
不变性:使用不可变对象安全地管理数据共享。
线程局部存储:使用ThreadLocal类,为每个线程提供单独的变量副本。
使用原子类:如AtomicInteger等,利用CAS操作提供无锁的线程安全性。
线程安全是多线程程序设计中的核心概念,特别是在Java这样的并发编程语言中。线程安全意味着在多个线程访问共享资源时,无论运行时环境采用何种调度方式或线程如何交替执行,程序都能表现出正确的行为。为了达到线程安全,开发者需要采用多种策略来保护数据不被并发的线程访问所破坏。
1. 不可变性
将数据声明为不可变是实现线程安全的一种简单而强大的策略。不可变对象一旦被创建,其状态就不可改变,因此无需同步就可以被多个线程安全地访问。
实现方法:在Java中,不可变对象可以通过将所有成员变量声明为final并仅通过构造函数设置它们的值来创建。
优点:简单、安全且易于理解和实现。
public final class ImmutableValue {private final int value;public ImmutableValue(int value) {this.value = value;}public int getValue() {return value;}
}
2. 封锁
对于必须可变的数据,保护其访问的传统方式是使用锁。在Java中,可以使用synchronized关键字或显示锁(如ReentrantLock)来同步代码块,确保一次只有一个线程可以执行该代码块。
使用synchronized:可以锁定一个对象或一个类,为调用锁定对象或类的方法的线程提供独占访问。
使用ReentrantLock:提供比synchronized更灵活的锁定机制,包括尝试非阻塞获取锁、尝试获取锁并可中断地等待获取锁,以及支持公平锁等。
public class Counter {private int count = 0;private final Object lock = new Object();public void increment() {synchronized (lock) {count++;}}public synchronized int getCount() {return count;}
}
3. 无锁并发控制
现代多核CPU支持原子指令集,使得无锁编程成为可能。Java的java.util.concurrent.atomic包提供了一系列原子类,如AtomicInteger和AtomicReference,它们使用高效的机器级指令(如CAS —— 比较并交换)来避免使用锁。
优点:在高度竞争的环境下,无锁机制通常比传统的锁机制提供更好的性能。
缺点:编程模型更复杂,逻辑处理更困难。
import java.util.concurrent.atomic.AtomicInteger;public class AtomicCounter {private final AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();}public int getCount() {return count.get();}
}
4. 线程局部存储
如果一个数据结构只被一个线程访问,那么它自然是线程安全的。ThreadLocal类在Java中提供了线程局部变量。每个线程都可以访问自己的、独立初始化的变量副本。
使用场景:经常用于维护线程特定的状态,如用户会话或事务上下文。
public class ThreadLocalExample {private static final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 1);public static void main(String[] args) {Thread thread1 = new Thread(() -> {threadLocalValue.set(100);System.out.println("Thread1: " +
5. Java并发模式
理解并能应用各种并发模式,如生产者-消费者、读写锁、双检锁单例等。
import java.util.concurrent.ConcurrentHashMap;public class ConcurrentHashMapExample {public static void main(String[] args) {ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();map.put("key", 1);// 使用原子方法 computeIfPresent 更新map.computeIfPresent("key", (k, v) -> v + 1);System.out.println(map.get("key")); // 输出: 2}
}
Java 并发模式是在多线程环境中有效管理和组织线程行为的设计模式,这些模式解决了并发程序设计中的典型问题,如资源共享、线程协作等。了解和应用这些并发模式可以帮助开发者编写更清晰、更高效、更稳定的并发代码。
1. 生产者-消费者模式
生产者-消费者模式是一种经典的并发模式,用于处理生成数据的线程(生产者)和处理数据的线程(消费者)之间的协作。它通常通过一个共享的缓冲区来实现,生产者将数据放入缓冲区,消费者从缓冲区取出数据。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;public class ProducerConsumerExample {private static BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);static class Producer extends Thread {public void run() {for (int i = 0; i < 20; i++) {try {queue.put(i);System.out.println("Produced: " + i);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}}static class Consumer extends Thread {public void run() {while (true) {try {Integer value = queue.take();System.out.println("Consumed: " + value);Thread.sleep(1000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}}public static void main(String[] args) {new Producer().start();new Consumer().start();}
}
2. 读写锁模式
读写锁模式允许多个线程同时读取共享数据,但一个时间内只允许一个线程修改数据。这种模式通过java.util.concurrent.locks.ReadWriteLock接口实现,通常使用ReentrantReadWriteLock类。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockExample {private ReadWriteLock lock = new ReentrantReadWriteLock();private int value;public void setValue(int value) {lock.writeLock().lock();try {this.value = value;} finally {lock.writeLock().unlock();}}public int getValue() {lock.readLock().lock();try {return value;} finally {lock.readLock().unlock();}}
}
3. 工作窃取模式
工作窃取模式用于分散工作负载,特别适用于任务数量和执行时间不均匀的情况。在这个模式中,每个线程都维护自己的任务队列。如果一个线程完成了所有任务,它可以从其他线程的队列中窃取任务来执行。
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;public class ForkJoinExample {static class SumTask extends RecursiveTask<Integer> {private final int[] numbers;private final int start;private final int end;public SumTask(int[] numbers, int start, int end) {this.numbers = numbers;this.start = start;this.end = end;}@Overrideprotected Integer compute() {if (end - start < 20) {return java.util.Arrays.stream(numbers, start, end).sum();} else {int mid = start + (end - start) / 2;SumTask left = new SumTask(numbers, start, mid);SumTask right = new SumTask(numbers, mid, end);right.fork(); // 异步执行右侧任务return left.compute() + right.join(); // 等待并获取右侧任务的结果}}}public static void main(String[] args) {ForkJoinPool pool = new ForkJoinPool();int[] numbers = new int[100];for (int i = 0; i < numbers.length; i++) {numbers[i] = i;}int result = pool.invoke(new SumTask(numbers, 0, numbers.length));System.out.println("Result: " + result);}
}
4. 双检锁/单例模式
双重检查锁模式(Double-Checked Locking)是一种用于延迟初始化资源的技术。它特别适用于实现单例模式,确保只创建单个实例,且在多线程环境中线程安全。
public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}
6. 避免常见的并发问题
理解并避免死锁、活锁、饥饿等常见的并发问题。使用工具和技术(如JVisualVM, Thread Dumps等)来检测和诊断这些问题。
1. 数据竞争与竞态条件
数据竞争发生在两个或多个线程同时访问相同的数据,并且至少有一个线程在写入时。竞态条件是指程序的行为依赖于多个线程的交错执行时序。
解决策略:
锁定:使用互斥锁(mutexes),如synchronized关键字或ReentrantLock,确保同一时间只有一个线程可以访问共享资源。
原子操作:使用原子类,如AtomicInteger,这些类利用CPU提供的原子指令集来保证操作的原子性。
避免共享状态:尽可能设计无状态或只读的共享资源。利用局部变量和不可变对象可以减少同步需求。
2. 死锁
死锁是指两个或多个进程或线程在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,它们都将无法推进下去。
解决策略:
锁顺序:定义一个全局的锁顺序,并且在代码中严格按照这个顺序来获取锁。
超时尝试:使用带超时的锁尝试机制,例如tryLock(long timeout, TimeUnit unit)方法,可以在超时后释放锁,防止永久等待。
死锁检测工具:利用Java中的JMX和工具(如jConsole、VisualVM)来监控和检测死锁。
3. 活锁
活锁类似于死锁,涉及的线程或进程不断重复相同的交互,没有进展。这通常发生在两个线程都在尝试避免冲突并恢复对方原状的情况下。
解决策略:
随机等待:在重试之前引入随机的延迟,以减少两个线程或进程再次冲突的可能性。
优先级:为交互的线程引入优先级,允许优先级高的线程有更多的执行机会。
4. 饥饿
饥饿是指一个或多个线程无法获得必要的资源,以至于无法进行有效的进展,这通常是由于资源被其他“贪婪”的线程长时间占用所致。
解决策略:
公平锁:使用公平锁机制,如ReentrantLock(true),确保线程按照请求锁的顺序来获得锁。
优先级调整:调整线程的优先级,以确保低优先级的线程不会永久饿死。
5. 内存泄漏
在并发环境中,内存泄漏通常发生在缓存、监听器和其他共享资源中,当它们的生命周期被错误管理时。
解决策略:
弱引用:使用WeakReference和WeakHashMap等,允许垃圾收集器自动清理只被弱引用持有的对象。
定期清理:为使用的资源如缓存等定期清理机制,移除不再使用的条目。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class AvoidDeadlock {private final Lock lock1 = new ReentrantLock(true);private final Lock lock2 = new ReentrantLock(true);public void method1() {lock1.lock();try {// 模拟处理lock2.lock();try {System.out.println("Method 1 is running");} finally {lock2.unlock();}} finally {lock1.unlock();}}