JUC并发包
Java并发包(java.util.concurrent包,简称J.U.C)的构成:
J.U.C核心由5大块组成:atomic包、locks包、collections包、tools包(AQS)、executor包(线程池)
一、Atomic包
Atomic包是java.util.concurrent下的另一个专门为线程安全设计的Java包,包含多个原子操作类。这个包里面提供了一组原子变量类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。
Atomic中标量类AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference这四种基本类型用来处理布尔,整数,长整数,对象四种数据,其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法,从而避免了synchronized的高开销,执行效率大为提升。
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值
(一)AtomicBoolean源码分析
AtomicBoolean 内部的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 设置为使用Unsafe.compareAndSwapInt进行更新 private static final Unsafe unsafe = Unsafe.getUnsafe(); //保存修改变量的实际内存地址,通过unsafe.objectFieldOffset读取 private static final long valueOffset; // 初始化的时候,执行静态代码块,计算出保存的value的内存地址便于直接进行内存操作 //objectFieldOffset(Final f):返回给定的非静态属性在它的类的存储分配中的位置(偏移地址)。 static { try { valueOffset = unsafe.objectFieldOffset (AtomicBoolean. class .getDeclaredField( "value" )); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; |
构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * Creates a new {@code AtomicBoolean} with the given initial value. * 通过给定的初始值,将boolean转为int后初始化value * @param initialValue the initial value */ public AtomicBoolean( boolean initialValue) { value = initialValue ? 1 : 0 ; } /** * Creates a new {@code AtomicBoolean} with initial value {@code false}. * 初始化为默认值,默认为false,因为int的默认值是0 */ public AtomicBoolean() {} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * Atomically sets to the given value and returns the previous value. * * @param newValue the new value * @return the previous value */ // 通过原子的方式设置给定的值,并返回之前的值 public final boolean getAndSet( boolean newValue) { boolean prev; do { //先get()到原来的值,再进行原子更新,会一直循环直到更新成功 prev = get(); } while (!compareAndSet(prev, newValue)); return prev; } |
compareAndSet
1 2 3 4 5 6 7 | public final boolean compareAndSet( boolean expect, boolean update) { int e = expect ? 1 : 0 ; int u = update ? 1 : 0 ; //unsafe.compareAndSwapInt:原子性地更新偏移地址为valueOffset的属性值为u,当且仅当偏移地址为alueOffset的属性的当前值为e才会更新成功,否则返回false。 return unsafe.compareAndSwapInt( this , valueOffset, e, u); } |
AtomicBoolean的使用举例
1 2 3 4 5 6 | //如果想让某种操作只执行一次,初始atomicBoolean为false AtomicBoolean atomicBoolean = new AtomicBoolean( false ); //如果当前值为false,设置当前值为true,如果设置成功,返回true if (atomicBoolean.compareAndSet( false , true )){ //执行操作 } |
(二)AtomicInteger源码分析
构造方法
1 2 3 4 | public AtomicBoolean( boolean initialValue){ value = initialValue ? 1 : 0 ; } public AtomicBoolean(){} |
一个有参,一个无参,无参时成员变量value值为0,也就是false.
set()和lazySet()
1 2 3 | public final void set( int newValue) { value = newValue; } |
lazySet()方法实现了对value的非volatile赋值,通过调用unsafe.putOrderedInt()方法,直接向固定偏移量的内存上写入数据,但不使其对其他线程立刻可见(putOrderedInt()是putIntVolatile()的延迟实现)。
1 2 3 | public final void lazySet( int newValue) { unsafe.putOrderedInt( this , valueOffset, newValue); } |
getAndSet()方法实际上调用getAndSetInt()方法,它的底层实现逻辑是利用getIntVolatile()方法获取value后进行的自旋CAS操作。
getAndIncrement(),getAndDecrement()和getAndAdd()
这两个方法实现的原理和getAndSet()方法基本是一样的,只是将get出来的value加1或减1了而已。
(三)AtomicStampedReference & AtomicMarkableReference
防止ABA问题,分别靠时间戳和Mark标记位来方式ABA问题的发生。
AtomicMarkableReference和 AtomicStampedReference源码几乎相同,唯一区别就在于一个是int型的时间戳,而这个类则是布尔型的标记值。
两者区别在于AtomicStampedReference可以知道修改了多少次,而AtomicMarkableReference则只知道有没有被修改过
cas的ABA问题就是 假设初始值为A,线程3和线程1都获取到了初始值A,然后线程1将A改为了B,线程2将B又改回了A,这时候线程3做修改时,是感知不到这个值从A改为了B又改回了A的过程:
AtomicStampedReference 本质是有一个int 值作为版本号,每次更改前先取到这个int值的版本号,等到修改的时候,比较当前版本号与当前线程持有的版本号是否一致,如果一致,则进行修改,并将版本号+1(当然加多少或减多少都是可以自己定义的),在zookeeper中保持数据的一致性也是用的这种方式;
AtomicMarkableReference则是将一个boolean值作是否有更改的标记,本质就是它的版本号只有两个,true和false,修改的时候在这两个版本号之间来回切换,这样做并不能解决ABA的问题,只是会降低ABA问题发生的几率而已;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 它里面只有一个成员变量,要做原子更新的对象会被封装为Pair对象,并赋值给pair; private volatile Pair<V> pair; 先看它的一个内部类Pair ,要进行原子操作的对象会被封装为Pair对象 private static class Pair<T> { final T reference; //要进行原子操作的对象 final int stamp; //当前的版本号 private Pair(T reference, int stamp) { this .reference = reference; this .stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { //该静态方法会在AtomicStampedReference的构造方法中被调用,返回一个Pair对象; return new Pair<T>(reference, stamp); } } 现在再看构造方法就明白了,就是将原子操作的对象封装为pair对象 public AtomicStampedReference(V initialRef, int initialStamp) { pair = Pair.of(initialRef, initialStamp); } 获取版本号 就是返回成员变量pair的stamp的值 public int getStamp() { return pair.stamp; } 原子修改操作,四个参数分别是旧的对象,将要修改的新的对象,原始的版本号,新的版本号 这个操作如果成功就会将expectedReference修改为newReference,将版本号expectedStamp修改为newStamp; public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; |
二、LinkedBlockingQueue
java.util.concurrent.LinkedBlockingQueue
是一个基于单向链表的、范围任意的(其实是有界的)、FIFO 阻塞队列。访问与移除操作是在队头进行,添加操作是在队尾进行,并分别使用不同的锁进行保护,只有在可能涉及多个节点的操作才同时对两个锁进行加锁。
队列是否为空、是否已满仍然是通过元素数量的计数器(count)进行判断的,由于可以同时在队头、队尾并发地进行访问、添加操作,所以这个计数器必须是线程安全的,这里使用了一个原子类 AtomicInteger
,这就决定了它的容量范围是: 1 – Integer.MAX_VALUE。
由于同时使用了两把锁,在需要同时使用两把锁时,加锁顺序与释放顺序是非常重要的:必须以固定的顺序进行加锁,再以与加锁顺序的相反的顺序释放锁。
1.offer操作
向队列尾部插入一个元素,如果队列有空闲容量则插入成功后返回true,如果队列已满则丢弃当前元素然后返回false,如果 e元素为null,则抛出空指针异常(NullPointerException ),还有一点就是,该方法是非阻塞的put 操作
向队列尾部插入一个元素,如果队列有空闲则插入后直接返回true,如果队列已经满则阻塞当前线程知道队列有空闲插入成功后返回true,如果在阻塞的时候被其他线程设置了中断标志
take 操作
获取当前队列头部元素并从队列里面移除,如果队列为空则阻塞调用线程。如果队列为空则阻塞当前线程知道队列不为空,然后返回元素,如果在阻塞的时候被其他线程设置了中断标志,则被阻塞线程会抛出InterruptedException 异常而返回。
peek 操作
获取队列头部元素但是不从队列里面移除,如果队列为空则返回 null,该方法是不阻塞的
poll操作
从队列头部获取并移除一个元素,如果队列为空则返回 null,该方法是不阻塞的。
remove 操作
移除指定元素。由于移除元素涉及该结点前后两个结点的访问与修改,
对两把锁加锁简化了同步管理。
三、LinkedBlockingDueue
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
相比于其他阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。
LinkedBlockingDeque是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE。
该类继承自AbstractQueue抽象类,又实现了BlockingDeque接口
LinkedBlockingDeque类对元素的操作方法比较多,我们下面以putFirst、putLast、pollFirst、pollLast方法来对元素的入队、出队操作进行分析。
入队
putFirst(E e)方法是将指定的元素插入双端队列的开头
入队操作是通过linkFirst(E e)方法来完成的
若入队成功,则linkFirst(E e)方法返回true,否则,返回false。若该方法返回false,则当前线程会阻塞在notFull条件上。
putLast(E e)方法是将指定的元素插入到双端队列的末尾
该方法和putFirst(E e)方法几乎一样,不同点在于,putLast(E e)方法通过调用linkLast(E e)方法来插入节点
若入队成功,则linkLast(E e)方法返回true,否则,返回false。若该方法返回false,则当前线程会阻塞在notFull条件上。
出队
pollFirst()方法是获取并移除此双端队列的首节点,若不存在,则返回null,移除首节点的操作是通过unlinkFirst()方法来完成的
pollLast()方法是获取并移除此双端队列的尾节点,若不存在,则返回null
移除尾节点的操作是通过unlinkLast()方法来完成的
其实LinkedBlockingDeque类的入队、出队操作都是通过linkFirst、linkLast、unlinkFirst、unlinkLast这几个方法来实现的
四、DelayQueue
DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到期时间最长。注意:不能将null元素放置到这种队列中。
1 2 3 4 5 6 | 主要构造方法 public DelayQueue() {} public DelayQueue(Collection<? extends E> c) { this .addAll(c); } |
入队
因为DelayQueue是阻塞队列,且优先级队列是无界的,所以入队不会阻塞不会超时,因此它的四个入队方法是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public boolean add(E e) { return offer(e); } public void put(E e) { offer(e); } public boolean offer(E e, long timeout, TimeUnit unit) { return offer(e); } public boolean offer(E e) { final ReentrantLock lock = this .lock; lock.lock(); try { q.offer(e); if (q.peek() == e) { leader = null ; available.signal(); } return true ; } finally { lock.unlock(); } } |
入队方法比较简单:
(1)加锁;
(2)添加元素到优先级队列中;
(3)如果添加的元素是堆顶元素,就把leader置为空,并唤醒等待在条件available上的线程;
(4)解锁;
出队
因为DelayQueue是阻塞队列,所以它的出队有四个不同的方法,有抛出异常的,有阻塞的,有不阻塞的,有超时的。
我们这里主要分析两个,poll()和take()方法。
poll()方法比较简单:
(1)加锁;
(2)检查第一个元素,如果为空或者还没到期,就返回null;
(3)如果第一个元素到期了就调用优先级队列的poll()弹出第一个元素;
(4)解锁。
take()方法稍微要复杂一些:
(1)加锁;
(2)判断堆顶元素是否为空,为空的话直接阻塞等待;
(3)判断堆顶元素是否到期,到期了直接调用优先级队列的poll()弹出元素;
(4)没到期,再判断前面是否有其它线程在等待,有则直接等待;
(5)前面没有其它线程在等待,则把自己当作第一个线程等待delay时间后唤醒,再尝试获取元素;
(6)获取到元素之后再唤醒下一个等待的线程;
(7)解锁;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义