JUC就是java.util.concurrent包,俗称java并发包

通过看JDK的API,我们发现JUC下有俩子包,分别是atomic和locks包,这篇文章重点就是看这两个包下的内容

java 第三方并发工具包 java并发包常用类juc_java

Atomic 原子类

atomic,翻译过来就是原子的意思,也就是这个包下的所有类,都是原子性的,所谓原子性,就算不可再分

CAS

CAS(Compare And Swap) 比较并交换,是一个很重要的同步的思想,简单来说就是如果主内存中所保存的值是和期望的值一样的话,那么就对它进行修改否则就一直重试,直到一直为止。

写一个示例:

public class Atomic {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(4);
        System.out.println(atomicInteger.compareAndSet(3,2021) + ":: value = "+ atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(4,2022) + ":: value = "+ atomicInteger.get());

    }
}

我们执行这个方法看下输出

java 第三方并发工具包 java并发包常用类juc_多线程_02


我们看到当第一次期望值是3的时候,并没有做出修改,value还是等于4,但是第二次,将期望值改为4,则value修改成了 2022,这是为什么?? 其实可以看出 如果期望值等于当前的值,则会对其修改,不一样的话,则不会。

接下来我们打个断点 调试下,看看它具体走的流程,然后对其进行分析

然后找到了 unsafe这个类下面的compareAndSwapInt,这个方法。。。。是由java的native访问的,并不是java的代码,,调试不过去了。

/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

以原子方式将值设置为给定的更新值,如果当前值 == 预期值。
如果是的话,则返回true,并修改,否则返回false

这也是CAS的思想,比较并交换。用于保证并发时的无锁并发的安全性。

CAS底层

AtomicInteger类中有两个重要的属性

private volatile int value;
private static final Unsafe unsafe = Unsafe.getUnsafe();

unsafe就算在上面说的用native实现的方法
看value上有一个修饰符 volatile,这个下一篇文章就说下这个

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

上面这个方法,简单来说它的意思就是:

var1和var2,就是根据对象和偏移量得到在主内存的快照值var5。
然后compareAndSwapInt方法通过var1和var2得到当前主内存的实际值。如果这个实际值跟快照值相等,那么就更新主内存的值为var5+var4。如果不等,那么就一直循环,一直获取快照,一直对比,直到实际值和快照值相等为止。

CAS的缺点

CAS实际上是一种自旋锁
自旋锁:
就是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取。自己在那儿一直循环获取,就像“自旋”一样。这样的好处是减少线程切换的上下文开销,缺点是会消耗CPU。

一直循环,开销比较大。(自旋锁的缺点)
只能保证一个变量的原子操作,多个变量依然要加锁。
有ABA的问题

ABA的问题

问题

CAS中比较并交换是有时间差异的,在这个时间差异中可能会发生意想不到的事情,例如 将值改变后在改回来,另一个线程只确认首位是否一致,所以依然操作成功
简单来讲就是线程T中值为A,然后将A改成了B,然后又将B改成A,在线程T1中看到的值仍是A,但是它并不知道,线程T中的值发生了什么改变,它只看最终的那个结果

解决

  • AtomicInteger对整数进行原子操作
AtomicStampedReference和ABA问题的解决

使用AtomicStampedReference类可以解决ABA问题。这个类维护了一个“版本号”Stamp,在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才执行更新操作。

AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);

如果是一个类?可以用AtomicReference 来对其进行包装

AtomicReference<XXX> atomicReference = new AtomicReference<>();
atomicReference.set(XXX1);
System.out.println(atomicReference.compareAndSet(XXX1,XXX2)); // true
System.out.println(atomicReference.compareAndSet(XXX1,XXX2)); //false

Locks

接下来在来看看locks包下的内容

java 第三方并发工具包 java并发包常用类juc_面试_03

这个包下最重要的是 AbstractQueuedSynchronizer(AQS)

抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。

如果请求的共享资源是空闲的,那么会将当前请求资源的这个线程设置为有效的工作线程,并把共享资源设置为锁定的状态,如果被请求的共享资源被占用,就需要阻塞等待以及获得锁分配的机制,这个机制AQS是用CLH来实现的,将暂时没有拿到锁的线程加入到队列等待

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。

AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。

AQS是一个自旋锁,实现AQS的锁有:

  • 自旋锁
  • 互斥锁
  • 读写锁

lock包下的锁

  • ReentrantLock(重入锁)
  • ReentrantReadWriteLock(读写锁)
  • StampedLock (1.8新出的读写锁)

我们通过看 AQS的源代码,找到一个重要的内部类Node

static final class Node {

        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;

        static final int PROPAGATE = -3;

        volatile int waitStatus;

        volatile Node prev;

        volatile Node next;

        volatile Thread thread;


        Node nextWaiter;

		............
    }

EXCLUSIVE,SHARED两种模式,即共享和独占,而我们的锁Lock和Synchronized都是属于独占锁

这个类里面有两个核心的方法,一个是tryAcquire,一个是tryRelease,我们发现这两个
方法是用protected修饰的,再看看ReentranLock的源码,我们会发现其实重入锁是实现了tryAcquire和tryRelease实现的

Synchronized和Lock的区别

synchronized关键字和java.util.concurrent.locks.Lock都能加锁,两者有什么区别呢?

  • 原始构成:sync是JVM层面的,底层通过monitorenter和monitorexit来实现的。Lock是JDK API层面的。(sync一个enter会有两个exit,一个是正常退出,一个是异常退出)
  • 使用方法:sync不需要手动释放锁,而Lock需要手动释放。
  • 是否可中断:sync不可中断,除非抛出异常或者正常运行完成。Lock是可中断的,通过调用interrupt()方法。
  • 是否为公平锁:sync只能是非公平锁,而Lock既能是公平锁,又能是非公平锁。
  • 绑定多个条件:sync不能,只能随机唤醒。而Lock可以通过Condition来绑定多个条件,精确唤醒。