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
- 本文固定链接: https://weiguangli.com/archives/642
- 转载请注明: lwg0452 于 Weiguang的博客 发表