一、java原子类并发原理
文章目录
- 一、java原子类并发原理
- 1.1 AtomicLong原理解析
- 1.2 LongAdder原理解析
- 1.3 LongAccumulator原理浅析
- 二、java并发包中锁原理(AQS)
- 2.1 LockSupport工具类
- 2.2 抽象同步队列AQS
- 2.2.1 AQS——锁的底层支持
- 2.2.2 AQS——条件变量的支持
- 三、线程池
- 3.1 ThreadPoolExecutor
- 3.2 ScheduledThreadPoolExecutor
JUC提供了一系列原子性操作类,这些类都是使用非阻塞算法CAS实现的,比使用锁实现原子性操作在性能上有很大提升
1.1 AtomicLong原理解析
public class AtomicLong extends Number implements java.io.Serializable {
private static final long serialVersionUID = 1927816293512124184L;
//(1)获取Unsafe实例(AtomicLong也是rt.jar包下的,可以直接获取Unsafe实例)
private static final Unsafe unsafe = Unsafe.getUnsafe();
//(2)存放变量value的偏移量
private static final long valueOffset;
//(3)判断JVM是否支持Long类型无锁CAS
static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
private static native boolean VMSupportsCS8();
static {
try {
//(4)获取value在AtomicLong中的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//(5)实际变量value(声明为volatile,多线程保证内存可见性,value是具体存放计数的的变量)
private volatile long value;
public AtomicLong(long initialValue) {
value = initialValue;
}
.....
1.递增和递减操作代码:
//(6)调用unsafe方法,原子性设置value值为原始值+1,返回值为递增后的值
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
//(7)调用unsafe方法,原子性设置value值为原始值-1,返回值为递减后的值
public final long decrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
}
//(8)调用unsafe方法,原子性设置value值为原始值+1,返回值为原始值
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
//(9)调用unsafe方法,原子性设置value值为原始值-1,返回值为原始值
public final long getAndDecrement() {
return unsafe.getAndAddLong(this, valueOffset, -1L);
}
以上代码都是通过getAndAddLong方法来实现的,这个函数是原子性操作,第一个参数是AtomicLong实例的引用,第二个参数是value变量在AtomicLong中的偏移量,第三个参数是要设置的第二个变量的值
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
//获取对象var1偏移量为var2的变量对应volatile语句的值
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
2. boolean compareAndSet(long expect, long update)方法
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
内部还是调用了unsafe类中的compareAndSwapLong方法。如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect。这是处理器提供的一个原子性命令。
3.一个统计0个数的例子:
public class AtomicLongTest {
//(10)创建Long原子计数器
private static AtomicLong atomicLong = new AtomicLong();
//(11)创建数据源
private static Integer[] arrayone = new Integer[]{0,1,2,3,0,5,6,56,0};
private static Integer[] arraytwo = new Integer[]{10,1,2,3,0,5,6,56,0};
public static void main(String[] args) throws InterruptedException{
//(12)线程one统计数组arrayone中0的个数
Thread threadone = new Thread(new Runnable() {
@Override
public void run() {
int size = arrayone.length;
for(int i=0; i<size; i++){
if(arrayone[i].intValue() == 0){
atomicLong.incrementAndGet();
}
}
}
});
//(13)线程two统计数组arraytwo中0的个数
Thread threadtwo = new Thread(new Runnable() {
@Override
public void run() {
int size = arraytwo.length;
for(int i=0; i<size; i++){
if(arraytwo[i].intValue() == 0){
atomicLong.incrementAndGet();
}
}
}
});
//(14)启动子线程
threadone.start();
threadtwo.start();
//(15)等待线程执行完毕
threadone.join();
threadtwo.join();
System.out.println("count 0: "+atomicLong.get());
}
}
执行结果:
原子类都使用CAS非阻塞算法,性能比加锁好。但是高并发情况下AtomicLong还会存在性能问题。下面介绍一个在高并发性能更好地LongAdder类。
1.2 LongAdder原理解析
使用AtomicLong在高并发下大量线程会同时去竞争更新同一个原子变量,但是只有一个会成功,其他的线程会无限自旋尝试CAS的操作,浪费了CPU资源。LongAdder类把一个变量分解成多个变量,让同样多的线程去竞争多个资源。
LongAdder内部维护了一个cell数组,数组每个元素都是一个初始值为0的long型变量,当多个线程争夺同一个cell原子变量失败了,会尝试去其他cell的变量上进行CAS尝试。最后,在LongAdder当前值时,是把所有cell变量的value值累加后再加上base返回的。
- LongAdder内部维护了一个延迟初始化的原子性更新数组cell和一个基值变量base。cell刚开始不创建,在需要时创建,也就是惰性加载。
- 一开始cell为null并且并发少的时候,所有的累加操作都是对base变量进行的。cell是加了@sun.misc.Contended注解来解决伪共享
代码分析:
Striped64这个类里面有三个变量,base默认值为。cellBusy用来实现自旋锁,状态值只有0和1。下面介绍主要方法,其他的看源码就行了,比较简单。
1.cell的构造:
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
//下面这段代码是为了获取到valueOffset
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
构造很简单,维护了一个被声明为volatile的变量,因为没有锁,用volatile保证可见性。cas方法保证线程更新的原子性。
2.add(long x)方法:
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {//(1)
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||//(2)
(a = as[getProbe() & m]) == null ||//(3)
!(uncontended = a.cas(v = a.value, v + x)))//(4)
longAccumulate(x, null, uncontended);//(5)
}
}
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
代码(1)首先看cells是否为null,如果是null则当前在基础变量base上进行累加。如果cells不为null或者代码(1)的CAS操作失败则去执行代码(2),代码(2)(3)决定当前线程应该去访问cells数组里面的哪一个cells元素,如果映射存在执行代码(4),(4)使用cas更新分配的cell元素的value值,如果当前映射不存在或者存在但是cas更新失败就去执行代码(5)。其中getProbe() & m中是cells.length-1,getProbe() 是获取对当前线程中的ThreadLocalRandomProbe的值,一开始为0,在代码(5)里面进行初始化。
3.longAccumulate(x, null, uncontended)方法:
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
//(6)初始化当前线程的变量ThreadLocalRandomProbe的值
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) { // 后面的代码都在这个循环里面
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {//(7)
if ((a = as[(n - 1) & h]) == null) {//(8)
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}//if语句(8)结束
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//当前Cell存在,则执行CAS操作(9)
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
//当前Cell元素大于CPU个数(10)
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
//是否有冲突(11)
else if (!collide)
collide = true;
//如果当前元素元素个数没有达到CPU个数并且有冲突则扩容(12)
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
//为了能够找到一个空闲的cell,重新计算hash值,xorshift算法生成随机数
h = advanceProbe(h);
}//最上面的第一个if语句(7)结束
//初始化Cell数组(14)
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
//14.1
Cell[] rs = new Cell[2];
//14.2
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
//14.3
cellsBusy = 0;
}
if (init)
break;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}//循环结束
}
分析:
- 先分析代码(14)执行初始化操作,cellsBusy是一个标志,为0这说明当前cells数组并没有被初始化或者扩容,也没有在新建Cell元素,为1这说明cells数组在被初始化或者扩容,或者当前在创建新的cell元素、通过CAS操作来进行0或1状态的切换。(14.1)初始化数组大小为2,用h&1计算当前线程应该访问cell数组的哪个位置,然后标记数组已经被初始化,最后代码(14.3)重置了cellsBusy标记。这里没有使用CAS操作但是线程是安全的,原因是cellsBusy是volatile类型的,保证内存可见性,其他地方也没有机会修改cellsBusy的值。
- 数组扩容是在代码(12)中进行的,扩容条件是代码(10)(11)的条件都不满足时(即当前cells的元素个数小于当前CPU个数并且当前多个线程访问了cells中同一个元素,导致CAS失败时才会进行扩容操作)。代码(12)中扩容操作也是先通过设置cellsBusy为1,然后进行扩容。(12.1)将容量扩充为之前的2倍,并赋值cell元素到扩容后数组。
- 代码(7)(8)中,当前线程调用add方法并根据当前线程的随机数ThreadLocalRandomProbe和cells元素个数计算要访问的cell下标,如果发现对应元素的值为null,则新增一个cell元素到cells数组,并且在将其添加到cells数组之前要竞争设置cellsBusy为1
1.3 LongAccumulator原理浅析
LongAdder类是LongAccumulate的一个特例,LongAccumulator比LongAdder功能更强大。LongAccumulate可以为累加器提供非0的初始值,LongAdder只能提供默认的0值。另外,LongAccumulate还可以指定累加规则,比如不进行累加而进行累乘,只需要在构造LongAccumulate时传入自定义的双目运行算器即可,LongAdder则是内置累加的规则。
二、java并发包中锁原理(AQS)
2.1 LockSupport工具类
JDK中的rt.jar包里面的LockSupport是个工具类,主要作用是挂起和唤醒线程,是创建锁和其他同步类的基础。LockSupport是使用Unsafe类实现的
- void park()方法:如果调用了prak方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则阻塞。在其他线程调用unpark(Thread thread)方法并且当前线程作为参数时,调用park()方法而被阻塞的线程会返回。如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒,则阻塞线程也会返回。
注意: 因调用了park()方法被阻塞的线程被其他按现场中断而但会时并不会抛出InterruptedException异常 - void unpark(Thread threa)方法:当一个线程调用unpark时,如果参数thread没有持有thread与LockSupport类关联的许可证,则让thread持有。thread之前因调用park()而被挂起,则调用unpark后,该线程会被唤醒。如果thread之前没有调用park,则调用unpark方法后,再次调用park方法,其会立刻返回
- void park(Object blocker) 设置一个blocker对象(一般是传this),主要用来给监视工具和诊断工具确定线程受阻塞的原因。
park和wait的不同 park、unpark方法和wait、notify()方法有一些相似的地方。都是休眠,然后唤醒。但是wait、notify方法有一个不好的地方,就是我们在编程的时候必须能保证wait方法比notify方法先执行。如果notify方法比wait方法晚执行的话,就会导致因wait方法进入休眠的线程接收不到唤醒通知的问题。而park、unpark则不会有这个问题,我们可以先调用unpark方法释放一个许可证,这样后面线程调用park方法时,发现已经许可证了,就可以直接获取许可证而不用进入休眠状态了。
2.2 抽象同步队列AQS
2.2.1 AQS——锁的底层支持
AbstractQueuedSynchronizer抽象同步队列简称AQS,他是实现同步器的基础组件,并发包锁的底层就是使用AQS实现的
AQS是一个FIFO的双向队列,通过节点head和tail记录队列的队尾元素,队列元素的类型为Node。Node中的thread变量用来存放进入AQS队列里面的线程;
- Node节点内部的SHARED标记线程是获取共享资源时被阻塞挂起后放入AQS队列的,EXCLUSIVE(获取独占资源时)。
- waitStatus记录当前线程等待状态,CANCELLED(线程被取消了),SIGNAL(线程被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGATE(释放共享资源时需要通知其他节点);prev前驱结点,next后继节点
AQS内部维持了一个单一的状态信息state,可以通过getState、setState、compareAndSetState函数修改其值。
- ReentrantLock,state可以用来表示当前线程获取锁的可重入次数;
- ReentrantReadWriteLock,state的高16位表示读状态,也就是获取读锁的次数,低16位表示获取到写锁的可重入次数
- semaphore,state用来表示当前可用信号的个数
- CountDownlatch,state用来表示计数器当前的值
AQS内部类ConditionObject,用来结合锁实现线程同步。
- ConditionObject可以直接访问AQS对象内部的变量,比如state状态值和AQS队列。
- ConditionObject是条件变量,每个条件变量对应一个条件队列(单向链表队列),用来存放调用条件变量的await方法后被阻塞的线程
对AQS来说,线程同步的关键是对state进行操作。根据state是否属于一个线程,操作state的方式分为独占方式和共享方式:
- 独占方式:与具体线程绑定的,就是如果一个线程获取到了资源,就会标记这个线程获取到了(reentrantLock设置当前锁的持有者为当前线程),其他线程再次尝试操作state获取资源就会发现该资源不是自己获取的,就会被阻塞。
- 共享方式:与具体线程不相关的,一个线程获取资源后,另外一个线程再次获取时如果当前资源还能满足需求,则当前线程只需要使用CAS方式获取即可。
独占方式下,获取与释放资源:
- 当一个线程调用acquire(int arg)方法获取独占资源时,会首先使用tryAcquire方法尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node.EXCLUSIVE的Node节点后插入到AQS阻塞队列的尾部,并调用LockSupport.park(this)方法挂起自己
public final void acquire(int arg) {
//tryAcquire(arg)方法需要子类自己实现
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2.当一个线程调用release(int arg)方法时会尝试使用tryRelease方法尝试释放资源,这里是设置状态变量state的值,然后调用LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程。被激活的会尝试上面的tryAcquire。
public final boolean release(int arg) {
//tryRelease(arg)方法需要子类自己实现
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//tryRelease成功后,解锁头结点的下一个为被取消的后继节点
return true;
}
return false;
}
共享方式下,获取与释放资源:
- 当一个线程调用acquireShared(int arg)方法获取共享资源时,会首先使用tryAcquireShared方法尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node.SHARED的Node节点后插入到AQS阻塞队列的尾部,并调用LockSupport.park(this)方法挂起自己
public final void acquireShared(int arg) {
//tryAcquireShared(arg)方法需要子类自己实现
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
- 当一个线程调用releaseShared(int arg)方法时会尝试使用tryRelease方法尝试释放资源,这里是设置状态变量state的值,然后调用LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程。被激活的会尝试上面的tryAcquireShared。
public final boolean releaseShared(int arg) {
//tryReleaseShared(arg)方法需要子类自己实现
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
子类还要重写isHeldExclusively方法,来判断锁是被当前线程独享还是共享。其中reentrantLock中是这样写的
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
AQS入队操作:
enq(final Node node)这个方法该节点插入到AQS的阻塞队列
private Node enq(final Node node) {
for (;;) {
Node t = tail;//(1)
if (t == null) { // Must initialize 设置哨兵节点
if (compareAndSetHead(new Node()))//(2)
tail = head;
} else {
node.prev = t;//(3)
if (compareAndSetTail(t, node)) {//(4)
t.next = node;
return t;
}
}
}
}
- 第一次循环中,状态为下图(default)。head = tail = null,执行代码(1)后状态为(Ⅰ),t==null所以执行代码(2),用CAS设置哨兵节点,状态为(Ⅱ)
- 哨兵节点建立好了,需要插入node节点,第二次循环执行代码(1),状态为(Ⅲ),然后执行代码(3)设置node的前驱结点为尾部节点,状态为(Ⅳ)。然后通过CAS设置node节点为尾部节点,CAS成功后状态为(Ⅴ),CAS成功后再设置原来的尾部节点的后继节点为node,完成后状态为(Ⅵ)
2.2.2 AQS——条件变量的支持
notify和wait,是配合synchronized内置锁实现线程间同步。条件变量的signal和await方法也是用来配合锁(使用AQS实现的锁)实现线程同步的基础设施。
- synchronized同时只能与一个共享变量的notify或wait实现线程同步,而AQS的一个锁可以对应多个条件变量
- 在调用共享变量的notify和wait方法前必须先获取该共享变量的内置锁,同理,在调用条件变量的signal和await方法前也必须先获取条件变量对应的锁。
ConditionObject是AQS的内部类,可以访问AQS内部变量和方法。每个条件变量对应一个条件队列(单向链表队列),用来存放调用条件变量的await方法后被阻塞的线程。注意这个条件队列和AQS队列不是一回事
1.await()方法: 这个方法的部分代码
public final boolean await(long time, TimeUnit unit)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//创建新的node节点,并插入到条件队列末尾
Node node = addConditionWaiter();
//释放当前线程获取的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
//调用park方法阻塞挂起当前线程
while (!isOnSyncQueue(node)) {
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
...
}
}
当前线程调用条件变量的await()方法时(必须先调用锁的lock()方法获取锁),在内部会构造一个类型为Node.CONDITON的node节点,然后将该节点插入条件队列末尾,之后当前线程会释放获取的锁(也就是会操作锁对应的state变量的值),并被阻塞挂起。
2.signal() 方法:
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
//将条件队列头元素移动到AQS队列
doSignal(first);
}
当另一个线程调用条件变量的signal方法时(必须先获取锁),会把条件队列里面队头的一个线程节点从条件队列里面移除并放入AQS的阻塞队列里面,然后激活这个线程。
小结:
- 当多个线程同时获取锁时,只有一个线程获取到了嗦,其他线程会被转换为Node节点插入到锁对应的AQS阻塞队列里面,并做自旋CAS尝试获取锁
- 如果获取到锁的线程又调用了对应的条件变量的await()方法,则该线程会释放获取到的锁,并被转换为Node节点插入到条件变量对应的条件队列里面。
- 当另一个线程调用了signal()或signalAll()方法时,会把条件队列里面的一个或者全部Node节点移动到AQS的阻塞队列里面,等待时机获取锁。
一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。