一、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());
    }
}

执行结果:

java测试多个线程并发_并发编程


原子类都使用CAS非阻塞算法,性能比加锁好。但是高并发情况下AtomicLong还会存在性能问题。下面介绍一个在高并发性能更好地LongAdder类。

1.2 LongAdder原理解析

使用AtomicLong在高并发下大量线程会同时去竞争更新同一个原子变量,但是只有一个会成功,其他的线程会无限自旋尝试CAS的操作,浪费了CPU资源。LongAdder类把一个变量分解成多个变量,让同样多的线程去竞争多个资源。

java测试多个线程并发_java测试多个线程并发_02

LongAdder内部维护了一个cell数组,数组每个元素都是一个初始值为0的long型变量,当多个线程争夺同一个cell原子变量失败了,会尝试去其他cell的变量上进行CAS尝试。最后,在LongAdder当前值时,是把所有cell变量的value值累加后再加上base返回的。

  1. LongAdder内部维护了一个延迟初始化的原子性更新数组cell和一个基值变量base。cell刚开始不创建,在需要时创建,也就是惰性加载。
  2. 一开始cell为null并且并发少的时候,所有的累加操作都是对base变量进行的。cell是加了@sun.misc.Contended注解来解决伪共享

代码分析:

java测试多个线程并发_并发编程_03


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
        }//循环结束
    }

分析:

  1. 先分析代码(14)执行初始化操作,cellsBusy是一个标志,为0这说明当前cells数组并没有被初始化或者扩容,也没有在新建Cell元素,为1这说明cells数组在被初始化或者扩容,或者当前在创建新的cell元素、通过CAS操作来进行0或1状态的切换。(14.1)初始化数组大小为2,用h&1计算当前线程应该访问cell数组的哪个位置,然后标记数组已经被初始化,最后代码(14.3)重置了cellsBusy标记。这里没有使用CAS操作但是线程是安全的,原因是cellsBusy是volatile类型的,保证内存可见性,其他地方也没有机会修改cellsBusy的值。
  2. 数组扩容是在代码(12)中进行的,扩容条件是代码(10)(11)的条件都不满足时(即当前cells的元素个数小于当前CPU个数并且当前多个线程访问了cells中同一个元素,导致CAS失败时才会进行扩容操作)。代码(12)中扩容操作也是先通过设置cellsBusy为1,然后进行扩容。(12.1)将容量扩充为之前的2倍,并赋值cell元素到扩容后数组。
  3. 代码(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类实现的

  1. void park()方法:如果调用了prak方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则阻塞。在其他线程调用unpark(Thread thread)方法并且当前线程作为参数时,调用park()方法而被阻塞的线程会返回。如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒,则阻塞线程也会返回。
    注意: 因调用了park()方法被阻塞的线程被其他按现场中断而但会时并不会抛出InterruptedException异常
  2. void unpark(Thread threa)方法:当一个线程调用unpark时,如果参数thread没有持有thread与LockSupport类关联的许可证,则让thread持有。thread之前因调用park()而被挂起,则调用unpark后,该线程会被唤醒。如果thread之前没有调用park,则调用unpark方法后,再次调用park方法,其会立刻返回
  3. 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实现的

java测试多个线程并发_并发编程_04


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方式获取即可。

独占方式下,获取与释放资源:

  1. 当一个线程调用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;
    }

共享方式下,获取与释放资源:

  1. 当一个线程调用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);
    }
  1. 当一个线程调用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;
                }
            }
        }
    }
  1. 第一次循环中,状态为下图(default)。head = tail = null,执行代码(1)后状态为(Ⅰ),t==null所以执行代码(2),用CAS设置哨兵节点,状态为(Ⅱ)
  2. 哨兵节点建立好了,需要插入node节点,第二次循环执行代码(1),状态为(Ⅲ),然后执行代码(3)设置node的前驱结点为尾部节点,状态为(Ⅳ)。然后通过CAS设置node节点为尾部节点,CAS成功后状态为(Ⅴ),CAS成功后再设置原来的尾部节点的后继节点为node,完成后状态为(Ⅵ)
2.2.2 AQS——条件变量的支持

notify和wait,是配合synchronized内置锁实现线程间同步。条件变量的signal和await方法也是用来配合锁(使用AQS实现的锁)实现线程同步的基础设施。

  1. synchronized同时只能与一个共享变量的notify或wait实现线程同步,而AQS的一个锁可以对应多个条件变量
  2. 在调用共享变量的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的阻塞队列里面,然后激活这个线程。

小结:

  1. 当多个线程同时获取锁时,只有一个线程获取到了嗦,其他线程会被转换为Node节点插入到锁对应的AQS阻塞队列里面,并做自旋CAS尝试获取锁
  2. 如果获取到锁的线程又调用了对应的条件变量的await()方法,则该线程会释放获取到的锁,并被转换为Node节点插入到条件变量对应的条件队列里面。
  3. 当另一个线程调用了signal()或signalAll()方法时,会把条件队列里面的一个或者全部Node节点移动到AQS的阻塞队列里面,等待时机获取锁。

一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。

java测试多个线程并发_java_05

三、线程池

3.1 ThreadPoolExecutor
3.2 ScheduledThreadPoolExecutor