ConcurrentHashMap --- 怎样在高并发环境下初始化一个数组


目标:假定我们要定义一个类似于HashMap的数组结构,该数据结构要确保即使在高并发多次初始化的背景下,具体存储的数组的初始化仍然是正确的。我们对这个结构可以简化一些,先考虑其元素的存储

 

实现1:实现一个线程不安全的容器

class TableHolder {
    static final int DEFAULT_CAPACITY = 16;
    volatile Element[] table;
    volatile int sizeCtl; // 当前数组的大小, 默认为0
    public void initializeTable() {
        int sc = sizeCtl;
        if (sc == 0) {
       System.out.pritln("initializing the table"); sc
= sizeCtl = DEFAULT_CAPACITY; table = new Element[sc]; } } } class Element { int key; int val; }

上述代码块为最为常见的初始化模式为什么说上述实现时不安全呢?请看

@Test
    public void how2InitializeATableSafely() throws InterruptedException {
        TableHolder th = new TableHolder();
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            Thread t = new Thread(() -> {
                th.initializeTable();
                System.out.println(th.sizeCtl);
            });
            threadList.add(t);
            t.start();
        }

        for (Thread thread : threadList) {
            thread.join();
        }
    }

通过上述方法测试发现,初始化Element数组的关键区域被多次调用

实现2:实现一个重量级锁的容器

为解决实现1出现的 table 数组被多次初始化的问题,作为直接的方式是在外层方法直接加锁,如下:

class TableHolder {
    static final int DEFAULT_CAPACITY = 16;
    volatile Element[] table;
    volatile int sizeCtl; // 当前数组的大小, 默认为0
    public synchronized void initializeTable() {
        int sc = sizeCtl;
        if (sc == 0) {
            sc = sizeCtl = DEFAULT_CAPACITY;
            table  = new Element[sc];
        }
    }
}

此时再借助我们的 how2InitializeTableSafely 方法我们可以正确的初始化 table 数组了。但有个问题,采用 synchronized 加锁的方法,性能较差。为此我们引入了第三种,采用CAS的方式进行table的初始化

 

实现3:实现一个相对高效的容器

相对于实现2中粗暴的直接对方法加锁的方式,我们可以降低粒度,对控制数组大小的一个属性 sizeCtl 进行 cas 操作,进而避免对热区初始化代码的多次进入

 1 class TableHolder {
 2     static final int DEFAULT_CAPACITY = 16;
 3     volatile Element[] table;
 4     volatile int sizeCtl; // 当前数组的大小, 默认为0
 5     public void initializeTable() {
 6         int sc;
 7 
 8         while (table == null || table.length == 0) {
 9             System.out.println("table is 0, go to initialize");
10             if ((sc = sizeCtl) < 0) {
11                 System.err.println("lost race, cause another thread is initializing the table");
12                 Thread.yield();
13             }
14             else if (U.compareAndSwapInt(this, SIZE_CTL_OFFSET, sc, -1)) {
15                 if (table == null || table.length == 0) {
16                     int n = sc > 0 ? sc : DEFAULT_CAPACITY;
17                     System.out.println("win race, initializing the table of size: " + n);
18                     table  = new Element[n];
19                     sc = n - (n >>> 2);
20                 }
21                 sizeCtl = sc;
22                 break;
23             }
24         }
25     }
26 
27     static Long SIZE_CTL_OFFSET;
28     static final Unsafe U;
29     static {
30         try {
31             U = getUnsafe();
32             Class<?> k = UnsafeCASTest.TableHolder.class;
33             SIZE_CTL_OFFSET = U.objectFieldOffset(k.getDeclaredField("sizeCtl"));
34         } catch (NoSuchFieldException | IllegalAccessException e) {
35             throw new Error(e);
36         }
37     }
38 
39     static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
40         Field f = Unsafe.class.getDeclaredField("theUnsafe");
41         f.setAccessible(true);
42         return (Unsafe) f.get(null);
43     }
44 }

我们可以看到控制初始化代码的关键在于第 14 行,多个线程会在这里竞争更新 sizeCtl 这个 volatile 变量。

如果可以原子化的更新 sizeCtl,则获得了15-22 这一段热区代码的执行权,初始化 table 数组(初始化期间,sizeCtl 为 -1),退出循环。

没有获得热区代码执行权的线程会循环判断 table 是否初始化,并争取 sizeCtl 更新的所有权 (sizeCtl 为 volatile 变量,其他线程对该变量的更改是对当前执行线程可见的)。

如果某个线程可一个成功的更新 sizeCtl(获得15-22执行权后),此时我们还要额外的判断一下 table 是否已被初始化(见15),确保 table 不会被初始化多次

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM