前言
JUC 是 Java 并发包(java.util.concurrent)的简称,该包在 Java 5.0 版本引入,提供了在并发编程中常用的工具类。这些工具类包括用于多线程同步的锁(如 ReentrantLock),用于线程池的 ExecutorService,用于原子操作的 AtomicInteger 等。这些类和接口都是非阻塞的,设计目标是在多线程环境下提供高性能和可扩展性。
JUC 包中的类和接口主要分为三类:
- 同步工具类:例如 Lock、Semaphore、CountDownLatch、CyclicBarrier、Exchanger 等,这些类提供了多种同步机制,可以灵活地控制多个线程之间的执行顺序和协作。
- 线程池类:例如 ThreadPoolExecutor、ScheduledThreadPoolExecutor 等,这些类可以方便地创建和管理线程池,有效地利用系统资源,避免线程过多或者过少的问题。
- 原子操作类:例如 AtomicInteger、AtomicLong、AtomicReference 等,这些类提供了原子操作,可以在多线程环境下保证数据的一致性和安全性。
一、Callable
1、介绍
Callable接口是Java中的一个泛型接口,它允许在并发环境中定义可以返回结果的任务。与Runnable接口不同的是,Callable接口可以返回执行结果,并且可以抛出异常。
2、Callable的使用
使用Callable接口需要实现Callable接口,并重写call()方法。call()方法定义了要执行的任务,并返回执行结果。
代码语言:javascript复制public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 执行任务,并返回结果
return 42;
}
}
在上述代码中,MyCallable实现了Callable接口,并返回一个Integer类型的结果。call()方法定义了要执行的任务,并返回执行结果。
3、具体例子
使用线程池ExecutorService执行一个Callable任务,并获取返回结果:
代码语言:javascript复制import java.util.concurrent.*;
public class Example {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(1);
Callable<Integer> task = new MyCallable();
Future<Integer> future = executor.submit(task);
try {
Integer result = future.get(); // 获取返回结果
System.out.println("Result: " result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
例子解析:Example创建了一个线程池ExecutorService,并提交了一个MyCallable任务。通过调用Future的get()方法获取任务的返回结果。如果任务执行成功,则返回结果;如果任务执行失败,则抛出ExecutionException异常。最后,Example关闭线程池。
4、Callable与Runnable比较
- Callable接口可以返回执行结果,而Runnable接口不能。
- Callable接口可以抛出异常,而Runnable接口不能。
- Callable接口的实现类需要实现call()方法,而Runnable接口的实现类需要实现run()方法。
- Runnable接口通常用于实现简单的后台任务,而Callable接口通常用于需要返回结果的复杂任务。
- Runnable和Callable的设计目标不同:Runnable设计目标是在多线程环境下提供简单的执行代码的机制;Callable设计目标是在多线程环境下提供具有返回值的任务执行机制。
- 在使用线程池时,通常使用Callable接口,因为线程池可以更好地管理线程资源,并允许在任务完成后自动关闭线程。而Runnable通常直接提交给Thread对象来执行。
- Callable可以在任务执行前进行初始化操作;而Runnable没有这样的机制。比如:可以在任务执行前对局部变量进行初始化;或者在任务执行前进行资源的预分配等。这种特性在某些场景下非常有用。比如:一个任务需要大量计算和内存消耗,为了减少垃圾回收的频率,可以在任务执行前进行内存的大量分配。但是请注意:大量使用这种方式可能会导致内存的浪费和内存溢出等问题。因此在使用时需要谨慎考虑其性能和内存消耗问题。
当然Callable接口主要是创建线程的新方式,目前我们学过的创建线程的方式总结如下: 1、继承Thread类 2、实现Runnable接口 3、实现Callable接口 4、线程池
二、ReentrantLock
1、介绍
ReentrantLock是一种可重入锁,它允许一个线程多次获取同一个锁,而不会产生死锁。ReentrantLock在加锁时采用了公平锁策略,即等待时间最长的线程将获得锁。与内置的synchronized锁不同,ReentrantLock是非阻塞的,它允许线程在等待锁的过程中执行其他任务。
2、ReentrantLock的使用
通常,我们使用ReentrantLock时需要先创建一个ReentrantLock对象,然后在需要加锁的地方调用lock()方法获取锁,执行任务后调用unlock()方法释放锁。
代码语言:javascript复制import java.util.concurrent.locks.ReentrantLock;
public class MyClass {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
lock.lock(); // 加锁
try {
// 执行任务
} finally {
lock.unlock(); // 释放锁
}
}
}
代码解析:MyClass创建了一个ReentrantLock对象,并在doSomething()方法中进行了加锁和释放锁的操作。使用try-finally语句块确保在任务执行完毕后释放锁。
3、具体例子
可以使用ReentrantLock实现线程安全的计数器:
代码语言:javascript复制import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 加锁
try {
count ;
} finally {
lock.unlock(); // 释放锁
}
}
public int getCount() {
return count;
}
}
通常,我们使用的计数器只支持我们在单线程下使用,如果想要在多线程下使用,通常需要加锁,Counter创建了一个ReentrantLock对象和一个计数器变量count。increment()方法用于对计数器进行加1操作,并使用ReentrantLock进行同步。getCount()方法返回计数器的当前值。由于使用了ReentrantLock,这个计数器可以在多线程环境下安全地使用。
4、ReentrantLock和Synchronized的区别
1、底层实现:synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
2、实现原理:synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁;ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
3、是否可手动释放: synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。
4、是否可中断synchronized是不可中断类型的锁,除非加锁的代码中出现异常或非正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
5、是否公平锁synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁,公平锁性能非常低。
三、Atomic 原子类
1、介绍
在多线程环境下,多个线程可能会同时访问和修改同一个共享变量,这时就需要一种机制来保证操作的原子性,以避免出现竞争条件和数据不一致的问题。Java中的Atomic原子类提供了一种高效的方式来解决这个问题。Atomic原子类通过使用CPU的原子指令来保证操作的原子性,避免了使用锁的开销和竞争。
2、Atomic 使用
代码语言:javascript复制import java.util.concurrent.atomic.AtomicInteger;
public class MyApp {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
// 增加计数器值
counter.incrementAndGet();
// 获取计数器值
int value = counter.get();
System.out.println("Counter value: " value);
}
}
3、具体例子
也是和上面一样实现一个线程安全的计数器:
代码语言:javascript复制import java.util.concurrent.atomic.AtomicLong;
public class AtomicCounter {
private AtomicLong counter = new AtomicLong(0);
public void increment() {
counter.incrementAndGet();
}
public long get() {
return counter.get();
}
}
代码解析:我们创建了一个AtomicLong对象counter,并使用incrementAndGet()方法增加计数器的值。然后使用get()方法获取计数器的值。由于AtomicLong提供了原子操作,因此即使多个线程同时访问和修改计数器,也不会出现竞争条件和数据不一致的问题。
4、Atomic特点
- 原子性:Atomic原子类提供了一种在多线程环境下进行原子操作的方式,保证了操作的原子性。
- 非阻塞性:Atomic原子类避免了使用锁的开销和竞争,从而提高了程序的响应能力和吞吐量。
- 高效性:Atomic原子类通过使用CPU的原子指令来保证操作的原子性,避免了出现竞争条件和数据不一致的问题。
- 可移植性:Atomic原子类的实现与Java虚拟机规范保持一致,因此可以在不同的Java虚拟机上使用。