ConcurrentHashMap解析

本文作者:王一飞,叩丁狼高级讲师。原创文章,转载请注明出处。  

接上篇,本篇主要讲解ConcurrentHashMap类在并发环境下的使用。惯例,我们先来看下ConcurrentHashMap实现原理:
jdk7以前跟以后(jdk8)实现原理不一样,所以我们分2个版本研究,先看jdk7版

jdk7版本

ConcurrentHashMap和HashMap设计思路差不多,但是为支持并发操作,做了一定的改进,ConcurrentHashMap引入Segment 的概念,目的是将map拆分成多个Segment(默认16个)。操作ConcurrentHashMap细化到操作某一个Segment。在多线程环境下,不同线程操作不同的Segment,他们互不影响,这便可实现并发操作。

Segment 字面翻译是一段,部分的意思,但我们更多称之为”槽”。Segment 继承 ReentrantLock类 ,当我们对ConcurrentHashMap并发操作时,只要锁住一个 segment,其他剩余的Segment依然可以操作。这样只要保证每个 Segment 是线程安全的,我们就实现了全局的线程安全。
具体结构:
ConcurrentHashMap结构图

上图可看出ConcurrentHashMap的大体结构,ConcurrentHashMap由一个Segment[]数组组成,数组元素是一个数组+链表的结构。
抽取部分源码:

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {
  final Segment<K,V>[] segments;
  ...
}
static final class Segment<K,V> extends ReentrantLock implements Serializable {
  transient volatile HashEntry<K,V>[] table;
}
static final class HashEntry<K,V> {
   final int hash;
   final K key;
   volatile V value;
   volatile HashEntry<K,V> next;
 }

从源码上看,我们可以看到Segment就类似一个小型的hashMap,ConcurrentHashMap就是HashMap集合。那么我们就来看下put添加操作:
ConcurrentHashMap

public V put(K key, V value) {
        Segment<K,V> s;
        //计算hash key值
        int hash = hash(key);
        //通过hash key值计算出存入Segment的位置
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          
             (segments, (j << SSHIFT) + SBASE)) == null) 
            //初始化Segment
            s = ensureSegment(j);
         //添加
        return s.put(key, hash, value, false);
}

Segment:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    //segment操作加锁,使用尝试获取锁方式。如果获取失败,进入scanAndLockForPut方法
    HashEntry<K,V> node = tryLock() ? null :
    scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
    HashEntry<K,V>[] tab = table;
    int index = (tab.length - 1) & hash;
    HashEntry<K,V> first = entryAt(tab, index);
    for (HashEntry<K,V> e = first;;) {
        if (e != null) {
        K k;
        if ((k = e.key) == key ||
            (e.hash == hash && key.equals(k))) {
            oldValue = e.value;
            if (!onlyIfAbsent) {
            e.value = value;
            ++modCount;
            }
            break;
        }
        e = e.next;
        }
        else {
        if (node != null)
            node.setNext(first);
        else
            node = new HashEntry<K,V>(hash, key, value, first);
        int c = count + 1;
        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    //扩容, 这是后续做解释
            rehash(node);
        else
            setEntryAt(tab, index, node);
        ++modCount;
        count = c;
        oldValue = null;
        break;
        }
    }
    } finally {
    //释放锁
    unlock();
    }
    return oldValue;
}

上面几个方法,ConcurrentHashMap在进行put操作时,先通过key找到承载的Segment对象位置,然后竞争操作Segment的独占锁,以确保操作线程。获取锁方式很简单,就是tryLock(),如果获取锁失败,执行scanAndLockForPut方法

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // 迭代次数
    while (!tryLock()) {
    HashEntry<K,V> f; 
    if (retries < 0) {
        if (e == null) {
        if (node == null) // speculatively create node
            node = new HashEntry<K,V>(hash, key, value, null);
        retries = 0;
        }
        else if (key.equals(e.key))
        retries = 0;
        else
        e = e.next;
    }
        //超过迭代次数,阻塞
    else if (++retries > MAX_SCAN_RETRIES) {
        lock();
        break;
    }
    else if ((retries & 1) == 0 &&
         (f = entryForHash(this, hash)) != first) {
        e = first = f; // re-traverse if entry changed
        retries = -1;
    }
    }
    return node;
}

scanAndLockForPut 实现也比较简单,循环调用tryLock,多次获取,如果循环次数retries 次数大于事先设置定好的MAX_SCAN_RETRIES,就执行lock() 方法,此方法会阻塞等待,一直到成功拿到Segment锁为止。

//循环次数,单核为1, 多核为64.
static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

解决上篇场景3问题:

回到上一篇<<并发中的Map>>提到并发场景3中map扩容问题,ConcurrentHashMap的扩容跟HashMap有点不同, ConcurrentHashMap的Segment槽是固定的16个,不变的

final Segment<K,V>[] segments;

而ConcurrentHashMap的扩容讲的是Segment中的HashEntry数组扩容。当HashEntry达到某个临界点后,会扩容2为之前的2倍, 原理跟HashMap扩容类似。

现在我们,在看会put方法中一个if分支

if (c > threshold && tab.length < MAXIMUM_CAPACITY)
    //扩容, 这是后续做解释
    rehash(node);
else

rehash方法就是HashEntry扩展逻辑。当线程执行到rehash方法时,表示当前线程已经获取到到当前Segment的锁对象,这就表示rehash方法的执行是线程安全,不会存在并发问题。
ConcurrentHashMap的remove方法跟put方法操作一样,先获取segement对象后再操作,这里就不重复了。那么我们来看下get操作:

   public V get(Object key) {
        Segment<K,V> s; 
        HashEntry<K,V>[] tab;
        //1:计算key的hash值
        int h = hash(key);
        //2:确定在segment的位置,得到HashEntry数组
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            //3:得到数据链表,迭代,查找key对应的value值
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

细心的朋友会发现get方法并没有获取锁的操作,这时就得讨论下执行get操作线程安全问题啦。
1:一线程执行put,另一个线程执行get
ConcurrentHashMap约定新添的节点是在链表的表头, 所以如果先执行get,后执行put, get操作已经遍历到链表中间了, 不会影响put的安全执行。如果先执行put,这时候,就必须保证刚刚插入的表头节点能被读取,ConcurrentHashMap使用的UNSAFE.putOrderedObject赋值方式保证。
2:一个线程执行put,并在扩容操作期间, 另一个线程执行get
ConcurrentHashMap扩容是新创建了HashEntry数组,然后进行迁移数据,最后面将 newTable赋值给oldTable。如果 get 先执行,那么就是在oldTable 上做查询操作,不发送线程安全问题;而如果put 先执行,那么 put 操作的可见性保证就是 oldTable使用了 volatile 关键字即可。

transient volatile HashEntry<K,V>[] table;

3:一线程执行remove,另一个线程执行get
ConcurrentHashMap的删除分2种情况, 1>删除节点在链表表头。那操作节点就是HashEntry数组元素了,虽然HashEntry[] table 使用了volatile修饰, 但是, volatile并保证数据内部元素的操作可见性,所以只能使用UNSAFE 来操作元素。2>删除节点中标中间, 那么好办, 只需要保证节点中的next属性是volatile修饰即可

    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
   }

总结:get方法之所以不需要加锁,原因比较简单,get为只读操作,不会改动map数据结构,所以在操作过程中,只需要保证涉及读取数据的属性为线程可见即可,也即使用volatile修饰。

jdk8版本

jdk8版本的ConcurrentHashMap相对于jdk7版本,发送了很大改动,jdk8直接抛弃了Segment的设计,采用了较为轻捷的Node + CAS + Synchronized设计,保证线程安全。
ConcurrentHashMap结构图

看上图ConcurrentHashMap的大体结构,一个node数组,默认为16,可以自动扩展,扩展速度为0.75

private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;
private static final float LOAD_FACTOR = 0.75f;

每一个节点,挂载一个链表,当链表挂载数据大于8时,链表自动转换成红黑树

static final int TREEIFY_THRESHOLD = 8;

部分代码:

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
    transient volatile Node<K,V>[] table;
}
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
}
static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
}
static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;
 }

Node
ConcurrentHashMap核心内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。
TreeNode
树节点类,当数据链表长度大于8时,会转换为TreeNode。注意,此时的TreeNode并不是红黑树对象,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。
TreeBin
TreeNode节点的包装对象,可以认为是红黑树对象。它代替了TreeNode的根节点,ConcurrentHashMap的node“数组”中,存放就是TreeBin对象,而不是TreeNode对象。
来看下jdk8版本ConcurrentHashMap 的put操作

    public V put(K key, V value) {
        return putVal(key, value, false);
    }
final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        //一个死循环,目的,并发情况下,也可以保障安全添加成功
        //原理:cas算法的循环比较,直至成功
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                //第一次添加,先初始化node数组
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //计算出table[i]无节点,创建节点
                //casTabAt : 底层使用Unsafe.compareAndSwapObject 原子操作table[i]位置,如果为null,则添加新建的node节点,跳出循环,反之,再循环进入执行添加操作
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   
            }
            else if ((fh = f.hash) == MOVED)
                 //如果当前处于拓展状态,返回拓展后的tab,然后再进入循环执行添加操作
                tab = helpTransfer(tab, f);
            else {
                //链表中或红黑树中追加节点
                V oldVal = null;
                //使用synchronized 对 f 对象加锁, 这个f = tabAt(tab, i = (n - 1) & hash) :table[i] 的node对象,并发环境保证线程操作安全
               //此处注意: 这里没有ReentrantLock,因为jdk1.8对synchronized 做了优化,其执行性能已经跟ReentrantLock不相上下。
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        //链表上追加节点
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //红黑树上追加节点
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    //节点数大于临界值,转换成红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

从put源码可看,JDK8版本更多使用的cas编程方式控制线程安全, 必要时也会使用synchronized 代码块保证线程安全。
最后,再看会ConcurrentHashMap的get方法:

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            //获取table[i] 的node元素
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
 //确保多线程可见,并且保证获取到是内存中最新的table[i] 元素值
 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

get源码也没有加锁操作,操作原理跟jdk1.7版本一样,这里就不累赘了。

回到上篇中
场景1:多线程复合操作时是否能保证线程安全
答案是不能,原因: ConcurrentHashMap 使用锁分离(jdk7)/cas(jdk8)方式保证并发环境下,添加/删除操作安全,但这进针对的是单个put 或者 remove方法,如果多个方法配合复合使用,依然需要额外加锁。
场景2:多线程同时添加相同hash 码值时转换成红黑树时,是否存在并发问题
答案是可以保证线程安全,原因:ConcurrentHashMap 链表转换成红黑树时,对转换方法做加锁防护了

private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

最后对ConcurrentHashMap 来个大总结:
1、get方法不加锁;
2、put、remove方法要使用锁
jdk7使用锁分离机制(Segment分段加锁)
jdk8使用cas + synchronized 实现锁操作
3、Iterator对象的使用,运行一边更新,一遍遍历(可以根据原理自己拓展)
4、复合操作,无法保证线程安全,需要额外加锁保证
5、并发环境下,ConcurrentHashMap 效率较Collections.synchronizedMap()更高

 

相关推荐

运行时数据区

本文作者:李敏,叩丁狼高级讲师。原创文章,转载请注明出处。前言为什么要了解虚拟机如何操作内存?java与c/c++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来.对于java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出.有虚拟机管理内存,这一切看起...

wolfcode_cn的博客 1015

使用Mycat实现数据库读写分离

本文作者:蓝雄威,叩丁狼高级讲师。原创文章,转载请注明出处。01前言在上一篇文章中,我们基于CentOS7实现了MySQL的主从复制,我们需要把DML操作放在MySQL的主节点执行,需要把DQL操作放在MySQL的从节点执行.那在应用程序中我们怎么控制这个逻辑呢?下面是一种可行的方案:1.在项目中配置两个数据源(一主一从),分别是A(主)和B(从)。2.在程序中判断是不是select...

wolfcode_cn的博客 1342

线程基础(三十三)

接上篇,本篇讲解线程另外一个设计模式:Producer-Consumer Pattern.概念Producer是生产,生产者的意思, 指生产数据的线程, Consumer 则是消费,消费者的意思. 指使用数据的线程.Producer-Consumer 模式主要目标:生产者生产数据能安全得交给消费者进行处理.当Producer 跟 Consumer 都为一个线程时, Producer-Con...

wolfcode_cn的博客 963

线程基础(三十二)

接上篇,本篇讲解线程另外一个设计模式:Balking Pattern.概念Balking 是停止并返回意思. 如果当前线程不适合(不满足守护条件)执行某个操作直接返回,这就是Balking 模式.Balking 模式跟之前的Guarded Suspension模式有点类似, 区别:Guarded Suspension模式不满足守护条件,线程需要挂起等待, 而Balking 模型不满足守...

wolfcode_cn的博客 804

线程基础(三十一)

本文作者:王一飞,叩丁狼高级讲师。原创文章,转载请注明出处。接上篇,本篇讲解线程另外一个设计模式:Guarded Suspension Pattern.概念Guarded是被守护,被保卫,被保护的意思, Suspension 则是暂停的意思. 如果不满足执行条件,先让当前处理的线程暂停.这就是Guarded Suspension模式.参与角色Guarded Suspension模式参...

wolfcode_cn的博客 1017

刚进职场的程序员,和工作了2、3年的程序员到底有什么不一样?

“老师,我们刚进职场,和工作了2、3年的程序员到底有什么不一样?”这是很多同学在进入职场后,问过我最多的问题。不妨让我来翻译一下:老师,我到底有哪些表现让我在职场显得特别“嫩”,没办法获得领导、同事更多的认可和信任?于是我总结分析了工作中常见的三个误区,如果在工作中有意识地规避这些误区,也许就能帮助你快速成为“职场老司机”——01 不敢争取【情景】公司有一个做大项目的机会,经理问你是否愿意参与,但你没有十足的把握,最后权衡来权衡去还是拒绝了。【解析】这里面有两个层次的问题。第一,还是自信.

wolfcode_cn的博客 4942

『面试篇』注意面试中常见的三大误区,大幅度提升你被录用的机会

​先分享一个真实案例:之前我发现有一个学员把简历上面的项目经验删除了三分之二,我问他为什么?他说因为在一场面试中HR说不相信这些项目是他做的,没有给他解释机会就结束了面试。于是他认为找不到工作是因为自己做的项目经验太多了,于是在接下来的面试中,不仅删除了自己做过的项目,还在面试环节装作不懂的样子。这是很明显的心态和认知都被面试官影响的例子。大家好,我是叩丁狼的就业老师刘梓华。这些年帮助过上千个学员找到工作,不管是学员的面试问题还是工作期间遇到的非技术问题,我都有一些想和大家分享的经验。针.

wolfcode_cn的博客 1122

初级程序员应该怎么选电脑?来自从业5年维修工程师的建议

大家好,我是叩丁狼的彬辉老师。因为工作原因,经我手修好的电脑没有一千也有八百,关于编程开发对硬件的要求,也是了然于心。不管你是什么级别的程序员,选配置也是一件头疼的事,这里我给大家整理一些简单的配置参考,希望能帮到大家——其实电脑配置远不止这些,还有散热、主板、处理器、电源等等,这次主要提到对电脑性能影响较大的四个核心参数,方便同学们更快更精确地挑选合适的电脑,如果你对电脑已经有一定的了解,欢迎来办公室找我交流~至于选择笔记本还是台式,完全看个人喜好:同等配置下,台式机性价比更高,可以定制化.

wolfcode_cn的博客 8286

互联网最大谣言:程序员35岁必淘汰?今天我就来击碎他!

朋友,只要你是程序员,你一定知道996和“程序员35岁必死”的言论。这两个话题在互联网上的讨论一次比一次激烈。996工作制,众所周知,每天早上9点到岗,一直待到晚上9点,每周工作6天,很多互联网公司在招聘前甚至就会私下告诉你公司实行996制度。尽管很多年轻程序员在工作之前已经做了一定的心理准备,而一旦真正直面这些问题,心里肯定会打怵:“我现在年轻,还可以接受加班,可等我成家了、年纪大了、身体吃不消怎么办?”今天我就从个人的角度跟大家分享一下我的看法。程序员最爱给自己挖坑本人从事过开发工作,现在

wolfcode_cn的博客 1万+

用了这么久开源社区,还搞不明白免费/自由/开源软件?

在开源社区中,有很多人经常把免费软件、自由软件、开源软件这三个概念混淆在一起。今天就来和大家谈谈这三个软件到底有什么不一样。免费软件用户可以免费使用该软件,但通常会有其他权限的限制:1.源码不一定会公开/不能修改软件源码;2.限制拷贝的代码数量和段落;3.不得用于商业用途、不能以该软件牟利、仅供个人使用、仅供学习交流使用等等,如果有人未经作者允许则随意散布、传播,作者有权进行追责;4.限制使用者研究、修改和再散布的权利;免费软件免费的目的无非出于:先提供免费版本软件扩大市场占有率,当

wolfcode_cn的博客 1635

中国开发者现状报告:最想学Java语言、金融业高收入比例大、仅2%的人不安排学习时间......

​在《2019-2020 年中国开发者调查报告》中,我们从「开发者现状」中有最新的发现:6成开发者在使用Java语言;在一线城市,30%开发者月薪超过1.7万元;互联网、软件、IT制造三个技术领域涵盖了国内84%以上的开发者;开发者学习热情高涨,31%接受了软件开发的在职培训…*以下数据报告来源:优快云软件开发工具使用特点分析01Java长盛:使用最多、最想学60%的开发者使用最多的编程语言是Java,Java、Python、Javascript是开发者最想学的三种语言。左图为开

wolfcode_cn的博客 1094

如何在百度中脱颖而出——前端与SEO

SEO:译为搜索引擎优化,搜索引擎优化技术现在随着百度,谷歌,搜狗等搜索引擎的兴起,越来越火爆,那今天,我们一起来看看,SEO和前端有着怎样千丝万缕的联系~SEO的由来:早在90年代国外便有了SEO的概念,曾经由于谷歌的SEO,导致很多媒体行业的不满。后来大约03年,由于一款游戏(传奇私服),各种私服产业恶性竞争,慢慢的,SEO在中国逐渐商业化。SEO分类:SEO主要分为白帽SEO和黑帽SEO,就像白帽黑客和黑帽黑客一样。白帽SEO:改良网站设计规范,使网站对搜索引擎和用户更加友好黑帽SEO:利

wolfcode_cn的博客 990

28岁程序员郭宇财务自由退休,都说他运气爆棚,但我看未必...

今年6月,一封退休告别信最近引爆了微博、知乎等各大平台,一位名叫郭宇的程序员走进了大众的视野。郭宇是谁?他的微博简介是这么写的:株式会社山月夜代表取缔役社长/住过150+个日本温泉乡/100万公里飞行进度86.5%/程序员/原字节跳动资深技术专家。说到程序员,大家都会想到996、过劳、无休止地加班、没有自由…而这位程序员颠覆了大众的认知,不仅在28岁成功实现财务自由从字节跳动退休,并在日本开了一家温泉旅馆,当起了作家。面对这一系列的“暴击”,让不少互联网行业人直呼:“我酸了!”01 28岁财

wolfcode_cn的博客 3901

疫情让程序员活得更难吗?2020年程序员薪资满意度、跳槽状况、裁员补偿数据大公开!

自从网传程序员到了35岁之后必须要转行,现在又有人传言:“疫情之下,程序员今年要过苦日子了,降薪裁员是大趋势。”不是,我就不明白了,你们怎么就看不得程序员好呢?天天巴望着程序员降薪、转行、裁员…我们不可否认,从去年年底开始,疫情就导致了很多企业陷入危机,继而停发薪资、变相裁员,在如此“难搞”的大环境下,对程序员来说究竟意味着什么?在疫情期间,程序员客栈对北京、上海、广东、浙江等全国29个省、直辖市、特别行政区的近40万程序员进行了一次详细调查。我想这份详细调查数据足以吊打网络上传出莫名其妙的“程序

wolfcode_cn的博客 2173

程序员和码农有什么不同?从这三个单词就看得出来

码农这个词很早就有了,但我以为这只是程序员偶尔自黑的幽默,直到论坛、博客上越来越多人正儿八经地自称“码农”、“码畜”、“搬砖的”…有人认为“码农”、“程序员”、“工程师”等等这些称谓大可不必过于在意,名称和真实情况是两回事。不过我也比较介意别人叫我码农,可能本身并无恶意,但是感觉这个称呼实在配不上自己的工作创意与工作程度,就像我可以说自己是屌丝,但别人这么叫自己肯定不舒服。举个对比的例子:设计师VS美工艺人VS唱歌的心理咨询师VS路边算卦财务计算VS收银新媒体运营VS小编…程序员不是码

wolfcode_cn的博客 6987

CSS3绘制各种形状

本文为叩丁狼高级讲师原创文章,转载请注明出处。现在的CSS功能非常强大了,特别是CSS3的属性,例如转换属性,过渡属性,动画属性,能做的效果非常多。在网页开发中,会经常遇到一些小符号或者形状,在以前就只能通过切图来实现,切图这种方式是用起来方便,但是会增加请求。而现在浏览器对CSS3的兼容基本没有什么问题,所以在网页开发的时候遇到符号或者形状,能写的都是用CSS来书写了。而这篇文章就是收...

wolfcode_cn的博客 1114

强大而好用的选择器:focus-within

本文为叩丁狼高级讲师原创文章,转载请注明出处。伪类和伪元素在开发网页样式中,选择器必不可少,而且选择器也是在开发css中非常重要的内容,包括常用的类选择器,id选择,同时还有伪类,伪类选择器最大的特点就是冒号开头。平时也经常会有小伙伴问到,在使用的时候有伪类,也有伪元素,有些添加单冒号,有些添加双冒号,有没有什么区别?在网上总结的比较多的一个特点就是,其实在使用的时候,无论是伪类还是伪...

wolfcode_cn的博客 1046

JavaScript-DOM操作

本文为叩丁狼高级讲师原创文章,转载请注明出处。在之前的文章中,学习过了DOM的访问方式,其中也把DOM相关的概念进行了学习,其实DOM的内容还有很多,所以本文将会继续学习DOM操作相关的内容,DOM操作涉及到的操作有:增、删、改、查,而查这方面的内容其实就是上一次所写的“访问方式”。##DOM的增加DOM操作中增指的是增加节点,分为两部分:创建节点和插入节点。###创建节点创建节...

wolfcode_cn的博客 682

超简单自动编译sass扩展和工具

本文为叩丁狼高级讲师原创文章,转载请注明出处。在之前的文章中,学习过了DOM的访问方式,其中也把DOM相关的概念进行了学习,其实DOM的内容还有很多,所以本文将会继续学习DOM操作相关的内容,DOM操作涉及到的操作有:增、删、改、查,而查这方面的内容其实就是上一次所写的“访问方式”。##DOM的增加DOM操作中增指的是增加节点,分为两部分:创建节点和插入节点。###创建节点创建节...

wolfcode_cn的博客 684

Css、less和Sass(SCSS)

本文为叩丁狼高级讲师原创文章,转载请注明出处。背景CSS(层叠样式表)是一门非程序式语言,入门学习使用非常直观方便,但是队友一些比较复杂或者重用性比较强的项目来说,因为CSS没有变量、函数、SCOPE(作用域),需要书写大量看似没有逻辑的代码,不方便维护及扩展,不利于复用,尤其对于非前端开发工程师来讲,往往会因为缺少 CSS 编写经验而很难写出组织良好且易于维护的 CSS 代码。为了方便前...

wolfcode_cn的博客 1642

面试题:如何在10亿个随机整数中找出前1000个最大的数

本文作者:梁开权,叩丁狼高级讲师。原创文章,转载请注明出处。我们知道排序算法有很多:冒泡算法:通过两层for循环,外层第一次循环找到数组中最大的元素放置在倒数第一个位置,第二次循环找到第二大的元素放置在倒数第二个位置。。。循环N次就可以找到TopN。缺点:冒泡排序内层循环需要大量交换元素。复杂度介于O(n)和O(n^2)之间。快速排序:选一个基准元素,每次排序可以将这个基准元素...

wolfcode_cn的博客 1631

MyBaits-Plus

本文作者:范佳龙,叩丁狼高级讲师。原创文章,转载请注明出处。MyBaits-PlusMyBatis的优点Mybatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old ...

wolfcode_cn的博客 1217

线程基础(三十四)-Read-Write Lock Pattern

本文作者:王一飞,叩丁狼高级讲师。原创文章,转载请注明出处。接上篇,本篇讲解线程另外一个设计模式:Read-Write Lock Pattern.概念Read是读, 指获取/查询数据的线程, Write是写,指操作(增删改)数据的线程.Read-Write Lock 模式要求:1>在读模式时,多个线程可以同时执行读操作,但不允许写操作2>在写模式时,只允许一个线程执行写...

wolfcode_cn的博客 509

一起来看源代码-01TreeMap添加操作

本文作者:黄海燕,叩丁狼高级讲师。原创文章,转载请注明出处。##前言之前很多小伙伴问我怎么看源代码,还有就是越来越多的程序员都想要看源代码,搞懂底层原理,但是感觉源代码非常的晦涩难懂,不够直接和清晰,所以我希望这篇文章能够快速带同学们看懂java源码,更加深入的学习java,帮助小伙伴们节约学习的时间成本.##1.树的介绍什么是树结构?其实就是一个节点下面有多个子节点,我们称之为...

wolfcode_cn的博客 483

MongoDB With Jav

本文作者:杨龙,叩丁狼高级讲师。原创文章,转载请注明出处。准备工作要使用 Java 代码操作 MongoDB 数据库,创建 Maven 项目则先添加 MongoDB 的 Java 驱动依赖.<!-- MongoDB 的 Java 驱动 --><dependency> <groupId>org.mongodb</groupId>...

wolfcode_cn的博客 623

垃圾回收器和内存分配策略

本文作者:李敏,叩丁狼高级讲师。原创文章,转载请注明出处。4. 垃圾回收器和内存分配策略**GC(Garbage Collection)**的历史比java久远.1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言.GC一直致力于解决的问题:哪些内存需要回收(what)?什么时候回收(when)?如何回收(how)?在虚拟机运行时数据区中,最频繁使用的是...

wolfcode_cn的博客 419

Sharding JDBC-分库分表

本文作者:梁开权,叩丁狼高级讲师。原创文章,转载请注明出处。环境准备pom.xml<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <ver...

wolfcode_cn的博客 558

Sharding JDBC-读写分离

本文作者:梁开权,叩丁狼高级讲师。原创文章,转载请注明出处。读写分离接着上一篇的分表分库我们继续来说读写分离案例模型让数据库的读和写功能分开,其中master负责所有的写操作以及在特殊情况下负责少量的读操作,slave在任何情况下都负责所有的读操作,不负责任何写操作,同样完成这个操作我们需要至少需要2个连接池,具体选择哪个连接池来操作取决于内部的一个叫路由的组件,该组件具有SQL的识别...

wolfcode_cn的博客 466

线程基础(三十)

本文作者:王一飞,叩丁狼高级讲师。原创文章,转载请注明出处。接上篇,本篇讲解线程另外一个设计模式:Immutable Pattern. 不可变模式.概念Immutable Pattern:不可变模式, 设计思路非常简单, 要求共享资源是一个不可改变对象, 线程只能访问资源,而不允许对资源修改.并发环境下即使不使用加锁操作,也可保证线程操作安全.知识补充敞开讲述前,先来科普一下不可对象...

wolfcode_cn的博客 282

线程基础(二十八)

本文作者:王一飞,叩丁狼高级讲师。原创文章,转载请注明出处。#####List集合######SynchronizedList :一个由普通list转换而来的线程安全集合List list = Collections.synchronizedList(new LinkedList(…));######CopyOnWriteArrayList :一种读写安全的ArrayList集合Co...

wolfcode_cn的博客 279
上一篇: 第一个基于Apache Ignite的应用
下一篇: Java并发环境下的队列(Queue)概述
叩丁狼
叩丁狼 企业官方账号 企业官方账号
博客等级 码龄9年 914粉丝 349原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值