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);
}

  

想要获取value的值就需要使用get()方法,AtomicInteger除了提供基本的get()方法之外,还提供了getAndSet(),getAndIncrement(),getAndDecrement(),getAndAdd(),getAndUpdate()和getAndAccumulate()等方法。

getAndSet()方法实际上调用getAndSetInt()方法,它的底层实现逻辑是利用getIntVolatile()方法获取value后进行的自旋CAS操作。

getAndIncrement(),getAndDecrement()和getAndAdd()

这两个方法实现的原理和getAndSet()方法基本是一样的,只是将get出来的value加1或减1了而已。

getAndUpdate()和getAndAccumulate()分别以函数式编程接口IntUnaryOperator和IntBinaryOperator为入参,实现AtomicInteger的自定义变换。用户可以自定义一个函数,让value按函数计算结果递增,也可以定义两个可以互相翻转的int,使value交替变换。

(三)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)解锁;

posted @   zq231  阅读(162)  评论(0编辑  收藏  举报
编辑推荐:
· .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 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示