JUC包常用类
- 一、atomic包
- 1.基本类型
- 2.数组类型
- 3.引用类型
- 4.对象的属性修改类型
- 二、locks包
- 1.ReentrantLock
- 2.ReentrantReadWriteLock
- 3.AQS类(`AbstractQueuedSynchronizer`)
- 4.补充:ReentrantLock和Synchronized对比
- 三、同步工具类
- 1.CountDownLatch(倒计时器)
- 2.Semaphore(信号量)
- 3.Cyclicbarrier(循环栅栏)
- 4.FutureTask
- 5.补充
- `CountDownLatch` 和 `CyclicBarrier` 的不同之处
- 四、并发容器类
- 1.ConcurrentHashMap
- 2.CopyOnWriteArrayList
- 3.ConcurrentLinkedQueue
- 4.BlockingQueue
- ArrayBlockingQueue
- LinkedBlockingQueue
- PriorityBlockingQueue
- 5.ConcurrentSkipListMap
- 五、Executor框架相关类
- 1.简介
- 2.结构
- 3.使用
- 4.ThreadPoolExecutor(核心)
- 对比
- 常见线程池
主要包含
- atomic包:
- 基本类型:AtomicInteger、AtomicLong、AtomicBoolean
- 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 引用类型:AtomicReference、AtomicMarkableReference、AtomicStampedReference
- 对象的属性修改类型:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
- locks包:ReentrantLock、ReentrantReadWriteLock
- 同步工具类:CountDownLatch、Semaphore、Cyclicbarrier、FutureTask
- 并发容器类:ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、BlockingQueue、ConcurrentSkipListMap
- Executor框架相关类
一、atomic包
1.基本类型
- AtomicInteger、AtomicLong、AtomicBoolean
- 原子性更新基本类型
- 常用方法get()、getAndSet(int)、getAndIncrement()、getAndAdd(int)、compareAndSet(int, int)等
- 优点
- 不需要加锁就可以保证线程安全
- 线程安全原理
- CAS+volatile+(native方法),保证线程总能拿到变量的最新值
2.数组类型
- AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 原子性更新数组中某个元素
- 常用方法get(int)、getAndSet(int, int)、getAndIncrement(int)、getAndAdd(int, int)、compareAndSet(int, int, int)等
3.引用类型
- AtomicReference、AtomicMarkableReference(带有版本号,可以解决ABA问题)、AtomicStampedReference(带有标记,不能解决ABA问题)
- 可以原子性更新多个变量
- 常用方法:set()、compareAndSet()、get()等
- 将需要修改的对象放入AtomicReference 对象中set(Object),再修改整个对象compareAndSet(Object, Object),另外两个需要设置版本号(Integer)或标记(Boolean)
4.对象的属性修改类型
- AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
- 可以原子性修改对象内部分字段
- 常用方法newUpdater()等
- 先创建更新器,传入需要修改的类和字段newUpdater(class,String),修改的字段必须是
public volatile
修饰的
二、locks包
1.ReentrantLock
- 可重入锁,一个线程能够对共享资源多次加锁。
- 支持公平锁和非公平锁
- 加锁流程:
- 若通过 CAS 设置变量 State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。否则使用Acquire 方法进行后续处理(将线程放入队列等待唤醒)。
- Lock方法实际调用内部类Sync的Lock方法,本质上执行AQS的Acquire方法,Acquire方法会执行tryAcquire方法,如果获取锁失败,执行AQS后续方法。
- 解锁流程:
- Unlock方法实际调用内部类Sync的Release方法(继承于AQS),Release调用tryRelease方法,释放成功后,执行AQS后续方法。
2.ReentrantReadWriteLock
- 读写锁
ReentrantReadWriteLock
可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。
3.AQS类(AbstractQueuedSynchronizer
)
- 构建锁和同步器的框架,使用 AQS 能简单且高效地构造出大量应用广泛的同步器,包括以下类:
ReentrantLock
,ReentrantReadWriteLock
,SynchronousQueue
,FutureTask
,Semaphore
,CountDownLatch
- AQS在
ThreadPoolExecutor
的Worker中也有应用:利用 AQS 同步状态实现对独占线程变量的设置(tryAcquire 和 tryRelease) - 锁是针对使用者而言的,底层是通过队列同步器实现的。
- 子类仅使用getState(获取当前状态)、setState(设置状态)和compareAndSetState(CAS保证下设置状态)方法操作原子更新的int值(volatile修饰的同步状态值state)
- state:标记资源状态:未占用0,大于0每加1则加一重锁(可重入锁),每释放一重锁就减1。(可用于计算共享锁数量)
- head、tail:CLH队列的头节点和尾节点
- exclusiveOwnerThread:存放当前获得锁的线程(可重入锁的判断:该属性和申请锁的线程进行对比)
- 资源共享方式
- 独占Exclusive:只有一个线程能占有资源,如
ReentrantLock
- 共享Share:多个线程可以同时占有资源,如
Semaphore
和CountDownLatch
。 -
ReentrantReadWriteLock
允许多个线程同时对某一资源进行读,但是只允许一个线程进行写。
- 如何基于AQS设计同步器
- 自定义同步器类继承AQS并重写指定方法(钩子方法)
- 可选择性地重写:独占或共享可重写相应方法即可,也可全部重写实现读共享和写独占(
ReentrantReadWriteLock
)
//独占方式。获取和释放资源,arg为获取锁次数
protected boolean tryAcquire(int arg)
protected boolean tryRelease(int arg)
//共享方式。获取和释放资源
//负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int arg)
protected boolean tryReleaseShared(int arg)
//该线程是否正在独占资源。只有用到condition才需要实现。
protected boolean isHeldExclusively()
- 自定义锁组件类(实现Lock接口),重写加锁lock()、尝试加锁trylock()、带超时加锁trylock(long time, TimeUnit unit) throws InterruptedException、释放锁unlock()、条件变量newCondition()(可选)
- 将自定义同步器类组合进自定义锁组件类中,可以在锁组件类中调用同步器类的方法,也可以将同步器类作为锁组件类的内部类。
- 如果线程A请求的共享资源空闲,则将资源分配给A并将资源加锁。如果A请求的共享资源被占用了,则将A加入到CLH队列(FIFO,双向队列,公平)中。
- 线程调用acquire方法获取锁,获取失败则将线程阻塞,同时保存为一个Node,包含了线程的状态字段(waitStatus、prev、next等),插入到CLH同步队列的队尾,当同步状态变化时,将CLH队列的头节点唤醒。
- waitStatus:节点状态,默认0尝试获取锁,1获取锁请求取消,-2等待唤醒,-3共享锁下使用,-1线程准备好等待资源释放
- thread:节点对应线程
- prev、next:节点前驱和后继节点
- predecessor:返回前驱节点,没有则抛出npe
- 双向队列中,head节点为虚拟头节点,不存储任何信息。
- 入队:addWaiter(Node mode),其中插入时使用compareAndSetTail(Node expect, Node update)保证线程安全
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 插入到队列尾部
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果上面插入失败,则多次尝试
enq(node);
return node;
}
private Node enq(final Node node) {
// 自旋保证节点插入队列
for (;;) {
Node t = tail;
if (t == null) { // 队列为空,直接初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 否则多次尝试插入队列尾部
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 出队:acquireQueued(final Node node, int arg)、挂起shouldParkAfterFailedAcquire()
- 入队后,如果当前节点的前驱节点为头节点,则自旋获取锁,可以设置是否阻塞(LockSupport.park)防止cpu浪费
- 线程A入队如果前驱节点H状态(waitStatus)为0,则自旋获取锁,如果获取成功,则设A为新的头节点,否则将H状态设为-1(阻塞A)。
- 当有线程释放锁时发现头节点状态为-1就会唤醒其后继节点。这里不需要CAS保证因为只有后继节点能够获取锁(公平锁)
- 如果状态大于0则取消前驱节点cancelAcquire(Node node)直到前驱节点状态小于等于0,这时只修改next指针,当获取锁失败时(shouldParkAfterFailedAcquire)才修改prev指针。(防止之前的节点获取锁出队了,prev指针指向了队列外的节点,不安全)
4.补充:ReentrantLock和Synchronized对比
三、同步工具类
1.CountDownLatch(倒计时器)
- 可以让
count
个线程阻塞,直到所有线程执行完毕。用于主线程等待其他线程处理完毕后再执行、实现多个线程在某一时刻同时开始执行。
- 商品详情页的加载需要等待商品信息、评论信息等数据加载完毕后再包装发送给前端
- 基于AQS的共享锁机制实现,默认构造AQS的
state
为count
。 - 不足:一次性的,使用不当容易产生死锁,count到不了0的话
- 使用:
// 1.主线程等待其他线程处理完毕后再执行
// 构造函数,初始化计数器count——550
final CountDownLatch countDownLatch = new CountDownLatch(550);
//
threadPool.execute(() -> {
try {
test(threadnum);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 一个线程经处理完毕,count减1
countDownLatch.countDown();
}
});
}
// 当count为0时唤醒countDownLatch上的线程,否则阻塞当前线程,不执行后面的代码
countDownLatch.await();
// 2.实现多个线程在某一时刻同时开始执行
// (1)初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 (new CountDownLatch(1))
// (2)多个线程在开始执行任务前首先 coundownlatch.await()
// (3)当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。
- countDown():让count减1,减为0时唤醒countDownLatch上所有线程
- await():使当前线程进入同步队列中等待,直到count为0,可带超时时间
- getCount():获取当前count
2.Semaphore(信号量)
- 可以指定多个线程同时访问某个资源。用于控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
- 数据库连接限制
- 基于AQS的共享锁机制实现
- 使用:
// 允许同时执行的线程数量——20。
final Semaphore semaphore = new Semaphore(20);
//获取、释放许可,也可一次获取和释放多个许可(一般不用)
threadPool.execute(() -> {
try {
semaphore.acquire();//获取
test(threadnum);
semaphore.release();//释放
} catch (InterruptedException e) {
e.printStackTrace();
}
});
- acquire():获取不到许可阻塞,响应中断
- tryAcquire():获取不到许可立即返回false,不阻塞不响应中断,可带超时时间
- acquireUninterruptibly(),获取不到许可阻塞,不响应中断
- 可设置公平模式和非公平模式
- 公平模式:调用acquire()方法遵循FIFO,可以使用Semaphore(int permits, boolean fair)构造。
- 非公平模式:抢占,使用Semaphore(int permits)构造默认是非公平模式。
- 公平模式在获取许可时先调用
hasQueuedPredecessors
方法判断队列中是否有其他线程在排队
3.Cyclicbarrier(循环栅栏)
- 和
CountDownLatch
类似,也可以实现多线程等待,但是功能更多。应用场景也和CountDownLatch
类似。所有线程到达设定的屏障(同步点)时才会继续运行,否则阻塞。 - 基于
ReentrantLock
和Condition
实现的(ReentrantLock
也是基于AQS) - 使用
// 构造函数,parties表示屏障拦截的线程数
// 当满足条件时,优先执行barrierAction
CyclicBarrier(int parties)
CyclicBarrier(int parties, Runnable barrierAction)
// 线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞
cyclicBarrier.await();
- await():实际调用
dowait(false, 0L)
方法,每次count减1,减为0时继续执行
4.FutureTask
- 传入Runnable或Callable任务给FutureTask,调用run()方法运行,之后外部线程调用get()方法异步获取结果。用于异步获取执行结果或取消执行任务,处理各个线程之间结果相互依赖的情况,适合耗时的计算。
- 继承了RunnableFuture(Runnable和Future),通过state(int值)标志对象状态,共有七种状态,状态转换通过CAS保证原子性
- 使用
//构造方法
FutureTask(Callable<V> callable)
FutureTask(Runnable runnable, V result)
- run():执行提交的任务
- get():检索结果值,未完成则阻塞线程,可设置超时时间
- cancel():取消任务。如果当前任务已经结束或者已经取消,则无法再次取消;如果任务尚未开始,则直接不执行任务。
5.补充
CountDownLatch
和 CyclicBarrier
的不同之处
-
CountDownLatch
是一次性的,只能用一次,而CyclicBarrier
可以多次使用(reset) - 当计数为0时,下一步的动作实施者是不一样的。对于
CountDownLatch
,,下一步的动作实施者是“主线程”;对于CyclicBarrier
,下一步动作实施者是“其他线程”。从定义理解:CountDownLatch
是一个或多个线程等待其他线程;而CyclicBarrier
是多个线程互相等待
四、并发容器类
1.ConcurrentHashMap
- 保证HashMap读写时的并发安全
- 详见java容器
2.CopyOnWriteArrayList
- 类似
ReentrantReadWriteLock
读操作时共享锁,写操作时独占锁,CopyOnWriteArrayList
的读操作完全不加锁,写操作不会阻塞读操作,只有写操作和写操作之间相互阻塞,大幅提高读操作性能。 - 原理:所有修改的操作都是通过创建底层数组的副本,然后修改副本的内容,修改完后再替换掉原来的数组,因此不会影响读操作。
CopyOnWrite
:修改时不在原来的数据上进行修改,而是将数据拷贝一份,修改新的数据,修改完后将指向原来数据的指针指向新的数据,最后回收原来的数据。- 常用方法
- 读取get(int)
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
final Object[] getArray() {
return array;
}
- 写入add(E):通过ReentrantLock对复制过程Arrays.copyOf()(本质上还是调用System.arraycopy())加锁,保证多线程安全
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
3.ConcurrentLinkedQueue
- 非阻塞队列,通过CAS操作实现,底层使用链表实现,性能好
- 适合性能要求高,多线程读写队列的情况(加锁成本较高)
4.BlockingQueue
- 阻塞队列(FIFO),通过加锁实现,提供可阻塞的插入和移除方法
- 广泛应用于“生产者-消费者”问题中,队列满时阻塞生产者线程;队列空时阻塞消费者线程
- 常用方法add()、offer()、remove()、poll()(同队列,不阻塞)、put()、take()(阻塞,可设置超时时间)
- 批量操作不一定是原子操作addAll()、removeall()等
- 常用实现类:
ArrayBlockingQueue
、LinkedBlockingQueue
、PriorityBlockingQueue
ArrayBlockingQueue
- 底层使用数组实现,创建后容量不能改变(有界),通过
ReentrantLock
对读写操作进行加锁,队列满时,阻塞插入操作;队列空时,阻塞移除操作。多个线程阻塞时默认是非公平的等待(ReentrantLock
默认非公平),可以通过以下构造函数实现公平等待
// 实际上还是将fair传入ReentrantLock实现公平锁
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
LinkedBlockingQueue
- 底层使用链表实现,创建时可以指定容量也可以不指定容量(上限为
Integer.MAX_VALUE
,可能占用大量内存) - 使用虚拟头节点,put和take操作需要获取相应的锁和条件(put时需要获取putLock且notFull,take时需要获取putLock且notFull。
PriorityBlockingQueue
- 基于数组的二叉堆(根节点最小)实现,支持优先级的无界阻塞队列,指定初始大小后面自动扩容(默认初始大小11,上限为
Integer.MAX_VALUE - 8
),不能插入null,插入对象必须可比较大小(comparable),put不会阻塞(无界),take会阻塞。 - 遍历时不能保证有序性,可以通过Arrays.sort(queue.toArray())得到有序数组,默认升序排序,可以通过以下构造函数自定义比较器
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
- 自动扩容机制:扩容前先释放锁(扩容和读操作可以同时进行),CAS操作保证原子性,设原来容量为x,如果x小于64,则增加x+2的容量;如果大于64,则增加x / 2的容量。扩容后判断是否超出最大容量,若超过则设置为最大容量(防止溢出)
- 复习:二叉堆的插入和删除(手写堆排序)
5.ConcurrentSkipListMap
- 底层基于跳表实现,遍历时保证有序性。对比基于哈希算法实现的Map是无序的。
- 跳表:多层链表,最底层为原始链表,包含所有元素,往上为一层层索引,上层元素为下层元素的子集,同一列元素是相同的,每一行链表都是排好序的
- 查找:从左到右从上到下查找,若查找元素大于当前元素,则跳到下一层继续向右查找
- 详见数据结构
五、Executor框架相关类
1.简介
- Executor框架是java5之后引入的,通过Executor启动线程比Thread的start方法更好,更易于管理、效率更好、有助于避免this逃逸问题
- this 逃逸:在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法
2.结构
- 任务
- Runnable或Callable接口(一个有返回一个无返回)
- 任务执行
- Executor和ExecutorService接口(继承自Executor)
- ThreadPoolExecutor 、ScheduledThreadPoolExecutor(继承了ThreadPoolExecutor,实现了ScheduledExecutorService) 实现了 ExecutorService 接口。
- 异步计算的结果
- Future接口及其实现类FutureTask
- 将任务实现类提交给ThreadPoolExecutor执行,调用submit()提交时会返回一个FutureTask对象
3.使用
- 创建Runnable或Callable接口的任务实现类
- 将实现类对象交给ExecutorService执行
- ExecutorService.execute(Runnable command)或ExecutorService.submit(Runnable task)
- 上一步中调用submit()方法会返回一个FutureTask对象,由于 FutureTask 实现了 Runnable,也可以直接创建 FutureTask,然后直接交给 ExecutorService 执行
- 最后,主线程可以执行FutureTask.get()方法来等待任务执行完成,或者执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行
4.ThreadPoolExecutor(核心)
- 创建线程池使用ThreadPoolExecutor而不是Executors!
对比
- shutdown()和shutdownNow()
- shutdown()关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务,但是队列里的任务得执行完毕。
- shutdownNow()关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,不再执行队列中的任务并返回任务队列。
- isTerminated()和isShutdown()
- 调用shutdown()后isShutdown()为true,等所有任务执行完毕后isTerminated()为true
常见线程池
- FixedThreadPool,可重用固定线程数的线程池
- 可设置corePoolSize 和 maximumPoolSize
- 使用无界队列 LinkedBlockingQueue(队列的容量为 Integer.MAX_VALUE)作为线程池的工作队列,新任务会一直放入队列中,导致maximumPoolSize和keepAliveTime无效,不会拒绝任务,任务多时容易导致OOM
- SingleThreadExecutor
- corePoolSize 和 maximumPoolSize设置为1
- 同FixedThreadPool使用无界队列
- CachedThreadPool
- corePoolSize 设置为0,maximumPoolSize设置为 Integer.MAX.VALUE
- 如果任务提交速度大于任务处理速度会不断创建新线程,容易导致cpu和内存资源耗尽
- ScheduledThreadPool
- 实际项目一般不使用,也不推荐使用,了解即可
- 用于再给定延时后执行任务,或定期执行任务
- 使用DelayQueue(实现优先级)存放任务,时间短的任务先执行,若时间相同则先提交的任务优先。
- 执行周期任务时从DelayQueue获取任务时间大于当前时间的任务并执行,然后将这个任务时间修改为下次要执行的时间,最后放回DelayQueue中
- ScheduledThreadPoolExecutor 和 Timer 的比较
- Timer基于单线程、系统时间实现。执行时出错将导致线程终止,所有任务都终止;如果任务执行时间过长会导致周期性不准确;修改系统时间会破坏周期性;
- ScheduledThreadPoolExecutor 基于多线程、JVM时间实现。任务之间不会互相影响,周期性有保证,更加精确。