JUC就是java.util.concurrent包,俗称java并发包
通过看JDK的API,我们发现JUC下有俩子包,分别是atomic和locks包,这篇文章重点就是看这两个包下的内容
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());
}
}
我们执行这个方法看下输出
我们看到当第一次期望值是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包下的内容
这个包下最重要的是 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来绑定多个条件,精确唤醒。