多线程与并发编程 (2/3)
1 Java并发包(JUC)
1.1 JDK 核心库的包

其中,Java 的并发工具类定义在 java.util.concurrency 包中。
1.2 java.util.concurrency

JUC 包中主要包括一下几种类:
**锁机制类Locks : **Lock, Condition, ReentrantLock, ReadWriteLock,LockSupport **原子操作类Atomic : **AtomicInteger, AtomicLong, LongAdder **线程池相关类Executor : **Future, Callable, Executor, ExecutorService **信号量三组工具类Tools : **CountDownLatch, CyclicBarrier, Semaphore **并发集合类Collections : **CopyOnWriteArrayList, ConcurrentMap
2 到底什么是锁*
2.1 为什么需要显式的Lock
synchronized 方式的问题: 1、同步块的阻塞无法中断(不能Interruptibly); 2、同步块的阻塞无法控制超时(无法自动解锁); 3、同步块无法异步处理锁(即不能立即知道是否可以拿到锁); 4、同步块无法根据条件灵活的加锁解锁(即只能跟同步块范围一致)。
2.2 更自由的锁: Lock
Lock 接口设计:
// 1.支持中断的API
void lockInterruptibly() throws InterruptedException;
// 2.支持超时的API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 3.支持非阻塞获取锁的API
boolean tryLock();
- 使用方式灵活可控
- 性能开销小
- 锁工具包: java.util.concurrent.locks
基础接口 Lock

Lock 使用示例
public class LockCounter {
private int sum = 0;
// 可重入锁+公平锁
private Lock lock = new ReentrantLock(true);
public int addAndGet() {
try {
lock.lock();
return ++sum;
} finally {
lock.unlock();
}
}
public int getSum() {
return sum;
}
}
测试使用 Lock
// 测试代码
public static void testLockCounter() {
int loopNum = 100_0000;
LockCounter counter = new LockCounter();
IntStream.range(0, loopNum).parallel().forEach(i -> counter.incrAndGet());
}
**可重入锁:**当前已经持有一个锁,再次请求获取这个已经持有的锁的时候,不会发生阻塞。
**公平锁:**按照请求的先后获取锁。
**非公平锁:**所有人获得锁的机会均等。
2.3 读写锁 接口与实现

读读不互斥
读写互斥
写写互斥
构造方法
// 构造方法
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
读写锁的例子
public class ReadWriteLockCounter {
private int sum = 0;
// 可重入-读写锁-公平锁
private ReadWriteLock lock = new ReentrantReadWriteLock(true);
public int incrAndGet() {
try {
lock.writeLock().lock(); // 写锁; 独占锁; 被读锁排斥
return ++sum;
} finally {
lock.writeLock().unlock();
}
}
public int getSum() {
try {
lock.readLock().lock(); // 读锁; //共享锁; 保证可见性
return sum;
} finally {
lock.readLock().unlock();
}
}
}
注意:ReadWriteLock 管理一组锁,一个读锁,一个写锁。 读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。 所有读写锁的实现必须确保写操作对读操作的内存影响。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock 适用于读多写少的并发情况。
2.4 基础接口 Condition

通过 Lock.newCondition() 创建。 可以看做是 Lock 对象上的信号。类似于 wait / notify。
2.5 LockSupport 锁当前线程

LockSupport 类似于Thread 类的静态方法,专门处理(执行这个代码的)线程的。
2.6 用锁的最佳实践
Doug Lea《Java 并发编程:设计原则与模式》一书中,推荐的三个用锁的最佳实践,它们分别是:
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁
KK 总结 - 最小使用锁: 1、降低锁范围:锁定代码的范围/作用域 2、细分锁粒度:讲一个大锁,拆分成多个小锁
3 并发原子类*
Atomic 原子类工具包位于 java.util.concurrent.atomic,该工具包中包含了 AtomicInteger、AtomicLong、LongAdder 等类。

3.1 无锁技术
无锁技术的底层实现原理: • Unsafe API - CompareAndSwap • CPU 硬件指令支持- CAS 指令 • Value 的可见性- volatile 关键字
核心实现原理: 1、volatile 保证读写操作都可见(注意不保证原子); 2、使用CAS 指令,作为乐观锁实现,通过自旋重试保证写入。
原子类对数据的操作是原子性的,当需要对一个变量进行修改的时候,首先从内存中读取变量的值,然后在栈中对数据进行修改,在将数据写回到内存之前,原子类再次读取这个变量在内存中的值,如果与之前相同则直接将数据写回;如果不同,则重新读取数据到栈中,重复以上操作。
两次读取内存发现数据相同如何确保数据没有被修改过:
- 两次数据相同的状态是等价的
- 每次修改数据,版本号加1,同时比较变量的值和版本号
3.2 有锁好还是无锁好?
CAS 本质上没有使用锁。 并发压力跟锁性能的关系: 1、压力非常小,性能本身要求就不高; 2、压力一般的情况下,无锁更快,大部分都一次写入; 3、压力非常大时,自旋导致重试过多,资源消耗很大,加锁更好。
3.3 LongAdder 对 AtomicLong 的改进
AtomicLong 通过分段思想改进原子类,LongAdder 的改进思路: 1、AtomicInteger 和 AtomicLong 里的 value 是所有线程竞争读写的热点数据; 2、将单个value 拆分成跟线程一样多的数组 Cell[ ]; 3、每个线程写自己的 Cell[ i ]++,最后对数组求和。
4 并发工具类详解*
wait / notify 以及 Lock / Condition 可以作为简单的协作机制。但是更复杂的使用场景需要这些线程满足某些条件(例如数量、时间)。
更复杂的应用场景:
- 我们需要控制实际并发访问资源的并发数量
- 我们需要多个线程在某个时间同时开始运行
- 我们需要指定数量线程到达某个状态再继续处理
4.1 AQS

AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础(如 Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock),是 JUC 并发包中的核心基础组件,抽象了竞争的资源和线程队列。 • AbstractQueuedSynchronizer:抽象队列式的同步器 • 两种资源共享方式: 独占| 共享,子类负责实现公平 OR 非公平
4.2 Semaphore 信号量
- 准入数量N, N =1 则等价于独占锁
- 相当于synchronized 的进化版
使用场景:同一时间控制并发线程数
4.3 CountdownLatch
阻塞主线程,N 个子线程满足条件时主线程继续。 场景: Master 线程等待 Worker 线程把任务执行完,Master 线程再继续执行。 示例: 等所有人干完手上的活,一起去吃饭。

4.4 CyclicBarrier
场景: 任务执行到一定阶段, 等待其他任务对齐,阻塞N 个线程时所有线程被唤醒继续。 示例: 等待所有人都到达,再一起开吃。

4.5 CountDownLatch与CyclicBarrier比较


4.6 Future / FutureTask / CompletableFuture


