Java并发编程JUC-小结

Java并发编程JUC(java.util.concurrent)提供了一些可重用的线程安全组件,这些组件可以帮助我们更容易地实现高效且正确的并发程序。下面是对JUC的详细总结:

1. 原子类:

JUC提供了各种原子操作类,包括AtomicInteger、AtomicBoolean和AtomicReference等。这些类允许我们以原子方式执行单个变量上的操作,而不需要使用锁来确保线程安全。

以下是JUC中原子类AtomicInteger的示例代码:

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类。

2. 锁:

JUC提供了ReentrantLock、ReadWriteLock、StampedLock等各种锁。其中,ReentrantLock是一个可重入锁,具有与synchronized相同的语义,但它比synchronized更灵活。 ReadWriteLock支持读写分离,即多个线程可以同时读取共享数据,但只有一个线程可以写入共享数据。StampedLock则是一个乐观锁,它可以在不阻塞线程的情况下尝试获取锁。

下面是JUC提供的三种锁的示例代码:

  1. ReentrantLock:
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调用。

  1. ReadWriteLock:
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方法获取写锁并更新缓存数据。多个线程可以同时获取读取锁,但只有一个线程可以获取写锁。

  1. StampedLock:
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方法尝试获取乐观读取锁,如果成功则返回当前点到原点的距离,否则获取悲观读取锁并返回当前点到原点的距离。

以上示例代码仅为演示目的,实际应用中需要根据具体情况进行调整,以确保线程安全和性能最优。

3. 线程池:

JUC提供了Executor框架,它是一个管理线程池的高级工具。 Executor框架可以根据需要创建新线程,并使用线程池重新使用旧线程。线程池可以避免线程频繁创建和销毁的开销,从而为应用程序提供更好的性能和响应能力。

以下是Java语言中常见的几种线程池示例代码:

  1. 固定大小线程池
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.");

固定大小线程池适用于需要控制线程数目的情况,例如在服务器端处理客户端请求时,可以根据服务器的处理能力设置线程池大小。

  1. 缓存线程池
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.");

缓存线程池适用于执行短期异步任务的情况,例如在桌面应用程序中加载图片或其他资源。当任务完成后,线程会将其返回到线程池中以便重用。

  1. 单线程池
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接口的工作线程类。
另外,在使用线程池时,需要注意线程池的大小设置不能太大或太小。如果线程池过小,则可能会导致资源浪费和性能瓶颈;如果线程池过大,则可能会导致资源消耗和竞争问题。因此,在实际使用中,需要根据具体的场景进行调整和优化。

4. 阻塞队列:

JUC提供了各种阻塞队列,包括ArrayBlockingQueue、LinkedBlockingQueue和PriorityBlockingQueue等。阻塞队列是一种线程安全的数据结构,它可以在没有元素的情况下阻塞线程,并在有空间时允许线程继续执行。

以下是JUC中常见的几种阻塞队列示例代码:

  1. ArrayBlockingQueue
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
queue.put("A");
queue.put("B");
queue.put("C");
String element = queue.take();
System.out.println(element);

ArrayBlockingQueue是一个有界队列,内部使用数组实现。它适用于生产者-消费者模式,例如在多个线程之间传递数据时,可以限制队列大小并控制生产者的速度。

  1. LinkedBlockingQueue
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.add("A");
queue.add("B");
queue.add("C");
String element = queue.poll();
System.out.println(element);

LinkedBlockingQueue是一个可选有界队列,内部使用链表实现。它适用于任务调度,例如在线程池中等待任务时,可以无限制添加任务而不会造成系统崩溃。

  1. SynchronousQueue
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是无界队列。
在使用阻塞队列时,需要注意其性能和安全性。如果使用不当,可能会导致死锁、资源竞争或其他问题。因此,在实际使用中,需要根据具体的场景进行选择和优化。

5. Fork/Join框架:

Fork/Join框架是JUC提供的一个用于并行处理数据的工具。这个框架使用分治策略将大型任务拆分成小型任务,并使用线程池进行并发处理。该框架广泛用于递归算法(例如快速排序)和基于树形结构的问题(例如计算斐波那契数列)。

Fork/Join框架包含两个重要概念: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提供了许多高级工具,可以帮助我们更轻松地实现正确且高效的并发程序。当然,在使用这些工具时,也需要避免常见的并发问题,例如死锁、竞态条件、活锁等。

你可能感兴趣的:(JavaSE,java,java,jvm,开发语言)