目标:假定我们要定义一个类似于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 不会被初始化多次