拜托!别再问我hashmap是否线程安全

2023-10-27

拜托!别再问我hashmap是否线程安全

一、糟糕的面试

面试官:小王,你说说HashMap的是线程安全的吗?

小王:HashMap不安全,在多线程下,会出现线程安全问题。他兄弟HashTable

线程是安全的,但是出于性能考虑,我们往往会选择ConcurrentHashMap。

面试官:HashMap线程不安全的原因是什么?

小王:这个…暂时忘记了

面试官:为什么HashTable线程安全,为什么性能低?

小王:这个…

面试官:ConcurrentHashMap是怎么实现线程安全的?性能为什么较高?

小王:…

面试官:回答的很不错,回去等通知吧。

在这里插入图片描述

二、hashMap

2.1 暴露问题

大家都知道,HashMap在多线程下会存在线程安全问题,如下:

public class Demo2 {
    public static void main(String[] args) {
        //shift+ctrl+alt+u
        HashMap<String, String> map = new HashMap<>();

        Thread t1 =   new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 10; i++) {
                    map.put(i+"",i+"");
                }
            }
        });
        Thread t2 =   new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 11; i <= 20; i++) {
                    map.put(i+"",i+"");
                }
            }
        });
        t1.start();
        t2.start();
        //确保两个子线程执行完毕之后,主线程再来打印hashmap
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //遍历hashMap
        for (int i = 1; i <= 20; i++) {
            System.out.println(map.get(i + ""));
        }
    }
}

控制台:

null
2
null
null
null
6
7
8
9
10
null
null
13
null
null
null
17
18
19
20

以上例子证明了,HashMap确实存在线程安全问题。

2.2 源码追踪

翻阅源码(1.8)如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
	//此处线程不安全
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //此处线程不安全。
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

(1)代码一

if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

是否Hash冲突,没冲突就直接赋值给数组当前索引。

线程A判断通过,进入方法,切换B线程,判断通过,进入方法,切换A线程,赋值成功,切换B线程赋值成功,B线程的值覆盖了A线程的值,发生了数据覆盖,用户感受到是数据丢失。

(2) 代码二

if (++size > threshold)
        resize();

当元素个数size大于扩容阈值,则扩容,这里会有两个问题。

  • 成员的size变量没有保证原子性,因此多线程下size自增是存在原子性问题。即添加了两个元素,但是size只增加了1。
  • 两个线程如果都通过上面阈值的判断,就会发生扩容两次的情况,这也是一种安全问题。

三、HashTable

3.1 线程安全演示

我们可以使用HashTable来解决上面的安全问题。

看下面的代码:

public class Demo2 {
    public static void main(String[] args) {
        Hashtable<String, String> map = new Hashtable<>();
        Thread t1 =   new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 10; i++) {
                    map.put(i+"",i+"");//{i,i}
                }
            }
        });
        //20,20  21,21 ... 39,39
        Thread t2 =   new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 11; i <= 20; i++) {
                    map.put(i+"",i+"");//{i,i}
                }
            }
        });
        t1.start();
        t2.start();
        //确保两个子线程执行完毕之后,主线程再来打印hashmap
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 0; i <= 20; i++) {
            System.out.println(map.get(i + ""));
        }
    }
}

控制台:

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

以上代码,说明了Hashtable的确是线程安全的。

3.2 翻看源码

Hashtable源码:

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}

    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

    public synchronized int size() {
        return count;
    }

通过阅读源码可以发现,Hashtable每个操作数据的方法,都是使用了重量级锁synchronized。线程A在操作数据的时候,线程B只能阻塞。保证了整个Hash表只能线程串行化执行,从而解决了多线程产生的安全问题。

因为Hashtable是对整个哈希表进行加锁,加锁粒度过大,发生线程阻塞的概率非常大,虽然synchronized有自己的锁优化机制,但是也很快就会升级成重量级锁。而当synchronized成为了重量级锁,就会请求底层系统锁,跳出jvm级别,频繁涉及用户态和内核态的切换,性能开销比较大。

所以在今天已经不推荐使用HashTable了。

四、ConcurrentHashMap

4.1 线程安全演示

以上两个章节我们发现,在Map集合中HashMap是最常用的集合对象。但是多线程操作HashMap会有线程安全问题,解决方式就是使用HashTable,但是HashTable会全表加锁性能牺牲很大。

JDK1.5以后所提供了ConcurrentHashMap,使用它既能解决线程安全问题,性能又比HashTable高很多,所以这是目前主流的折中方案。

代码如下:

public class Demo3 {
    public static void main(String[] args) {
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 10; i++) {
                    map.put(i + "", i + "");//{i,i}
                }
            }
        });
        //20,20  21,21 ... 39,39
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 11; i <= 20; i++) {
                    map.put(i + "", i + "");//{i,i}
                }
            }
        });
        t1.start();
        t2.start();
        //确保两个子线程执行完毕之后,主线程再来打印hashmap
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 0; i <= 20; i++) {
            System.out.println(map.get(i + ""));
        }
    }
}

控制台:

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

线程安全得到保证。

总结 :

​ 1 ,HashMap是线程不安全的。多线程环境下会有数据安全问题

​ 2 ,Hashtable是线程安全的,但是会将整张表锁起来,效率低下

​ 3,ConcurrentHashMap也是线程安全的,效率较高。 在JDK7和JDK8中,底层原理不一样。

4.2 jdk7原理解析

1.ConcurrentHashMap集合底层是一个默认长度为16,加载因子为0.75的大数组 Segment数组。大数组通常是创建之后长度就固定的,而扩容是指小数组扩容。

2.默认情况下还会创建一个长度为2的小数组,把地址赋值给0索引处,其他索引此时的元素仍为null。

(Segment 继承 ReentrantLock 锁,用于存放数组 HashEntry[]。)

如下图

在这里插入图片描述

3.调用put方法时,此时会根据key的哈希值来计算出在大数组中存储的索引位置。

如果这个索引此时为null,则按照0素引的模板小数组来创建小数组。创建完毕后会二次哈希计算出key在小数组中存储的位置,然后把键值对对象存储小数组的该索引位。

如下图,先根据key的哈希算出来在大数组的4索引,创建小数组挂在4索引。接着继续使用key的hash算出存在小数组的0素引。

在这里插入图片描述

4.调用put方法时,此时会根据key的哈希值来计算出在大数组中存储的索引位置。

如果该位置不为null,就会根据记录的地址值找到小数组。二次哈希计算出小数组的索引位置。

如果需要扩容就把小数组扩容2倍。

如果不需要扩容,则会判断小数组该索引是否有元素

​ 如果没有元素,就直接存

​ 如果有元素,就调用equals方法比较key是否相同

​ 比较发现没有重复,就会在小数组上挂链表。

如下图

在这里插入图片描述

线程一来访问索引4,此时就会对索引4的Segment进行加锁。其他线程访问索引4就会阻塞,访问其他索引就可以访问,这种技术叫分段锁,将数据拆成一段一段的进行加锁。

在当前例子中,我们没有指定大数组的长度,因此长度默认是 16。在理想情况下,最多可以支持16个线程同时操作不同的segment对象,达到了并发的目的。但是如果多个线程同时操作同一个segment,就会阻塞,串行化执行。

关键字:分段锁、二次哈希、Segment数组不能扩容、HashEntry数组可以扩容

总结

ConcurrentHashMap1.7使用Segment+HashEntry数组实现的。本质上是一个 Segment 数组,Segment 继承 ReentrantLock ,同时具备了加锁和释放锁的功能。每个Segment都线程安全,全局也就安全了。把Hashtable的锁全表,变成了锁一段段的数据,粒度细提高性能。

补充:ConcurrentHashMap1.8则完全不同,放弃了Segment。数据结构使用synchronized+CAS+红黑树。锁的粒度也从段锁缩小为结点锁,粒度更细,同时数组支持扩容,并发能力更高。使用synchronized其实也是因为1.6jdk对synchronized的优化有关。

4.3 jdk8 原理解析

在1.8中,ConcurrentHashMap可以说发生翻天覆地的变化,底层数据结构不再采用segment数组,也不再采用分段锁。而是采用 数组+链表+红黑树来实现,锁也从分段锁提升成了节点锁,粒度更细。使用CAS+synchronized来保证线程安全。

底层结构:数组+链表+红黑树

CAS + synchronized同步代码块 保证线程安全

在这里插入图片描述

初始化数组源码如下:

//假设多线程来扩容,concurrentHashMap为了线程安全,只能让一个线程成功初始化数组,其他线程均失败。
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
    	//所有线程进入循环,去抢着初始化数组
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); //让线程让出cpu,以至于把cpu更多的可能让给初始化操作的线程
            //CAS操作,保证一个线程进入下面的逻辑,其他线程最终只能执行 Thread.yield();
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        //这个就是初始化数组,默认长度n为16
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

通过源码发现,这里使用了 自旋+CAS+线程让出cpu。

其中 自旋+Cas:初始化操作必须且只会由一个线程执行一次,不会初始化多个数组。

线程让出cpu:提高性能,让初始化操作更快执行

put操作源码如下:

   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;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //如果数组是null,说明第一次put,那就初始化数组
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //根据key找到索引,如果此索引为null,则使用CAS将node直接赋给当前索引
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //如果当前索引位置的元素的hash==MOVED说明此时正在发生数组扩容的数据迁移操作,
            //当前线程帮助完成数据迁移
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            //当前索引既不是null,也没有在数据迁移。此时索引位置存储的要么链表要么红黑树
            else {
                V oldVal = null;
                //对头结点进行加结点锁,保证同索引下的结点线程串行化执行
                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操作如果没有发生hash冲突,则CAS直接赋值到索引

​ 如果发生了hash冲突,判断此时是否正在扩容数据迁移,是就加入帮忙数据迁移

​ 如果此时是链表或者红黑树,就加节点锁,保证当前索引操作的线程串行化执行。

扩容源码如下:

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //计算步长,算法的目的是,cpu核数越多,步长越小。
        //步长最少为16,最多为数组的长度
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

通过源码发现:

ConcurrentHashMap采用的是多线程扩容,来提高扩容的效率。总体思想是,cpu核数越多线程越多,每个线程分得数据迁移任务量越小。

步长:单个线程负责迁移的桶数量。

下面做一个模拟扩容的流程:

假设现在数组长度有512,cpu核数2,步长32。

线程一:负责迁移索引(512-步长-1,512-1),即数组[479] - 数组[511]

线程二:负责迁移索引 数组[446] 数组[478]

如果线程一和线程二都执行完毕,两个线程就会通过CAS去抢下一个任务

如果线程二抢到了,数组[413] 数组[445]

线程一失败了,自旋继续CAS抢,抢到了

线程一 数组[381] 数组[412]

4.4 对比区别

(1)1.7用的是segment+hashentry数组实现的分段锁。只要线程没同时访问同一个分段数组,就可以并行访问默认长度16,segment数组不可以扩容(大数组),hashentry数组可以扩容。

(2)1.8用的是CAS+synchronized+voletile 实现的,底层是数组+链表+红黑树。对比1.7 锁的粒度更细,锁到了节点级别。

(3)1.8为什么synchronized替换segment?ReentrantLock:park unpark 用户态 - 内核态 性能开销比较大。synchronized:1.6之后优化了,偏向锁 轻量级锁 。此时加锁粒度非常小,比1.7小。转成重量级锁概率极小。

五、总结

通过上面的学习得知,hashmap在多线程情况下初始化数组和扩容的时候均会出现线程安全问题。我们可以通过HashTable来解决,HashTable是对整个hash表加锁,相当于线程串行化操作hash表,在解决问题的同时也会导致性能极低。最终我们可以使用ConcurrentHashMap将锁的粒度控制到最小,将性能影响控制到最低,同时扩容的时候ConcurrentHashMap还支持多线程扩容。可以说ConcurrentHashMap是多线程操作hashmap场景的不二之选,比如SpringCache框架中就使用了ConcurrentHashMap来作为本地缓存。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

拜托!别再问我hashmap是否线程安全 的相关文章

  • Maven 未运行 Spring Boot 测试

    我有一个要测试的 Spring Boot REST API 我可以在 Eclipse 中手动运行测试 无需 Maven 并通过将应用程序作为 JUnit 测试运行 它运行良好并显示结果 但是mvn test正如您将在下面发现的那样 它不起作
  • 从文件中读取文本并将每行中的每个单词存储到单独的变量中

    我有一个包含以下内容的 txt 文件 1 1111 47 2 2222 92 3 3333 81 我想逐行读取并将每个单词存储到不同的变量中 例如 当我读取第一行 1 1111 47 时 我想将第一个单词 1 存储到var 1 1111 进
  • Mockito 匹配器和基元数组

    有了 Mockito 我想verify 方法调用byte 在它的参数列表中 但我没有找到如何写这个 myMethod byte 我只想要类似的东西anyByteArray 如何使用 Mockito 做到这一点 我会尝试any byte cl
  • Spring Kafka - 为任何主题的分区消耗最后 N 条消息

    我正在尝试读取请求的卡夫卡消息数 对于非事务性消息 我们将从 endoffset N 对于 M 个分区 开始轮询并收集当前偏移量小于每个分区的结束偏移量的消息 对于幂等 事务消息 我们必须考虑事务标记 重复消息 这意味着偏移量将不连续 在这
  • Logback线程池

    我使用 Logback 进行日志记录 我有一个问题 我将 AsyncAppender 与 ConsoleAppender 结合使用 当应用程序启动时 它会创建具有 logback 线程名称的线程池 所有日志记录工作均由 AsyncAppen
  • Java 密钥库 - 以编程方式从密钥库文件中选择要使用的证书

    我有一个 java 密钥库文件 其中包含多个客户端证书 我希望在 Java 应用程序中仅选择其中一个证书来连接到服务 有没有一种简单的方法可以做到这一点 到目前为止 我找到解决方案的唯一方法是使用原始密钥库文件中的客户端证书详细信息 通过其
  • 为什么需要添加工件 JSR305 才能使用 Guava 14+?

    在stackoverflow上查找信息时 我看到了一个与我类似的问题 但没有真正的答案here https stackoverflow com questions 3800033 guava r07 gwt and javax annota
  • 如何在Eclipse中制作war文件[重复]

    这个问题在这里已经有答案了 制作war文件的简单方法是什么 当我右键单击 在服务器上运行 时 我的项目正在运行 但我想部署在 tomcat 服务器上 我已经安装了m2clipse但这给了我一个错误 maven是否必须制作war文件 我需要特
  • 如何使用 Selenium WebDriver 和 Java 滚动特定的 DIV?

    WebDriver 无法识别某些 WebElement WebDriver 无法找到浏览器可见区域中不可见的元素 In order to make the WebElement viewable by WebDriver We need t
  • 原型组件的 Spring 事件处理

    假设我有两个组件 X 和 Y 其中 X 是单例 而 Y 不是 当我发布XUpdateEvent时 没有问题 我可以捕获该事件 但是 对于 YUpdateEvent 我无法捕获事件 Spring 为每个触发的事件创建新实例 而不是使用已经创建
  • 在java中迭代日期

    我需要遍历一系列日期 不确定如何在 for 循环中获取第二天 我在用java util Date So plusDays 1 不能在 for 循环中用于获取下一个日期 Used date1 new Date date1 getTime 10
  • .NET 中的 Class.forName() 等效项?

    动态获取对象类型然后创建它的新实例的 C 方法是什么 例如 如何在 C 中实现以下 Java 代码的结果 MyClass x MyClass Class forName classes MyChildClass newInstance Lo
  • Eclipse 错误:“设置构建路径”遇到错误

    我正在使用一个名为 jtwitter 的 API 它有一个 jar 文件 jtwitter jar 我一直在使用它并使用 git 维护它 我把代码托管在github上 有些天 我没有碰过它的代码 今天 当我克隆我的 git repo 时 实
  • Java 性能:在 Java 机器控制中如何查找特定方法调用的堆大小、时间范围和内存统计信息

    使用 Java Mission Control 我们可以找到特定方法调用的堆大小 时间范围和内存统计信息 在方法分析部分 我无法弄清楚那些 要查找堆大小 您可以使用totalMemory and maxMemory Runtime 类的方法
  • 参数列表中的“...”是什么意思? doInBackground(字符串...参数)

    我不明白那个语法 尝试用谷歌搜索各种单词加上 是没有用的 它被称为varargs http java sun com j2se 1 5 0 docs guide language varargs html 这个事实应该产生更好的谷歌结果 h
  • android.R.layout.simple_list_item_1是什么?

    在我看到的所有示例中 他们在创建 ArrayAdapter 时仅使用 android R layout simple list item 1 android R layout simple list item 1是什么 它只是一个名为sim
  • 术语“可序列化”是什么意思? [复制]

    这个问题在这里已经有答案了 不太确定我读过的定义可序列化实际上做了什么 import java io Serializable import java text StringCharacterIterator import java uti
  • C中使用JNI从对象获取对象

    public class Student private People people private Result result private int amount 这是 Java 中类的示例 在C中 我试图获取 学生 中的 人 但失败了
  • 为什么永久代空间不断增长?

    我读过几篇文章 我理解以下内容 如果我错了 请纠正我和 或编辑问题 java堆是这样分段的 年轻代 创建的对象放在这里 这部分被频繁且廉价地垃圾收集 老一代 在年轻代的垃圾收集中幸存下来的对象会移到此处 该区域的垃圾收集频率较低 并且使用对
  • Java 中的下载管理器 [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我需要通过 FTP HTTP 从 Java 获取几个大文件 几个演出 有现成的库 java 命令行工具

随机推荐

  • 门级网表

    在电路设计中 网表 netlist 是用于描述电路元件相互之间连接关系的 一般来说是一个遵循某种比较简单的标记语法的文本文件 门级 gate level 指的是网表描述的电路综合级别 顾名思义 门级网表中 描述的电路元件基本是门 gate
  • git 将本地代码推送到远程仓库(核心常用)

    1 首先默认已经提前安装并且配置好git 2 选择要克隆或者保存的目标文件夹下 右键选择Git Bash Here 3 如果是将远端的项目拉取到本地 使用 git clone 命令 因为之前已经clone下来了 所以这次就不下载了 4 使用
  • Python教程:第2章(Python进阶)2.8-2.11

    文章目录 2 8 四大数据结构 2 8 1 单向队列 List 2 8 2 双向队列 Queue 2 8 3 栈 Stack 2 8 4 链表 Link 2 9 八大基础算法 2 9 0 算法复杂度 2 9 1 穷举算法 2 9 2 归纳
  • sqli-labs 38-45

    第38关 堆叠注入 简单理解就是一条语句结束后再连接一条语句 这就叫堆叠注入 举几个最简单的例子 查询数据并创建一个数据库 select from users creat database test 查询数据并删除一个数据库 select
  • java实现成绩及格的判定( true & false )

    成绩及格的判定 代码思想 1 使用条件语句 大于60及格 否则不及格 代码如下 List item IF else语句执行 import java util Scanner public class Test02 public static
  • Spring Boot中的Actuator是什么?Spring Boot中的Starter依赖是什么?

    Spring Boot中的Actuator是什么 在Spring Boot中 Actuator是一种用于监控和管理应用程序的工具 它提供了一些额外的端点和功能 使开发人员能够更好地了解和控制他们的应用程序 Actuator提供了以下功能 指
  • 【计算机视觉

    文章目录 一 分割 语义相关 6篇 1 1 Consistency guided Meta Learning for Bootstrapping Semi Supervised Medical Image Segmentation 1 2
  • Qt Creater创建项目时无法选择Kit选项 No suitable kits found

    项目场景 初步学习Qt编程 Qt Creator 安装 问题描述 创建工程时无法选择Kit套件 原因分析 No suitable kits found 没有找到合适的kits套件 在安装Qt Creator时没有安装MinGW 最直接的办法
  • 使用QT RemoteObject让两个软件(.exe)之间进行通信(VS2019+QT5.15)

    博主将两个软件进行通信是因为项目中出现调用的库只有32位的DLL和LIB 而自己写的软件却是64位 导致我的软件无法使用这个库 让我没办法控制 然后想到解决方法是写一个专门的32位软件调用这个库 然后通过与我现在写的软件进行通信 类似信号和
  • Gradle 入门之 Groovy 语言详解

    Gradle 核心是基于 Groovy 脚本语言 Groovy 脚本基于 Java 且拓展了 Java 因此 Gradle 需要依赖 JDK 和 Groovy 库 快速安装 Groovy 可以通过 Bash 命令如下 curl s get
  • 【数据结构】栈和队列的实现及应用

    需要云服务器等云产品来学习Linux的同学可以移步 gt 腾讯云 lt gt 阿里云 lt gt 华为云 lt 官网 轻量型云服务器低至112元 年 新用户首次下单享超低折扣 一 栈的概念 二 Stack h 三 Stack c 1 栈的初
  • centos 6.8 es安装

    配置操作系统参数 1 切换到root用户修改配置sysctl conf vi etc sysctl conf 添加下面配置 vm max map count 655360 1并执行命令 sysctl p 配置es参数 bootstrap m
  • C++ 数组

    C 支持数组数据结构 它可以存储一个固定大小的相同类型元素的顺序集合 数组是用来存储一系列数据 但它往往被认为是一系列相同类型的变量 数组的声明并不是声明一个个单独的变量 比如 number0 number1 number99 而是声明一个
  • STM32CubeIDE 集成的cube MX配置寄存器,IDE入门上手

    1 STM32CubeIDE属于一站式工具 本文带你体验它的强大 strongerHuang的博客 CSDN博客 STM32CubeIDE是一个多功能的集成开发工具 集成了TrueSTUDIO和STM32CubeMX 它是STM32Cube
  • ZYNQ QFLASH MX25L256 调试笔记

    该QFlash是32MB的 而ZYNQ的控制器是16MB的 所以想使用后半部分时 必须要使用4byte地址模式或者扩展模式 先调试扩展模式 在QFlash手册里面有一个写扩展模式命令 C5 写命令的时候 然后再读扩展模式寄存器 C8 就可以
  • Java项目:博客系统西瓜社区(springboot+mybatis-plus+thymeleaf)

    西瓜社区文档 项目全部源码百度网盘地址在文档中 仔细阅读就可发现 前端 社区目前基于开源的markdown框架editorme进行开发的问答和博客的两个功能 后端 springboot mybatis plus swagger2 thyme
  • 记Springboot项目中@Service(或@Component)注解失效的问题解决方法

    一 问题描述 今天基于SpringBoot写好的一个项目程序 启动时 报如下错误 2020 04 07 14 04 41 420 WARN o s b w s c AnnotationConfigServletWebServerApplic
  • 预测数值型数据:回归

    本文传送机 用线性回归找到最佳拟合直线 局部加权线性回归 通过缩减系数来 理解 数据 岭回归 lasso 前向逐步回归 用线性回归找到最佳拟合直线 线性回归 优点 结果易于理解 计算上不复杂 缺点 对非线性的数据拟合不好 适用数据类型 数值
  • 使用WIFI连接新大陆云平台(基于RT_Thread操作系统)

    前言 使用RT Thread Studio 连接WIFI 首先我们需要配置WIFI 具体的配置参考上面这篇文章 下面将会讲述使用WIFI连接到新大陆云平台 相关的AT指令 1 设置WIFI为Station模式 AT CWMODE 1 2 重
  • 拜托!别再问我hashmap是否线程安全

    拜托 别再问我hashmap是否线程安全 一 糟糕的面试 面试官 小王 你说说HashMap的是线程安全的吗 小王 HashMap不安全 在多线程下 会出现线程安全问题 他兄弟HashTable 线程是安全的 但是出于性能考虑 我们往往会选