Java并发编程JUC(java.util.concurrent)提供了一些可重用的线程安全组件,这些组件可以帮助我们更容易地实现高效且正确的并发程序。下面是对JUC的详细总结:
JUC提供了各种原子操作类,包括AtomicInteger、AtomicBoolean和AtomicReference等。这些类允许我们以原子方式执行单个变量上的操作,而不需要使用锁来确保线程安全。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
在上面的代码中,我们使用了AtomicInteger类来实现一个计数器。count变量被初始化为0,并且可以通过incrementAndGet方法原子地增加计数器的值。getCount方法返回当前计数器的值。需要注意的是,AtomicInteger只能保证单个变量的操作是原子的,而不能保证多个变量之间的操作是原子的。如果需要对多个变量进行原子操作,可以考虑使用AtomicReference类。
JUC提供了ReentrantLock、ReadWriteLock、StampedLock等各种锁。其中,ReentrantLock是一个可重入锁,具有与synchronized相同的语义,但它比synchronized更灵活。 ReadWriteLock支持读写分离,即多个线程可以同时读取共享数据,但只有一个线程可以写入共享数据。StampedLock则是一个乐观锁,它可以在不阻塞线程的情况下尝试获取锁。
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
在上面的代码中,我们使用了ReentrantLock类来实现一个计数器。increment方法首先获取锁,然后增加计数器的值,最后释放锁。需要注意的是,对于每个lock调用,都应该有对应的unlock调用。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Cache {
private Object data = null;
private ReadWriteLock lock = new ReentrantReadWriteLock();
public Object readData() {
lock.readLock().lock();
try {
return data;
} finally {
lock.readLock().unlock();
}
}
public void writeData(Object newData) {
lock.writeLock().lock();
try {
data = newData;
} finally {
lock.writeLock().unlock();
}
}
}
在上面的代码中,我们使用了ReadWriteLock类来实现一个缓存。readData方法获取读取锁并返回缓存数据,writeData方法获取写锁并更新缓存数据。多个线程可以同时获取读取锁,但只有一个线程可以获取写锁。
import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
void move(double deltaX, double deltaY) {
long stamp = lock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp);
}
}
double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead();
double currentX = x, currentY = y;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
在上面的代码中,我们使用了StampedLock类来实现一个点。move方法获取写锁并移动点,distanceFromOrigin方法尝试获取乐观读取锁,如果成功则返回当前点到原点的距离,否则获取悲观读取锁并返回当前点到原点的距离。
以上示例代码仅为演示目的,实际应用中需要根据具体情况进行调整,以确保线程安全和性能最优。
JUC提供了Executor框架,它是一个管理线程池的高级工具。 Executor框架可以根据需要创建新线程,并使用线程池重新使用旧线程。线程池可以避免线程频繁创建和销毁的开销,从而为应用程序提供更好的性能和响应能力。
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable worker = new MyRunnable(i + 1);
executor.execute(worker);
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("All threads are terminated.");
固定大小线程池适用于需要控制线程数目的情况,例如在服务器端处理客户端请求时,可以根据服务器的处理能力设置线程池大小。
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
Runnable worker = new MyRunnable(i + 1);
executor.execute(worker);
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("All threads are terminated.");
缓存线程池适用于执行短期异步任务的情况,例如在桌面应用程序中加载图片或其他资源。当任务完成后,线程会将其返回到线程池中以便重用。
ExecutorService executor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
Runnable worker = new MyRunnable(i + 1);
executor.execute(worker);
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("All threads are terminated.");
单线程池适用于需要顺序执行任务的情况,例如在日志系统中记录日志信息。由于只有一个工作线程,所以所有任务都按照提交的顺序执行。
其中,MyRunnable是实现了Runnable接口的工作线程类。
另外,在使用线程池时,需要注意线程池的大小设置不能太大或太小。如果线程池过小,则可能会导致资源浪费和性能瓶颈;如果线程池过大,则可能会导致资源消耗和竞争问题。因此,在实际使用中,需要根据具体的场景进行调整和优化。
JUC提供了各种阻塞队列,包括ArrayBlockingQueue、LinkedBlockingQueue和PriorityBlockingQueue等。阻塞队列是一种线程安全的数据结构,它可以在没有元素的情况下阻塞线程,并在有空间时允许线程继续执行。
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
queue.put("A");
queue.put("B");
queue.put("C");
String element = queue.take();
System.out.println(element);
ArrayBlockingQueue是一个有界队列,内部使用数组实现。它适用于生产者-消费者模式,例如在多个线程之间传递数据时,可以限制队列大小并控制生产者的速度。
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.add("A");
queue.add("B");
queue.add("C");
String element = queue.poll();
System.out.println(element);
LinkedBlockingQueue是一个可选有界队列,内部使用链表实现。它适用于任务调度,例如在线程池中等待任务时,可以无限制添加任务而不会造成系统崩溃。
BlockingQueue<String> queue = new SynchronousQueue<>();
new Thread(() -> {
try {
String element = queue.take();
System.out.println(element);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
queue.put("A");
SynchronousQueue是一个无界队列,只能存储一个元素。它适用于线程之间直接传输信息的情况,例如在生产者-消费者模式中,可以确保每个任务都在消费者准备好时立即处理。
其中,put()和take()方法是阻塞的,如果队列已满或为空,则会一直等待直到有空间或元素可用。add()和poll()方法则不阻塞,如果队列已满或为空,则会立即返回false或null。
另外,ArrayBlockingQueue和LinkedBlockingQueue都是有界队列,而SynchronousQueue是无界队列。
在使用阻塞队列时,需要注意其性能和安全性。如果使用不当,可能会导致死锁、资源竞争或其他问题。因此,在实际使用中,需要根据具体的场景进行选择和优化。
Fork/Join框架是JUC提供的一个用于并行处理数据的工具。这个框架使用分治策略将大型任务拆分成小型任务,并使用线程池进行并发处理。该框架广泛用于递归算法(例如快速排序)和基于树形结构的问题(例如计算斐波那契数列)。
Fork/Join框架包含两个重要概念:Fork(分解)和Join(合并)。当一个任务太大时,可以将其划分成若干个子任务进行并行处理。每个子任务处理完成后,再将结果合并。这个过程可以递归进行,直到所有任务都被处理完毕。
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class SumTask extends RecursiveTask<Long> {
private static final int THRESHOLD = 1000;
private int[] array;
private int start;
private int end;
public SumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid, end);
leftTask.fork();
rightTask.fork();
return leftTask.join() + rightTask.join();
}
}
public static void main(String[] args) {
int[] array = new int[1000000];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
long startTime = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
SumTask task = new SumTask(array, 0, array.length);
long result = pool.invoke(task);
pool.shutdown();
long endTime = System.currentTimeMillis();
System.out.println("Result: " + result);
System.out.println("Time: " + (endTime - startTime) + "ms");
}
}
在这个示例中,SumTask是一个继承自RecursiveTask类的计算任务。如果数组元素数量小于等于THRESHOLD(1000),则直接计算结果;否则,将任务分解为两个子任务,并利用fork()方法提交到线程池执行。最后,使用join()方法等待子任务计算完成并合并结果。
在main()方法中,创建了一个大小为4的ForkJoinPool线程池,并将任务提交给线程池执行。通过打印出来的时间和结果可以看出,使用Fork/Join框架比单线程计算更加快速高效。
JUC提供了许多高级工具,可以帮助我们更轻松地实现正确且高效的并发程序。当然,在使用这些工具时,也需要避免常见的并发问题,例如死锁、竞态条件、活锁等。