
JUC编程
06 Callable
java
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
- Callable泛型的参数<V>为 call方法的返回值
如何启动线程调用Callable接口?
java
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// new Thread().start(); 只能执行Runnable接口
MyThread myThread = new MyThread(); // Callable
FutureTask<String> futureTask = new FutureTask<>(myThread); // Callable -> FutureTask(Runnable)
new Thread(futureTask, "A").start();
new Thread(futureTask, "B").start();
String result = futureTask.get();
// 获取Callable的返回结果 等待结果可能会产生阻塞! 一般放在代码最后 或者采用异步通信
System.out.println(result);
}
}
// Callable泛型的参数为 call方法的返回值
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("call()");
return "1024";
}
}
-
利用Runnable接口的实现类FutureTask,使用构造函数FutureTask(Callable<V> callable)搭建Callable和Runnable之间的桥梁。
-
利用futureTask.get()方法获取Callable的返回值。
FutureTask类使用的两个注意事项
-
get方法需要获得返回结果,可能会产生阻塞!一般放在代码最后,或者采用异步通信来解决。
-
多个线程同时启动调用Callable,call方法只会执行一次。
-
FutureTask.run() 内部用 state + CAS(runner) 保证只有一个线程能真正进入 call() 执行区。
run方法中:
javaif (state != NEW || !RUNNER.compareAndSet(this, null, Thread.currentThread())) return;- state != NEW:如果任务不是"新建状态",说明它要么已经执行过、要么取消了、要么正在结束流程,直接return,不再执行。
- !RUNNER.compareAndSet(this, null, Thread.currentThread()):用CAS原子地把runner从null设置为当前线程。
- 成功:表示当前线程抢到了执行权。
- 失败:说明别的线程已经在执行(或刚执行完但还没清理),当前线程直接return,防止并发重复执行。
-
07 JUC常用辅助类
1、CountDownLatch
CountDownLatch 用来让一个(或多个)线程等待其他线程把事情做完,再继续往下执行。
关键词只有两个:
- 我等你
- 你们做完,我再走
把它想成一个 倒计时闸门:
- 初始化一个数字(这里是 6)
- 每调用一次
countDown(),数字减 1 - 当数字变成 0 :所有在
await()上等待的线程 同时被放行 - 一旦归零,就再也回不去了
java
// 计数器
public class CountDownLatchTest {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "Go out!");
countDownLatch.countDown(); // 数量-1
}, String.valueOf(i)).start();
}
try {
countDownLatch.await(); // 等待计数器归零 然后再向下执行
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Close door!");
}
}
- public void await():导致当前线程等到锁存器计数到零,除非线程是interrupted 。
- public void countDown():减少锁存器的计数,如果计数达到零,释放所有等待的线程。
总结:CountDownLatch 是一种同步工具,用于使一个或多个线程等待,直到其他线程完成一组操作,其内部通过一个不可重置的计数器实现线程间的协调。
2、CyclicBarrier
CyclicBarrier 是一个"让一组线程互相等待,等人到齐后再一起继续执行"的同步工具。
关键词只有三个:
- 一组线程
- 彼此等待
- 到齐后同时放行
CyclicBarrier的两个核心参数:
- parties:
java
CyclicBarrier cyclicBarrier = new CyclicBarrier(7);
必须有 7 个线程调用 await(),屏障才会被打开。
-
barrierAction(可选):
java() -> {System.out.println("召唤神龙!");}当第 7 个线程到达屏障时,先执行这段代码。注意:
- 只执行一次
- 由 最后一个到达的线程 执行
javapublic class CyclicBarrierTest { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(7, ()-> System.out.println("召唤神龙!") // 当第 7 个线程到达屏障时,先执行这段代码 ); for (int i = 0; i < 7; i++) { final int temp = i % 7 + 1; new Thread(()->{ System.out.println(Thread.currentThread().getName() + "收集了第" + temp + "颗龙珠!"); try { cyclicBarrier.await(); // 等待 必须有 7 个线程调用 await(),屏障才会被打开 } catch (InterruptedException | BrokenBarrierException e) { throw new RuntimeException(e); } System.out.println("开始许愿!"); // await() 返回,线程继续执行后面的代码 }, "Thread-" + i).start(); } } }
注意点:Lambda 只能捕获 final / effectively final 的局部变量,是为了避免线程执行时变量已经改变或消失,从而保证语义清晰和线程安全。
总结:CyclicBarrier 用于协调一组线程在某个同步点相互等待,直到所有线程都到达后,再统一继续执行,并且支持在屏障触发时执行一个统一的动作,同时屏障可重复使用。
3、Semaphore
Semaphore 用来控制"同时能访问某个资源的线程数量"。
关键词只有一个:
- 限流(并发数控制)
关键参数:
-
创建 Semaphore:最多允许 2 个线程同时进入临界区(只有两个停车位)
javaSemaphore semaphore = new Semaphore(2);注意:
- 2不是线程总数
- 是并发访问的最大数量
-
请求许可:要一个许可证(进入停车位)
javasemaphore.acquire();- 如果当前还有permit:立即获得,继续执行。
- 如果permit用完:线程阻塞,排队等待。
-
释放许可:释放许可证(离开停车位)
javasemaphore.release();- permit+1
- 如果有线程在等待:唤醒其中一个(或多个)
为什么release放在finally?
java
finally {
semaphore.release();
}
即时线程中途抛出异常,也必须释放许可,否则会"永久占坑",导致死锁。
Semaphore的几个关键特性
-
控制的是数量,不是顺序
Semaphore只关心有几个permit,不保证线程执行顺序。
-
acquire/release可以不成对,但不推荐。
-
可用来做互斥锁,等价于同一时刻只允许一个线程进入。类似synchronized/ReentrantLock
javaSemaphore mutex = new Semaphore(1); -
支持公平/非公平
javaSemaphore semaphore = new Semaphore(2, true);- true:先到先得
- false:可能插队,性能更好(默认)
Semaphore 是一种计数信号量,用于控制同时访问某一资源的线程数量,通过许可证(permit)的获取与释放来实现并发访问的限流。
08 读写锁ReadWriteLock
ReadWriteLock通过"读读并发、读写互斥、写写互斥"的机制,在读多写少的场景下显著提高并发性能。
关键词:
- 读共享
- 写独享
- 提升吞吐
java
public class ReadWriteLockTest {
public static void main(String[] args) {
// MyCache myCache = new MyCache();
MyLockCache myCache = new MyLockCache();
// 写线程
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(()-> myCache.put(String.valueOf(temp), temp), String.valueOf(i)).start();
}
// 读线程
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(()-> myCache.get(String.valueOf(temp)), String.valueOf(i)).start();
}
}
}
// 自定义缓存(无锁)
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
// 存 写
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入成功!");
}
// 取 读
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取成功!");
}
}
// 自定义缓存(加锁)
class MyLockCache {
private volatile Map<String, Object> map = new HashMap<>();
private ReadWriteLock lock = new ReentrantReadWriteLock();
// 存 写
public void put(String key, Object value) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入成功!");
} catch (Exception ex) {
ex.printStackTrace();
}finally {
lock.writeLock().unlock();
}
}
// 取 读
public void get(String key) {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取成功!");
} catch(Exception ex) {
ex.printStackTrace();
}finally {
lock.readLock().unlock();
}
}
}
1、为什么需要ReadWriteLock?
java
private volatile Map<String, Object> map = new HashMap<>();
问题在于:
- volatile关键字只保证可见性 ,不保证复合操作原子性。
- HashMap再并发写时不是线程安全的。
如果使用synchronized会怎样?
java
synchronized void put(...) { ... }
synchronized void get(...) { ... }
这样也是安全的,但是问题是:读和读也互斥,在**"读多写少"**的场景下,性能浪费很大。
2、ReadWriteLock的核心规则(重点)
以ReentrantReadWriteLock为例:
- 读-读:可以并发
- 多个线程同时读
- 互不阻塞
- 读-写:互斥
- 有线程在读(写)
- 写(读)线程必须等
- 写-写:互斥
- 同一时间只允许一个写线程
写锁是"独占锁",读锁是"共享锁"。
3、关键代码
-
写操作(put):
javalock.writeLock().lock();申请独占锁:
- 若有其他读锁/写锁 -> 阻塞
- 成功后:没有任何读/写线程能同时访问map
-
读操作(get):
javalock.readLock().lock();申请共享锁:
- 若当前没有写锁:多个线程可以同时拿到读锁
- 若有写锁:必须等待
-
finally解锁:
finally { lock.readLock().unlock(); }- 防止异常导致锁永远不释放 -> 全部线程阻塞
4、一个重要但容易踩的坑
-
不能"锁升级"(读->写)
javalock.readLock().lock(); // 想升级为写锁 lock.writeLock().lock(); // 可能死锁- 写锁要求"没有任何读锁"
- 但当前线程自己还持有读锁
-
可以"锁降级"(写->读)
javalock.writeLock().lock(); try { // 修改数据 lock.readLock().lock(); } finally { lock.writeLock().unlock(); } // 仍然持有读锁
ReadWriteLock 通过将访问分为"共享读"和"独占写",在保证线程安全的前提下显著提高了读多写少场景下的并发性能,其典型实现是 ReentrantReadWriteLock。
09 队列Queue
1、BlockingQueue
用"队列 + 阻塞等待"把生产者线程和消费者线程安全地连接起来,实现线程间通信与解耦。
四组API
| 方法/方式 | 抛出异常 | 返回值 | 阻塞等待 | 超时等待 |
|---|---|---|---|---|
| 添加 | add() | offer() | put() | offer(,) |
| 移除 | remove() | poll() | take() | poll(,) |
| 检索队首元素 | element() | peek() | - | - |
| 适合 | 你希望立刻知道出错, 不想等待。 | 你不想阻塞线程, 宁可拿不到就算了。 | 标准生产者-消费者模型(最常用)。 | 不想无限等待,业务上 允许超时处理。 |
BlockingQueue的作用是什么?
- 线程安全的队列操作:多线程同时put/take不会把队列搞乱。
- 自动协调生产与消费速度:
- 队列满:生产者自动等待(阻塞)
- 队列空:消费者自动等待(阻塞)
BlockingQueue适合写生产者-消费者模型、线程池任务队列、消息队列缓冲等。
2、SynchronousQueue
用于在线程之间"直接传递"元素(handoff),实现生产者和消费者一对一同步。
特点
- 容量为0(没有内部缓冲)
- put(x)必须等待另一个线程take()把x接走才会返回
- take()必须等到另一个线程put()递过来元素才会返回
常用于:
- 线程池(比如CachedThreadPool的任务移交)
- 不希望排队/缓存,只希望"来一个交一个"的场景
- 限制生产者不能超前生产(天然背压:下游处理不过来时,反向给上游"施加压力",迫使上游放慢或停下生产)
SynchronousQueue的原理
可以看成"没有仓库的快递交接点":
- 生产者到了就把包裹递出来,但没人接就只能等
- 消费者到了就准备接收包裹,但没人递也只能等
- 一旦双方都到齐,就完成一次配对(match)/交换(exchange)
内部怎么做配对?
实现上SynchronousQueue不使用数组存元素,而是维护一个"等待者结构":
- 要么有等待put的线程在排队(携带元素)
- 要么有等待take的线程在排队(等待接收)
当对方到来时:
- 直接把元素交给对方
- 唤醒对方线程
- 双方各自返回