【Java并发】- StampedLock实现浅析

StampedLock实现概述

StampedLock与之前的ReentrantLock,ReentrantReadWriteLock使用队列同步列AQS实现有所不同,StampedLock的state改为了一个long型的变量,同时状态的设计也有所不同。同时由于没有使用AQSStampedLock直接在内部实现了同步等待队列,并且节点属性中有一个叫做cowait的分支用于标识另一个等待获取读状态的链。

StampedLock的读写状态设计

StampedLock采用了一个long型作为state,把这个64位的state的前7位作为读状态,第8位标识写状态,这也是为什么不支持重入的原因吧。
一些属性的定义:

//获取CPU的可用线程数量,用于确定自旋的时候循环次数
private static final int NCPU = Runtime.getRuntime().availableProcessors();

//根据NCPU确定自旋的次数限制(并不是一定这么多次,因为实际代码中是随机的)
private static final int SPINS = (NCPU > 1) ? 1 << 6 : 0;
//头节点上的自旋次数
private static final int HEAD_SPINS = (NCPU > 1) ? 1 << 10 : 0;

//头节点上的最大自旋次数
private static final int MAX_HEAD_SPINS = (NCPU > 1) ? 1 << 16 : 0;

private static final int LG_READERS = 7;

//一个读状态单位
private static final long RUNIT = 1L;
//写状态标识
private static final long WBIT  = 1L << LG_READERS;
//读状态标识(前7位)
private static final long RBITS = WBIT - 1L;
//最大的读状态
private static final long RFULL = RBITS - 1L;
//用于获取读写状态
private static final long ABITS = RBITS | WBIT;
private static final long SBITS = ~RBITS; // note overlap with ABITS

//初始化状态
private static final long ORIGIN = WBIT << 1;

//中断标识
private static final long INTERRUPTED = 1L;

// 等待/取消
private static final int WAITING   = -1;
private static final int CANCELLED =  1;

//读/写状态
private static final int RMODE = 0;
private static final int WMODE = 1;
//因为读状态只有7位很小,所以当超过了128之后将使用一个int变量来记录
private transient int readerOverflow;

看一下上面几关键的二进制表示:

            -------------------  -------------------  -------------------  -------------------
WBIT :      0000 0000 0000 0000  0000 0000 0000 0000  0000 0000 0000 0000  0000 0000 1000 0000

RBITS :     0000 0000 0000 0000  0000 0000 0000 0000  0000 0000 0000 0000  0000 0000 0111 1111

RFULL :     0000 0000 0000 0000  0000 0000 0000 0000  0000 0000 0000 0000  0000 0000 0111 1110

ABITS :     0000 0000 0000 0000  0000 0000 0000 0000  0000 0000 0000 0000  0000 0000 1111 1111

SBITS :     1111 1111 1111 1111  1111 1111 1111 1111  1111 1111 1111 1111  1111 1111 1000 0000

ORIGIN :    0000 0000 0000 0000  0000 0000 0000 0000  0000 0000 0000 0000  0000 0001 0000 0000

相对会比较直观的看出读写状态的标识。

没有写线程获取到了写状态只需判断:state < WBIT
读状态是否超出:(state & ABITS) < RFULL
获取读状态:  state + RUNIT(或者readerOverflow + 1)
获取写状态:      state + WBIT
释放读状态:  state - RUNIT(或者readerOverflow - 1)
释放写状态:  (s += WBIT) == 0L ? ORIGIN : s
是否为写锁:  (state & WBIT) != 0L
是否为读锁:  (state & RBITS) != 0L

源码

由于源码非常的复杂,这里适当分析一下。

node节点

static final class WNode {
    volatile WNode prev;
    volatile WNode next;
    volatile WNode cowait;    // list of linked readers
    volatile Thread thread;   // non-null while possibly parked
    volatile int status;      // 0, WAITING, or CANCELLED
    final int mode;           // RMODE or WMODE
    WNode(int m, WNode p) { mode = m; prev = p; }
}

值得注意的就是这个cowait,用于链接等待获取读状态的节点。

获取读锁

public long readLock() {
    long s = state, next;
    //同步队列为空并且读状态没有超过最大值则直接尝试CAS设置同步状态
    //否则调用acquireRead方法
    return ((whead == wtail && (s & ABITS) < RFULL &&
             U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
            next : acquireRead(false, 0L));
}

再来看一个巨长的代码:

private long acquireRead(boolean interruptible, long deadline) {
    WNode node = null, p;
    for (int spins = -1;;) {
        WNode h;
        //如果同步队列头节点==尾节点,说明队列中没有节点或者只有一个节点
        if ((h = whead) == (p = wtail)) {
            for (long m, s, ns;;) {
                //直接尝试获取同步状态,成功返回stamp
                //state & ABITS结果是state的前8位,RFULL为读状态的前7位的最大值-1
                //m < WBIT说明没有写状态被占有
                if ((m = (s = state) & ABITS) < RFULL ?
                    U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                    (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
                    return ns;
                //如果有写线程占有
                else if (m >= WBIT) {
                    if (spins > 0) {
                        //有50%的几率 --spins,用于控制自旋的次数
                        if (LockSupport.nextSecondarySeed() >= 0)
                            --spins;
                    }
                    else {
                        if (spins == 0) {
                            WNode nh = whead, np = wtail;
                            //判断稳定性(有没有被修改),跳出循环
                            if ((nh == h && np == p) || (h = nh) != (p = np))
                                break;
                        }
                        //初始化spins
                        spins = SPINS;
                    }
                }
            }
        }
        //p(tail)为空初始化队列
        if (p == null) { // initialize queue
            //由于此时是有写线程占有同步状态所以用一个WMODE的节点放入队列
            WNode hd = new WNode(WMODE, null);
            //CAS插入,如果失败的话下次循环再次尝试
            if (U.compareAndSwapObject(this, WHEAD, null, hd))
                wtail = hd;
        }
        //初始化代表当前读线程的节点
        else if (node == null)
            node = new WNode(RMODE, p);
        //head==tail或者队列tail.mode不为读状态,那么将当前线程的节点node加入到队列尾部并跳出外层循环
        else if (h == p || p.mode != RMODE) {
            if (node.prev != p)
                node.prev = p;
            else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
                p.next = node;
                break;
            }
        }
        //如果head!= tail说明队列中已经有线程在等待或者tail.mode是读状态RMODE,那么CAS方式将当前线程的节点node加入到tail节点的cowait链中
        else if (!U.compareAndSwapObject(p, WCOWAIT,
                                         node.cowait = p.cowait, node))
            node.cowait = null;
        //如果上面加入tail节点的cowait链中的CAS操作成功
        else {
            for (;;) {
                WNode pp, c; Thread w;
                //如果head不为空那么尝试去解放head的cowait链中的节点
                if ((h = whead) != null && (c = h.cowait) != null &&
                    U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null) // help release
                    U.unpark(w);
                //如果tail节点的前驱就是head或者head==tail或者tail节点的前驱是null
                //也就是说当前node所在的节点(因为node可能在cowait链中)的前驱就是head或者head已经被释放了为null
                if (h == (pp = p.prev) || h == p || pp == null) {
                    long m, s, ns;
                    //如果没有写状态被占有那么自旋方式尝试获取读状态,成功则返回stamp
                    do {
                        if ((m = (s = state) & ABITS) < RFULL ?
                            U.compareAndSwapLong(this, STATE, s,
                                                 ns = s + RUNIT) :
                            (m < WBIT &&
                             (ns = tryIncReaderOverflow(s)) != 0L))
                            return ns;
                    } while (m < WBIT);
                }
                //判断是否稳定
                if (whead == h && p.prev == pp) {
                    long time;
                    //如果tail的前驱是null或者head==tail或者tail已经被取消了(p.status > 0)
                    //直接将node置为null跳出循环,回到最开的for循环中去再次尝试获取同步状态
                    if (pp == null || h == p || p.status > 0) {
                        node = null; // throw away
                        break;
                    }
                    //判断超时
                    if (deadline == 0L)
                        time = 0L;
                    //如果超时则取消当前线程
                    else if ((time = deadline - System.nanoTime()) <= 0L)
                        return cancelWaiter(node, p, false);
                    Thread wt = Thread.currentThread();
                    U.putObject(wt, PARKBLOCKER, this);
                    node.thread = wt;
                    //tail的前驱不是head或者当前只有写线程获取到同步状态
                    //判断稳定性
                    if ((h != pp || (state & ABITS) == WBIT) &&
                        whead == h && p.prev == pp)
                        U.park(false, time);
                    node.thread = null;
                    U.putObject(wt, PARKBLOCKER, null);
                    //中断的话取消
                    if (interruptible && Thread.interrupted())
                        return cancelWaiter(node, p, true);
                }
            }
        }
    }


    //如果队列中没有节点或者tail的mode是WMODE写状态,那么node被加入到队列的tail之后进入这个循环
    for (int spins = -1;;) {
        WNode h, np, pp; int ps;
        //如果p(node的前驱节点)就是head,那么自旋方式尝试获取同步状态
        if ((h = whead) == p) {
            //第一次循环
            if (spins < 0)
                spins = HEAD_SPINS;
            else if (spins < MAX_HEAD_SPINS)
                spins <<= 1;
            for (int k = spins;;) { // spin at head
                long m, s, ns;
                //自旋方式尝试获取同步状态
                //获取成功的话将node设置为head并解放node的cowait链中的节点并返回stamp
                if ((m = (s = state) & ABITS) < RFULL ?
                    U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                    (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) {
                    WNode c; Thread w;
                    whead = node;
                    node.prev = null;
                    while ((c = node.cowait) != null) {
                        if (U.compareAndSwapObject(node, WCOWAIT,
                                                   c, c.cowait) &&
                            (w = c.thread) != null)
                            U.unpark(w);
                    }
                    return ns;
                }
                //如果有写线程获取到了同步状态(因为可能有写线程闯入)那么随机的--k控制循环次数
                else if (m >= WBIT &&
                         LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
                    break;
            }
        }
        //如果head不为null,解放head的cowait链中的节点
        else if (h != null) {
            WNode c; Thread w;
            while ((c = h.cowait) != null) {
                if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null)
                    U.unpark(w);
            }
        }
        //判断稳定性
        if (whead == h) {
            if ((np = node.prev) != p) {
                if (np != null)
                    (p = np).next = node;   // stale
            }
            //尝试设tail的状态位WAITING表示后面还有等待的节点
            else if ((ps = p.status) == 0)
                U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
            //如果tail已经取消了
            else if (ps == CANCELLED) {
                if ((pp = p.prev) != null) {
                    node.prev = pp;
                    pp.next = node;
                }
            }
            else {
                //超时判定
                long time;
                if (deadline == 0L)
                    time = 0L;
                else if ((time = deadline - System.nanoTime()) <= 0L)
                    return cancelWaiter(node, node, false);
                Thread wt = Thread.currentThread();
                U.putObject(wt, PARKBLOCKER, this);
                node.thread = wt;
                if (p.status < 0 &&
                    (p != h || (state & ABITS) == WBIT) &&
                    whead == h && node.prev == p)
                    U.park(false, time);
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                if (interruptible && Thread.interrupted())
                    return cancelWaiter(node, node, true);
            }
        }
    }
}

基本流程就是先判断同步队列是否为空,如果为空那么尝试获取读状态,同时如果此时写状态被占有的话还是会根据spins的值随机的自旋一定的时间如果还是没获取到则跳出自旋进入外层的循环。如果不为空说明已经有别的线程在排队了自己肯定是没戏了,那么开始检查是否需要初始化,如果没有初始化则构造一个WMODE的节点作为头节点。此时构造当前线程的节点node尝试加入同步队列,加入的方式有两种,一种是如果队列的tail是WMODE或者队列的head==tail,那么直接加入队列的尾部,并跳出外层循环,一种是加入tail节点的cowait的链中。并继续执行。
在最后一个for循环中,节点的自旋限制为先驱节点就是头节点,并且自旋同样不是无休止的,二十通过一个spins的值来控制,并且是相对随机的。同时在cowait的上面等待的节点都是RMODE,所以如果cowai节点对应的同步队列中的节点一旦解放那就意味着这个链上的所有节点都会被解放。

释放读锁

释放读锁相对要简单许多

public void unlockRead(long stamp) {
    long s, m; WNode h;
    for (;;) {
        //通过传来的stamp(也就是上一次的state)来比较
        //state & SBITS之后将抹去前7位以外的部分只剩下读状态
        if (((s = state) & SBITS) != (stamp & SBITS) ||
            (stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
            throw new IllegalMonitorStateException();
        //读状态是否超过最大值
        if (m < RFULL) {
            //设置state
            if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
                if (m == RUNIT && (h = whead) != null && h.status != 0)
                    release(h);
                break;
            }
        }
        else if (tryDecReaderOverflow(s) != 0L)
            break;
    }
}

private void release(WNode h) {
    if (h != null) {
        WNode q; Thread w;
        //将h的状态设为0
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
        //从tail开始找到距离h最近的status <= 0的节点(没有取消的节点)
        if ((q = h.next) == null || q.status == CANCELLED) {
            for (WNode t = wtail; t != null && t != h; t = t.prev)
                if (t.status <= 0)
                    q = t;
        }
        //唤醒该节点
        if (q != null && (w = q.thread) != null)
            U.unpark(w);
    }
}

获取写锁

这里的写锁是会被读锁阻塞的(因为乐观读根本就没获取同步状态,后面会讲),所以直接判断(s = state) & ABITS) == 0L(是否有锁,也就是前8位的值)

public long writeLock() {
    long s, next;
    //state & ABITS只剩下前8位,因此如果有别的写锁或者读锁存在将失败
    //尝试CAS获取写锁
    //失败调用acquireWrite
    return ((((s = state) & ABITS) == 0L &&
             U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
            next : acquireWrite(false, 0L));
}

再看一个长的:

private long acquireWrite(boolean interruptible, long deadline) {
    WNode node = null, p;
    for (int spins = -1;;) { // spin while enqueuing
        long m, s, ns;
        if ((m = (s = state) & ABITS) == 0L) {
            if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT))
                return ns;
        }
        else if (spins < 0)
            spins = (m == WBIT && wtail == whead) ? SPINS : 0;
        else if (spins > 0) {
            if (LockSupport.nextSecondarySeed() >= 0)
                --spins;
        }
        else if ((p = wtail) == null) { // initialize queue
            WNode hd = new WNode(WMODE, null);
            if (U.compareAndSwapObject(this, WHEAD, null, hd))
                wtail = hd;
        }
        else if (node == null)
            node = new WNode(WMODE, p);
        else if (node.prev != p)
            node.prev = p;
        else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
            p.next = node;
            break;
        }
    }

    for (int spins = -1;;) {
        WNode h, np, pp; int ps;
        if ((h = whead) == p) {
            if (spins < 0)
                spins = HEAD_SPINS;
            else if (spins < MAX_HEAD_SPINS)
                spins <<= 1;
            for (int k = spins;;) { // spin at head
                long s, ns;
                if (((s = state) & ABITS) == 0L) {
                    if (U.compareAndSwapLong(this, STATE, s,
                                             ns = s + WBIT)) {
                        whead = node;
                        node.prev = null;
                        return ns;
                    }
                }
                else if (LockSupport.nextSecondarySeed() >= 0 &&
                         --k <= 0)
                    break;
            }
        }
        else if (h != null) { // help release stale waiters
            WNode c; Thread w;
            while ((c = h.cowait) != null) {
                if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null)
                    U.unpark(w);
            }
        }
        if (whead == h) {
            if ((np = node.prev) != p) {
                if (np != null)
                    (p = np).next = node;   // stale
            }
            else if ((ps = p.status) == 0)
                U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
            else if (ps == CANCELLED) {
                if ((pp = p.prev) != null) {
                    node.prev = pp;
                    pp.next = node;
                }
            }
            else {
                long time; // 0 argument to park means no timeout
                if (deadline == 0L)
                    time = 0L;
                else if ((time = deadline - System.nanoTime()) <= 0L)
                    return cancelWaiter(node, node, false);
                Thread wt = Thread.currentThread();
                U.putObject(wt, PARKBLOCKER, this);
                node.thread = wt;
                if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
                    whead == h && node.prev == p)
                    U.park(false, time);  // emulate LockSupport.park
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                if (interruptible && Thread.interrupted())
                    return cancelWaiter(node, node, true);
            }
        }
    }
}

写状态的获取基本和读一样,区别在于写状态获取的时候根本就没有去判断同步队列里面是否有节点,而且尝试获取写状态的条件是(s = state) & ABITS) == 0L,也就是说要没有任何的其他锁占用的情况下才会去CAS尝试获取写状态。同时如果获取失败加入同步队列的时候只会直接加入同步队列的尾部,不会加入cowait链。这也说明了StampedLock的写是无条件去获取锁。

写锁的释放

public void unlockWrite(long stamp) {
    WNode h;
    //因为写锁是独占式的所以可以简单判断state != stamp
    if (state != stamp || (stamp & WBIT) == 0L)
        throw new IllegalMonitorStateException();
    state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
    if ((h = whead) != null && h.status != 0)
        release(h);
}
/*
*这里的(stamp += WBIT) == 0L ? ORIGIN : stamp解释:
*假设stamp为:ORIGIN + WBIT(第一次获取了写锁的状态)
*0000 0000 0000 0000  0000 0000 0000  0000 0000 0000 0000  0000 0001 1000 0000
*那么加上一个WBIT之后位
*0000 0000 0000 0000  0000 0000 0000  0000 0000 0000 0000  0000 0010 0000 0000
*此时第八位已经为0,表示已经释放了写锁
*但是随着这样累加上去可能最后会溢出结果64位全部为0,所以如果这种情况就置为ORIGIN
/*

读/写锁释放

StampedLock还提供了一个unlock方法,该方法可以自己判断并释放相应的锁。

public void unlock(long stamp) {
    long a = stamp & ABITS, m, s; WNode h;
    //判断stamp是否符合
    while (((s = state) & SBITS) == (stamp & SBITS)) {
        //如果一个锁都没有
        if ((m = s & ABITS) == 0L)
            break;
        //如果是写锁则释放写锁
        else if (m == WBIT) {
            if (a != m)
                break;
            state = (s += WBIT) == 0L ? ORIGIN : s;
            if ((h = whead) != null && h.status != 0)
                release(h);
            return;
        }
        else if (a == 0L || a >= WBIT)
            break;
        //如果是读锁并且没有overflow
        else if (m < RFULL) {
            if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
                if (m == RUNIT && (h = whead) != null && h.status != 0)
                    release(h);
                return;
            }
        }
        //如果是读锁并且overflow
        else if (tryDecReaderOverflow(s) != 0L)
            return;
    }
    throw new IllegalMonitorStateException();
}

因为读锁和写锁是互斥的,而且当前线程只能获取这其中的一把锁,所以只需判断是否是写锁即可。

乐观读

上面讲了那么多读写锁的内容,最关键的乐观读又是怎么获取锁的呢,实际上乐观读根本就没有获取锁。只是单纯的将当前的state作为stamped返回,然后读取之后再调用validate看stamp和state是否匹配,不匹配说明有别的线程进行了修改,需要程序员决定下一步的策略(使用读锁或者忽略等等)。

public long tryOptimisticRead() {
    long s;
    //当前没有读线程占用锁则返回state
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
public boolean validate(long stamp) {
    //表示该方法之前的所有load操作在内存屏障之前完成(防止重排序)
    U.loadFence();
    //验证
    return (stamp & SBITS) == (state & SBITS);
}

小结

对于StampedLock源码来说过于复杂和追求代码的简洁,所以很难看的明吧(包括我自己),但是对于读写锁的使用的来说变得更加灵活,但是有几点已知的问题需要注意:一个是StampedLock是不可重入的,二一个是StampedLock对于带有中断的线程的处理可能导致CPU暴涨。三一个是StampedLock的所有的try开头的获取都将直接尝试获取锁。最后StampedLock适用于读多写少的场景。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
[JAVA工程师必会知识点之并发编程]1、现在几乎100%的公司面试都必须面试并发编程,尤其是互联网公司,对于并发编程的要求更高,并发编程能力已经成为职场敲门砖。2、现在已经是移动互联和大数据时代,对于应用程序的性能、处理能力、处理时效性要求更高了,传统的串行化编程无法充分利用现有的服务器性能。3、并发编程是几乎所有框架的底层基础,掌握好并发编程更有利于我们学习各种框架。想要让自己的程序执行、接口响应、批处理效率更高,必须使用并发编程。4、并发编程是中高级程序员的标配,是拿高薪的必备条件。 【主讲讲师】尹洪亮Kevin:现任职某互联网公司首席架构师,负责系统架构、项目群管理、产品研发工作。10余年软件行业经验,具有数百个线上项目实战经验。擅长JAVA技术栈、高并发高可用伸缩式微服务架构、DevOps。主导研发的蜂巢微服务架构已经成功支撑数百个微服务稳定运行【推荐你学习这门课的理由:知识体系完整+丰富学习资料】1、 本课程总计122课时,由五大体系组成,目的是让你一次性搞定并发编程。分别是并发编程基础、进阶、精通篇、Disruptor高并发框架、RateLimiter高并发访问限流吗,BAT员工也在学。2、课程附带附带3个项目源码,几百个课程示例,5个高清PDF课件。3、本课程0基础入门,从进程、线程、JVM开始讲起,每一个章节只专注于一个知识点,每个章节均有代码实例。 【课程分为基础篇、进阶篇、高级篇】一、基础篇基础篇从进程与线程、内存、CPU时间片轮训讲起,包含线程的3种创建方法、可视化观察线程、join、sleep、yield、interrupt,Synchronized、重入锁、对象锁、类锁、wait、notify、线程上下文切换、守护线程、阻塞式安全队列等内容。二、进阶篇进阶篇课程涵盖volatied关键字、Actomic类、可见性、原子性、ThreadLocal、Unsafe底层、同步类容器、并发类容器、5种并发队列、COW容器、InheritableThreadLocal源码解析等内容。三、精通篇精通篇课程涵盖JUC下的核心工具类,CountDownLath、CyclicBarrier、Phaser、Semaphore、Exchanger、ReentrantLock、ReentrantReadWriteLockStampedLockLockSupport、AQS底层、悲观锁、乐观锁、自旋锁、公平锁、非公平锁、排它锁、共享锁、重入锁、线程池、CachedThreadPool、FixedThreadPool、ScheduledThreadPool、SingleThreadExecutor、自定义线程池、ThreadFactory、线程池切面编程、线程池动态管理等内容,高并发设计模式,Future模式、Master Worker模式、CompletionService、ForkJoin等课程中还包含Disruptor高并发无锁框架讲解:Disruptor支持每秒600万订单处理的恐怖能力。深入到底层原理和开发模式,让你又懂又会用。高并发访问限流讲解:涵盖木桶算法、令牌桶算法、Google RateLimiter限流开发、Apache JMeter压力测试实战。 【学完后我将达到什么水平?】1、 吊打一切并发编程相关的笔试题、面试题。2、 重构自己并发编程的体系知识,不再谈并发色变。3、 精准掌握JAVA各种并发工具类、方法、关键字的原理和使用。4、 轻松上手写出更高效、更优雅的并发程序,在工作中能够提出更多的解决方案。  【面向人群】1、 总感觉并发编程很难、很复杂、不敢学习的人群。2、 准备跳槽、找工作、拿高薪的程序员。3、 希望提高自己的编程能力,开发出更高效、性能更强劲系统的人群。4、 想要快速、系统化、精准掌握并发编程的人群。【课程知识体系图】

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值