第1章:引言
Java并发编程是多线程技术的一种实现方式,它在现代软件开发中扮演着至关重要的角色。随着计算机处理器核心数量的增加,以及云计算和大数据技术的普及,能够有效利用并发编程的程序员将能为企业创造更高的效率和价值。此外,良好的并发控制还能显著提高程序的响应速度和吞吐量。
并发编程不仅限于提高执行效率,它还解决了多任务同时执行时的资源共享问题。例如,服务器同时处理成千上万个客户端请求,如果没有有效的并发控制,资源访问的冲突将导致程序失败。Java提供了一套完备的并发编程工具,从基本的线程控制到复杂的锁机制和并发集合,这些工具帮助开发者构建稳健的多线程应用程序。
理解并发的关键在于掌握如何合理地分配和管理资源,以及确保多个线程在访问共享资源时的正确性和高效性。随着技术的不断演进,Java并发编程也在不断地更新和发展,为开发者提供了更多的可能性和挑战。
第2章:Java中的线程基础
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。在Java中,线程的实现主要通过以下三种方式:
1. 继承Thread类
Java中的线程可以通过继承Thread
类来创建。这种方式简单直接,但因为Java不支持多重继承,所以如果一个类已经继承了其他类,则不能再继承Thread
。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
public class Example {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
在这个例子中,MyThread
继承自Thread
类,并重写了run()
方法。在主方法中通过创建MyThread
的实例,并调用start()
方法来启动线程。
2. 实现Runnable接口
实现Runnable
接口是创建线程的另一种常用方式。这种方法的好处是不会影响类的继承体系,具有更好的灵活性。
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
public class Example {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
这里,MyRunnable
实现了Runnable
接口并定义了run()
方法。通过将其实例传递给Thread
类的构造器,然后启动线程。
3. 使用Callable和Future
Callable
接口类似于Runnable
,不同之处在于它可以返回一个结果,并且能抛出异常。Callable
经常与Future
一起使用,以便于处理异步计算的结果。
import java.util.concurrent.*;
public class CallableExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 123;
}
});
System.out.println("Future result: " + future.get());
executor.shutdown();
}
}
在这个例子中,通过Callable
返回整数123,Future.get()
第3章:线程的生命周期
理解线程的生命周期对于精确控制Java中的并发行为至关重要。线程在其生命周期中会经历几种状态,每种状态的管理和转换都需精细操作以保证程序的正确性和效率。
线程状态
Java线程的生命周期包括以下几种状态:
- 新建(NEW):新创建了一个线程实例,但还没有调用
start()
方法。 - 就绪(RUNNABLE):线程已经启动,等待系统分配资源。
- 运行(RUNNING):线程正在执行
run()
方法中的代码。 - 阻塞(BLOCKED):线程被阻止执行,因为它正在等待一个监视器锁(进入一个同步块或方法)。
- 等待(WAITING):线程通过调用
wait()
方法,等待其他线程通知(通过notify()
或notifyAll()
)来继续执行。 - 计时等待(TIMED_WAITING):线程在指定的时间内等待另一个线程的通知,例如调用
sleep()
或join(long millis)
方法。 - 终止(TERMINATED):线程的
run()
方法执行完毕或者因异常终止。
线程状态转换的关键方法
线程状态之间的转换不仅受到程序代码的控制,还受到操作系统调度的影响。下面是一些常用的方法,这些方法会影响线程状态:
start()
:将新建线程的状态从NEW转变到RUNNABLE。run()
:执行线程要进行的操作。sleep(long millis)
:使当前正在执行的线程暂停指定的时间(毫秒),不释放锁。wait()
:使线程进入等待状态直到它被通知或中断。notify()/notifyAll()
:通知一个或所有等待(在此对象的监视器上)的线程继续执行。join()
:在一个线程中调用另一个线程的join()
方法,会将调用线程置于阻塞状态,直到被join()
的线程结束运行。
示例:线程的状态转换
public class ThreadStateExample implements Runnable {
public static Thread thread1;
public static void main(String[] args) throws InterruptedException {
thread1 = new Thread(new ThreadStateExample());
// 打印新建线程的状态
System.out.println("State of thread1 after creation - " + thread1.getState());
thread1.start();
// 打印启动后的状态
System.out.println("State of thread1 after calling start() - " + thread1.getState());
}
public void run() {
Thread myThread = new Thread(new MyRunnable());
// 新建线程
System.out.println("State of myThread - " + myThread.getState());
myThread.start();
// 等待线程完成
try {
myThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
// 打印终止后的状态
System.out.println("State of myThread after join() - " + myThread.getState());
}
private static class MyRunnable implements Runnable {
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}
}
这段代码展示了线程从新建到运行、等待、再到终止的全过程。thread1
和myThread
的状态在各个时间点被输出,展示了状态的转换。
通过深入了解线程的生命周期及其管理,开发者可以更好地设计和调试多线程应用,确保应用的高性能和稳定性。
第4章:同步基础
在Java并发编程中,同步是一种机制,用于控制多个线程对共享资源的访问,以防止数据的不一致性和腐败。理解并有效地实现同步是每个Java并发程序员必须掌握的技能。
为何需要同步
多线程环境下,当多个线程同时修改同一个资源(如修改同一个变量),而不进行适当的同步,就会引起线程安全问题。这可能导致数据不正确或程序行为异常。同步机制可以确保在任一时刻,只有一个线程可以访问特定的资源。
使用synchronized
关键字
synchronized
是Java中最基本的同步机制。它可以用于方法或代码块,保证同一时间只有一个线程执行该方法或代码块。
同步一个方法
可以将一个方法声明为synchronized
,这样,这个方法在任何时候只能由一个线程执行。
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
在这个示例中,increment
方法和getCount
方法都被标记为synchronized
。这意味着,执行这些方法的线程在其他线程可以调用同一个对象的任何synchronized
方法之前,必须获得对象的锁。
同步一个代码块
如果只有方法中的一部分代码访问共享资源,可以通过同步代码块来减小锁的范围,提高效率。
public class BetterCounter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public int getCount() {
synchronized (this) {
return count;
}
}
}
这里,只有修改count
的部分被同步了,从而减少了锁的持有时间。
volatile
关键字的作用
volatile
关键字在Java中用于确保变量的修改对所有线程立即可见,它可以帮助程序员编写无锁的代码,同时保证线程间的可见性。
public class Flag {
private volatile boolean flag = true;
public void toggle() {
flag = !flag;
}
public boolean checkFlag() {
return flag;
}
}
在上面的代码中,flag
变量被声明为volatile
。这保证了当一个线程修改了flag
的值时,这个修改对于其他检查flag
值的线程是立即可见的。
同步是确保多线程程序正确执行的基石。通过合理使用synchronized
和volatile
等同步机制,开发者可以避免多线程环境下的竞态条件和数据不一致等问题,从而编写出更加健壮、可靠的Java应用程序。
第5章:高级锁机制
在Java并发编程中,除了基本的同步机制如synchronized
和volatile
关键字之外,Java还提供了更复杂、更灵活的锁机制,这些高级锁能够帮助开发者在更加复杂的场景中有效管理线程间的协作和资源共享。
ReentrantLock的使用及其优势
ReentrantLock
是Java提供的一个高级同步机制,相较于synchronized
,它提供了更高的灵活性和更丰富的功能。ReentrantLock
支持重入性,意味着同一个线程可以多次获得同一把锁。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
在这个例子中,increment
方法中使用ReentrantLock
确保增加操作的线程安全。使用try
和finally
块确保在增加计数后,锁一定会被释放,这是一个良好的锁管理习惯。
ReentrantLock
还提供了如尝试锁定(tryLock)、定时锁定以及公平锁(Fairness)等高级功能,这些功能使得ReentrantLock
在某些场景下比synchronized
更加合适。
ReadWriteLock读写分离锁
当多个线程在进行读操作,而写操作相对较少时,ReadWriteLock
可以提升性能。ReadWriteLock
维护了一对锁——一个读锁和一个写锁。通过允许多个线程同时读取而不互相阻塞,这种锁机制提高了程序的并发性能。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CachedData {
private Object data;
private volatile boolean cacheValid;
private final ReadWriteLock rwl = new ReentrantReadWriteLock();
public void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have acquired
// write lock and changed state before we did.
if (!cacheValid) {
data = loadData();
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
private Object loadData() {
// Load data from external source
return new Object();
}
private void use(Object data) {
// Use the data
}
}
在这个例子中,使用了读写锁来控制对缓存数据的访问,使得多个线程可以同时读取数据,但只有一个线程可以进行写操作,并且写操作时会阻塞读操作。
StampedLock的特点和使用案例
StampedLock
是Java 8引入的一种锁机制,它也提供了读写锁的功能,但性能通常比ReentrantReadWriteLock
更优,因为它的锁方法返回一个戳记(stamp),用于释放锁或检查锁是否有效。
第6章:线程安全与不安全的实例分析
在Java并发编程中,确保线程安全是一个重要的挑战。线程安全意味着在多线程环境下,程序能够正确地处理多个线程对同一资源的访问和修改,不会导致数据损坏或不一致。本章将通过具体的实例来分析线程安全和线程不安全的情况,并讨论如何实现线程安全。
线程不安全的实例
首先,让我们看一个线程不安全的例子,并分析为什么会出现问题。
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 非线程安全
}
public int getCount() {
return count;
}
}
在这个例子中,increment
方法简单地将count
的值增加1。虽然这看起来是一个原子操作,但实际上它不是。该操作可以分解为三个独立的步骤:读取count
的值,增加1,然后写回新值。如果多个线程同时执行increment()
方法,它们可能读取相同的初始值,导致它们都只增加1,最终结果会小于预期。
线程安全的实现
现在,让我们看看如何使increment
操作变得线程安全。
使用synchronized
关键字
一个简单的方法是使用synchronized
关键字同步方法:
public class SafeCounterWithSync {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
通过同步increment
方法,我们确保任何时候只有一个线程可以执行该方法。这保证了当一个线程在修改count
值时,没有其他线程可以访问该方法,从而避免了并发修改的问题。
使用AtomicInteger
另一个实现线程安全的方法是使用Java的Atomic
类,如AtomicInteger
,这些类利用了底层硬件的原子性操作来保证操作的原子性。
import java.util.concurrent.atomic.AtomicInteger;
public class SafeCounterWithAtomic {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
在这个例子中,incrementAndGet
方法自动保证了增加操作的原子性。这种方法不需要synchronized
关键字,通常比同步方法更高效。
分析和对比
通过上述示例,我们可以看到不同方法实现线程安全的策略和效果。使用synchronized
是最直接的同步方法,但可能会因为锁的争用而降低性能。而AtomicInteger
等原子类提供了一种更细粒度的同步机制,通常在高并发场景下表现更好。
理解这些基本的线程安全模式对于编写健壮的并发程序至关重要。在设计并发程序时,开发者应该根据实际的应用场景选择合适的同步策略,以平衡性能和安全性。
第7章:条件变量和阻塞队列
在Java并发编程中,除了锁和同步方法,条件变量和阻塞队列也是重要的工具,用于在多线程环境中协调线程之间的操作。本章将深入探讨条件变量和阻塞队列的概念、用途和实现方式。
条件变量的使用
条件变量允许线程在某些条件不满足时挂起,直到其他线程改变条件并通知等待线程继续执行。在Java中,条件变量通常与一个锁(如ReentrantLock
)关联使用。
使用Condition
接口
Condition
接口提供了一种方式,允许线程获取一个锁并在某个条件上等待,直到被通知或中断。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private int[] items = new int[10];
private int putptr, takeptr, count;
public void put(int x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public int take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
int x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
在这个例子中,使用了两个条件变量notFull
和notEmpty
来控制数组的存取操作,确保生产者在数组满时等待,消费者在数组空时等待。
阻塞队列的实现和应用
阻塞队列是支持两个附加操作的队列,这些操作包括在队列为空时队列的获取操作将阻塞,等待队列变为非空,当队列满时插入操作将阻塞,等待队列可用。
使用ArrayBlockingQueue
ArrayBlockingQueue
是一个由数组支持的有界阻塞队列,此类队列按 FIFO(先进先出)原则对元素进行排序。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
Thread producer = new Thread(() -> {
try {
int value = 0;
while (true) {
queue.put(value++);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
while (true) {
System.out.println(queue.take());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
在此示例中,生产者不断地将数字放入队列,而消费者则从队列中取出数字。put
和take
方法都是阻塞方法,分别在队列满和队列空时阻塞。
通过使用条件变量和阻塞队列,Java开发者可以有效地管理线程间的协作,使得资源访问和任务执行更加高效和安全。这些工具极大地简化了复杂并发程序的开发,是实现生产者-消费者模式等多线程设计模式的关键。
第8章:Java中的原子类
在Java并发编程中,原子类提供了一种在无锁环境下进行线程安全操作的方法。这些类利用底层硬件的能力,以原子方式更新其值,从而避免了同步的开销。本章将探讨几种常用的原子类,并示例它们的应用。
AtomicInteger的使用
AtomicInteger
是一种基本的原子类,提供了原子更新整数值的操作。它是实现非阻塞算法的基础,常用于计数器或生成唯一序列号等场景。
示例:使用AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子方式递增
}
public int getCount() {
return count.get();
}
}
在这个例子中,incrementAndGet()
方法原子地将count
的值增加1,并返回新值。这种方式保证了即使多个线程同时调用increment()
方法,每次调用也能安全地修改count
的值,而无需同步。
AtomicLong和AtomicReference的应用
与AtomicInteger
相似,AtomicLong
提供了针对长整型的原子操作。AtomicReference
则允许我们对任何对象的引用进行原子更新,这在管理共享对象时非常有用。
示例:使用AtomicLong
import java.util.concurrent.atomic.AtomicLong;
public class SequenceGenerator {
private AtomicLong value = new AtomicLong(0);
public long next() {
return value.incrementAndGet(); // 原子方式递增
}
}
SequenceGenerator
类使用AtomicLong
来生成唯一的序列号。每次调用next()
方法都会安全地递增内部计数器。
示例:使用AtomicReference
import java.util.concurrent.atomic.AtomicReference;
public class Node {
static class Link {
final String data;
Link next;
Link(String data) {
this.data = data;
}
}
private AtomicReference<Link> head = new AtomicReference<>();
public void add(String data) {
Link newLink = new Link(data);
Link oldHead;
do {
oldHead = head.get();
newLink.next = oldHead;
} while (!head.compareAndSet(oldHead, newLink));
}
}
在这个例子中,使用AtomicReference
来管理链表的头部。add
方法通过原子的compareAndSet
操作尝试更新链表的头部,这保证了即使多个线程同时添加元素,链表的状态也总是一致的。
原子类的优势
原子类提供的主要优势是无锁的线程安全性,这通常比使用锁或其他同步机制具有更高的性能,特别是在高竞争的环境中。通过利用现代CPU提供的底层并发原语,原子类能够提供一种有效的方法来减少并发程序的复杂性。
通过上述示例和解释,可以看到原子类是实现高效并发处理的强大工具。它们不仅可以简化代码,还可以提高程序的响应速度和吞吐量,是现代Java并发编程中不可或缺的一部分。