Lock 用于解决互斥问题,Condition 用于解决同步问题
Semaphore:信号量。 Semaphore 可以实现一个互斥锁(令信号量等于1),Semaphore 还有一个功能是 Lock 不容易实现的,那就是:Semaphore 可以允许多个线程访问一个临界区,例如在池化设计中,限流器限制不能多于n个线程进入临界区。
// init
static final Semaphore s = new Semaphore(1);
// down操作
s.acquire();
// up操作
s.release()
ReadWriteLock:读写锁
- 允许多个线程同时读共享变量;
- 只允许一个线程写共享变量;
- 如果一个写线程正在执行写操作,此时禁止读线程读共享变量
ReentrantReadWriteLock是ReadWriteLock的常见实现类,有两种实现ReentrantReadWriteLock(boolean fair)
公平和非公平
- 公平:谁来都要排队,减少线程饥饿。
- 非公平:写锁直接插队、读锁会查看当前队列中第一个是不是要获取写锁,是写锁就不能插队
以上代码可以查看ReentrantReadWriteLock类中的NonfairSync的writerShouldBlock()和readerShouldBlock()
StampedLock: 支持写锁、悲观读锁和乐观读。StampedLock里的写锁和悲观读锁加锁成功之后会返回一个stamp;然后解锁的时候需要传入这个stamp。这个stamp其实就相当于乐观锁中的version。
final StampedLock sl = new StampedLock();
// 获取 / 释放悲观读锁示意代码
long stamp = sl.readLock();
try {
// 省略业务相关代码
} finally {
sl.unlockRead(stamp);
}
// 获取 / 释放写锁示意代码
long stamp = sl.writeLock();
try {
// 省略业务相关代码
} finally {
sl.unlockWrite(stamp);
}
StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。
StampedLock是ReadWriteLock的子集,不支持重入。悲观读锁、写锁都不支持条件变量。如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()
CountDownLatch:解决一个线程等待多个线程的场景
// 创建 2 个线程的线程池
Executor executor = Executors.newFixedThreadPool(2);
while(存在未对账订单){
// 计数器初始化为 2
CountDownLatch latch = new CountDownLatch(2);
// 查询未对账订单
executor.execute(()-> {
pos = getPOrders();
latch.countDown();
});
// 查询派送单
executor.execute(()-> {
dos = getDOrders();
latch.countDown();
});
// 等待两个查询操作结束
latch.await();
// 执行对账操作
diff = check(pos, dos);
// 差异写入差异库
save(diff);
}
CyclicBarrier:一组线程之间互相等待
// 订单队列
Vector<P> pos;
// 派送单队列
Vector<D> dos;
// 执行回调的线程池
Executor executor = Executors.newFixedThreadPool(1);
final CyclicBarrier barrier = new CyclicBarrier(2, ()->{
executor.execute(()->check());
});
void check(){
P p = pos.remove(0);
D d = dos.remove(0);
// 执行对账操作
diff = check(p, d);
// 差异写入差异库
save(diff);
}
void checkAll(){
// 循环查询订单库
Thread T1 = new Thread(()->{
while(存在未对账订单){
// 查询订单库
pos.add(getPOrders());
// 等待
barrier.await();
}
});
T1.start();
// 循环查询运单库
Thread T2 = new Thread(()->{
while(存在未对账订单){
// 查询运单库
dos.add(getDOrders());
// 等待
barrier.await();
}
});
T2.start();
}
CountDownLatch 的计数器是不能循环利用的,也就是说一旦计数器减到 0,再有线程调用 await(),该线程会直接通过。但CyclicBarrier 的计数器是可以循环利用的,而且具备自动重置的功能,一旦计数器减到 0 会自动重置到你设置的初始值。除此之外,CyclicBarrier 还可以设置回调函数
常用并发容器
List
List里边只有一个实现类就是CopyOnWriteArrayList
。写的时候复制一份出来,读操作无锁。内部维护一个数组,成员变量array指向这个内部数组,所有的读操作都是基于这个array进行。迭代器Iterator遍历的就是这个array数组。所以CopyOnwriteArrayList
的迭代器是只读的,不支持修改。对快照修改是没有意义的。
写操作的时候将array复制一份,然后再新复制的数组上执行写操作,执行完再将array指向这个新的数组。
应用场景:用于写操作非常少的场景,而且能够容忍读写的短暂不一致。
Map
Map 接口的两个实现是 ConcurrentHashMap
和 ConcurrentSkipListMap
,它们从应用的角度来看,主要区别在于ConcurrentHashMap 的 key 是无序的,而 ConcurrentSkipListMap 的 key 是有序的。所以如果你需要保证 key 的顺序,就只能使用 ConcurrentSkipListMap
。
跳表插入、删除、查询操作平均的时间复杂度是 O(log n),理论上和并发线程数没有关系,所以在并发度非常高的情况下,若你对 ConcurrentHashMap
的性能还不满意,可以尝试一下ConcurrentSkipListMap
。
Set
Set 接口的两个实现是 CopyOnWriteArraySet
和 ConcurrentSkipListSet
,使用场景可以参考前面讲述的 CopyOnWriteArrayList
和 ConcurrentSkipListMap
,它们的原理都是一样的。
Queue
队列可以按照阻塞和非阻塞和单端和双端
阻塞队列:当队列满了入队操作会阻塞;当队列为空出队操作阻塞
单端队列只能从队尾入队,从队首出列。
ThreadPoolExecutor
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
CompletableFuture
// 使用默认线程池
static CompletableFuture<Void> runAsync(Runnable runnable)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
// 可以指定线程池
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
Runnable 接口的 run() 方法没有返回值,而 Supplier 接口的 get() 方法是有返回值的。
默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池的线程数)。如果所有 CompletableFuture 共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议你要根据不同的业务类型创建不同的线程池,以避免互相干扰。
CompletableFuture 类实现了 CompletionStage 接口
原子类
- Atomic*基本类型原子类
- AtomicInteger
- AtomicLong
- AtomicBoolean
- Atomic*Array数组类型原子类
- AtomicIntegerArray
- AtomicLongArray
- AtomciReferenceArray
- Atomic*Reference引用类型原子类
- AtomicReference
- AtomicStampedReference
- AtomicMarkableReference
- Atomic*FieldUpdater升级类型原子类
- AtomicIntegerfieldupdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
- Adder累加器
- LongAdder:利用了多段锁,性能远远高于AtomicLong,大概差一个数量级
- DoubleAdder
- Accumulator累加器
- LongAccumulator
- DoubleAccumulator
CAS
Java中如何利用CAS实现原子操作
- AtomicInteger加载Unsafe工具,直接操作内存数据
- 用Unsafe来实现底层操作
- 用volatile修饰value字段,保证可见性
- getAndAddInt方法分析
BlockingQueue的主要方法
- put、take
- add、remove、element
- offer、poll、peek
AQS
全称AbstractQueuedSynchronizer
抽象队列同步器,许多同步类实现都依靠他。如常用的ReentrantLock、Semaphore、CountDownLatch...
核心三部分
- state。
- 控制线程抢锁和配合的FIFO队列。
- 期望协作工具类去实现的获取/释放等重要方法。
state
state在不同的实现类中有不同的含义,在Semaphore中,表示剩余的许可的数量。在CountDownLathch
里,表示还需要倒数的数量。在ReentrantLock
中state用来表示锁的占用情况,包括可重入计数,当state的值为0的时候,标识该Lock不被任何线程所占有。
state是volatitle修饰的,会被并发的修改,所以所有的修改state的方法都需要保证线程安全,比如getState、setState以及compareAndSetState操作来读取和更新这个状态。这些方法都依赖于j.u.c.atomic包的支持。
控制线程抢锁和配合的FIFO队列
这个队列用来存放等待的线程
,AQS就是一个排队管理器,当多个线程争用同一把锁时,必须有排队机制将那些没有拿到锁的线程串起来。当锁释放时,锁管理器就会挑选一个合适的线程来占用这个刚刚释放的锁
AQS会维护一个等待的线程队列,把这个线程都放到这个队列里
这是一个双向形式的队列。
期望协作工具类去实现的获取/释放等重要方法。
这里的获取和释放方法,是利用AQS的协作工具类里最重要的方法,是由协作类自己去实现的,并且含义各不相同。
获取方法
- 获取操作依赖state变量,经常会阻塞(获取不到锁的时候)
- 在
Semaphore
中,获取就是acquire
方法,作用就是获取一个许可证 - 在
CountDownLatch
中,获取就是await
方法,作用就是等待,直到倒数结束。
释放方法
- 释放操作不会阻塞
- 在
Semaphore
中,释放就是release
方法,作用就是释放一个许可证 - 在
CountDownLatch
中,释放就是countDown
方法,作用就是倒数一个数。
还需要重写tryAcquire
和tryRelease
等方法。
AQS结构图