concurrent
并发
并发是指在计算机系统中同时进行多个任务的能力。在 Java 中,并发编程允许程序在同一时间执行多个线程,从而提高程序的性能和响应速度,特别是在多核处理器上可以更好地利用硬件资源。并发编程的核心目标是提高程序的效率和吞吐量,同时保持数据的一致性和程序的正确性。
并发与多线程
多线程是实现并发的一种方式。线程是操作系统能够进行调度和执行的最小单位,Java 中的线程允许程序在同一时间执行多个任务。
并发是一个更广泛的概念,指的是多个任务在同一时间段内交替执行,而不仅仅是线程的执行。并发可以通过多线程、多进程等方式实现。
并发集合
ConcurrentHashMap
:线程安全的哈希表,支持高并发的读写操作。CopyOnWriteArrayList
:线程安全的列表,通过写时复制机制实现线程安全。BlockingQueue
:阻塞队列,用于在线程之间安全地传递数据。
并发工具类
CountDownLatch
:允许一个或多个线程等待一组操作完成CyclicBarrier
:允许一组线程相互等待,直到所有线程都到达一个公共屏障点。Semaphore
:用于控制同时访问某个资源的线程数量。Future
、Callable
:用于表示异步计算的结果,允许获取计算结果或取消计算。
并发编程的挑战
- 线程安全:确保多个线程在访问共享资源时不会发生冲突,需要使用同步机制来保证线程安全。
- 死锁:多个线程互相等待对方释放资源,导致程序无法继续执行。需要合理设计资源的获取顺序和使用方式来避免死锁。
- 性能优化:合理使用并发机制和资源,避免过度同步导致性能下降,同时充分利用多核处理器的资源来提高程序的性能。
- 调试和测试:并发程序的调试和测试比单线程程序更复杂,需要特别注意线程间的交互和数据一致性问题。
并发编程的最佳实践
- 最小化同步范围:尽量减少同步代码块的范围,只同步必要的代码,以提高程序的性能。
- 避免死锁:注意资源的获取顺序,避免死锁的发生,例如使用
tryLock()
方法尝试获取锁。 - 合理使用同步机制:根据需要选择合适的同步机制,避免过度同步导致性能下降。
- 使用并发集合:利用Java提供的线程安全的并发集合,如
ConcurrentHashMap
、CopyOnWriteArrayList
等,以简化并发编程。 - 使用并发工具类:使用 Java 提供的并发工具类,如
CountDownLatch
、CyclicBarrier
、Semaphore
等,以实现复杂的线程协调和同步。
线程同步
线程同步是多线程编程中的一个重要概念,用于确保多个线程在访问共享资源时不会发生冲突,从而保证数据的一致性和程序的正确性。
synchronized
:基本的线程同步机制,用于确保同一时间只有一个线程可以执行某个代码块或方法。
synchronized void method() {}
final Object lock = new Object();
void method() {
synchronized(lock) {}
}
volatile
:用于确保变量的可见性和禁止指令重排,但不能用于实现线程同步。- 可见性:当一个线程修改了 volatile 变量的值,其他线程能够立即看到该变量的最新值。
- 禁止指令重排:防止编译器和处理器对指令进行重排,确保程序的执行顺序。
class Flag {
private volatile boolean running = true;
public void stop () {
running = false;
}
public boolean isRunning() {
return running;
}
}
Lock
Lock
是 java.util.concurrent.locks
包中的一个接口,用于实现更灵活的线程同步机制。与传统的 synchronized
关键字相比,Lock
提供了更丰富的功能和更细粒度的控制,使得并发编程更加灵活和高效。
java.util.concurrent.locks.ReentrantLock
- 同一个线程可以多次获取同一把锁,每次获取锁后都需要调用
unlock()
方法来释放锁。 - 可以设置锁的公平性,确保等待时间最长的线程优先获取锁,避免某些线程长时间得不到锁。
- 支持中断操作,当线程尝试获取锁时,如果被中断,则可以响应中断并抛出
InterruptedException
。
- 同一个线程可以多次获取同一把锁,每次获取锁后都需要调用
class Clz {
private final ReentrantLock lock = new ReentrantLock();
private int i = 0;
void incr() {
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
}
void get() {
lock.lock();
try {
return i;
} finally {
lock.unlock();
}
}
}
java.util.concurrent.locks.ReentrantReadWriteLock
:读写锁允许多个线程同时读取共享资源,但同一时间只能有一个线程写入共享资源,适用于读多写少的场景。
class Clz {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private int i = 0;
private void set(int i) {
lock.writeLock().lock();
try {
this.i = i;
} finally {
lock.writeLock().unlock();
}
}
private int get() {
lock.readLock().lock();
try {
return i;
} finally {
lock.readLock().unlock();
}
}
}
wait(),notify(),notifyAll()
wait()
、notify()
、notifyAll()
是 Object 类的方法,用于实现线程间的协调和同步。wait()
:使当前线程等待,直到其他线程调用同一对象的 notify() 或 notifyAll() 方法。notify()
:唤醒等待在该对象上的一个线程。notifyAll()
:唤醒等待在该对象上的所有线程。
class Clz {
private int i = 0;
synchronized void incr() {
i++;
notifyAll();
}
synchronized void waitFor(int t) throw InterruptedException {
while (i < t) {
wait();
}
}
}
线程池
线程池是一种用于管理和复用线程的机制,它允许程序在执行多个任务时高效地利用线程资源。通过线程池,可以避免频繁地创建和销毁线程所带来的性能开销,同时还可以更好地控制线程的数量和资源的使用。Java 提供了 java.util.concurrent
包中的 ExecutorService
接口和 Executors
工厂类来创建和管理线程池。
线程池的工作原理
任务提交:当提交任务到线程池时,线程池会根据当前的工作线程数量和任务队列的状态来决定如何处理任务。
任务执行:如果线程池中有空闲的工作线程,则直接分配任务给工作线程执行;如果没有空闲的工作线程且任务队列未满,则将任务添加到任务队列中等待执行;如果任务队列也满了,则根据线程池的拒绝策略来处理任务(如抛出异常、丢弃任务等)。
线程管理:线程池会根据任务的执行情况和线程池的配置来管理线程的创建、销毁和复用
Java 提供了几种常用的线程池创建方式,通过Executors
工厂类可以方便地创建不同类型的线程池:
FixedThreadPool
:线程池中的线程数量是固定的,所有任务都在线程池中的线程上执行。适用于任务数量较多且任务执行时间较短的场景。
ExecutorService excutorservice = Executors.newFixedThreadPool(1);
CacheThreadPool
:线程池中的线程数量不固定,当线程空闲时会被复用,如果线程数量超过一定数量(默认为 60),则空闲线程会被回收。适用于任务数量较少且任务执行时间较短的场景。
ExecutorService excutorservice = Executors.newCachedThreadPool();
SingleThreadExecutor
:线程池中只有一个线程,所有任务按顺序执行,保证任务的执行顺序。适用于需要保证任务执行顺序的场景。
ExecutorService executorService = Executors.newSingleThreadExecutor();
ScheduledThreadPoolExecutor
:用于执行定时任务或周期性任务,可以在指定的延迟后执行任务或以固定的频率执行任务。适用于需要执行定时任务或周期性任务的场景。
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);