Java多线程进阶

目录

1:Java 运行过程大体介绍

2:运行时数据区详解

3:多线程三大特性

4:多线程控制类

4.1.1:ThreadLocal作用

4.1.2:ThreadLocal常用方法

4.1.3:ThreadLocal底层实现

4.2.1:原子类分类

4.2.2:原子类解决非原子性操作问题

4.2.3:原子类CAS原理分析

4.2.4:CAS的ABA问题

4.2.5:AtomicStampReference解决ABA问题

4.3.1:Lock锁的关系实现图

4.3.2:读写锁:可以同时读,不能同时写,写的时候不能读

4.4:volatile关键字

5:容器

5.1:容器类关系图如下:

5.2:HashMap实现分析

5.3:hashmap底层实现

5.4:hashmap并发问题解决方案

6:同步容器

7:并发容器

7.1:并发容器简介

7.2:ConcurrentHashMap数据结构简介

7.3:ConcurrentHashMap同步原理


1:Java 运行过程大体介绍

其中运行时数据区主要由五部分组成:虚拟机栈,堆,方法区(永久堆),程序计数器(帮助执行虚拟机栈中的方法),本地方法栈(最底层方法);

2:运行时数据区详解

  • 程序计数器:每个线程对应有一个程序计数器,各线程的程序计数器是私有的,互不影响,且线程安全的。程序计数器记录线程正在执行的内存地址,以便被中断线程恢复执行时再次继续执行;
  • 虚拟机栈:每个线程会对应一个Java栈,每个Java栈由若干栈帧组成,每个方法对应一个栈帧,栈帧在方法运行时入栈,完成时弹出栈帧作为返回值,该栈帧被清除。栈顶的方法叫活动栈,表示当前执行的方法才可被cpu执行。线程请求的栈深度大于虚拟机所允许的栈深度,将抛出StackOverFlowError异常。栈扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。栈中存的是引用,具体变量在堆中。

  • 方法区:是Java堆的永久区,存放了要加载的类的信息(名称,修饰符等),类中的静态变量,定义的final常量,field信息,类中的方法等。方法区是Java线程共享的,方法区内存溢出时,会抛出OutOfMemoryError:PremGen space的错误信息。
  • 常量池:是方法区的一部分,存储两类数据,字面量和引用量。字面量是:字符串,final类型等。引用量是:类/接口、方法字段的名称和描述符。常量池在编译期间就已经确定,并保存在了.class文件中;
  • 本地方法栈:为jvm执行native方法服务,也会抛出StackOverFlowError和OutOfMemoryError异常;

Java内存模型工作示意图:

3:多线程三大特性

  • 原子性: 一个或者多个操作,要么全部执行(不会被打断),要么都不执行
  • 可见性:当多个线程访问一个变量时,一个线程修改了这个值,其他线程要立即能看到这个修改结果;(避免脏读)
  • 有序性:即程序执行顺序按代码的先后顺序执行;

4:多线程控制类

4.1.1:ThreadLocal作用

ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本,当某些数据是以线程为作用域并且不同线程有不同副本的时候,就可以考虑ThreadLocal。比如数据库链接Connection,每个请求处理线程都需要,但又互不影响,就是要ThreadLocal实现。

4.1.2:ThreadLocal常用方法

  • initialValue():副本创建方法
  • get():获取副本方法
  • set():设置副本方法

代码例子:线程2new一个person命名为zhangsan,线程1调用get方法打印null值。原因是ThreadLocal为每个线程单独创建区域,每个线程只能控制自己的变量信息。

import java.util.concurrent.TimeUnit;

/**
 * ThreadLocal线程局部变量
 *
 * ThreadLocal是使用空间换时间,synchronized是使用时间换空间
 * 比如在hibernate中的session就存在ThreadLocal中,避免synchronized的使用
 */
public class ThreadLocal2 {
    //volatile static Person p =new Person();
    static ThreadLocal<Person> t1=new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(t1.get());
        }).start();


        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t1.set(new Person());
        }).start();
    }

    static class Person{
        String name="zhangsan";
    }
}


//打印结果为null

4.1.3:ThreadLocal底层实现

4.2.1:原子类分类

注:如果不适用原子类,比如在实现i++的时候没有上锁,i++可能分部执行,导致数据异常操作;

4.2.2:原子类解决非原子性操作问题

AtomicInteger类可以保证i++的原子性

  • getAndIncrement()对应n++;
  • incrementAndGet()对应++n;
  • decrementAndGet()对应--n;
  • getAndDecrement()对应n--;

4.2.3:原子类CAS原理分析

图示如下,相当于Mysql中的乐观锁(循环比较,直至和期望值相同返回True)

4.2.4:CAS的ABA问题

问题描述:线程1将A修改为B,又将B修改为A。其他线程判断此值没有修改过,在特定问题下会出错;

下图compareAndSet(A,B)方法为判断栈顶值,如果为A,则替换为B

t1初始为AB,转到t2,t2已经将整个栈修改了,但栈顶元素还是A,t1执行完方法后,栈内只剩一个B,和预期的BB不符。

解决方法:加时间戳,每次进行对比的时候同时对比时间戳,和自己上次时间戳相同时再执行操作。

4.2.5:AtomicStampReference解决ABA问题

实现每次操作时候对比时间戳,每次操作完成之后,时间戳也改变。循环对比直至成功。

4.3.1:Lock锁的关系实现图

  • 可重入锁:在需要锁的时候调用方法,依然可以拿到锁

4.3.2:读写锁:可以同时读,不能同时写,写的时候不能读

实例代码如下:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 读写操作类
 */
public class ReadWriteLockDemo {

    private Map<String, Object> map = new HashMap<String, Object>();
    //创建一个读写锁实例
    private ReadWriteLock rw = new ReentrantReadWriteLock();
    //创建一个读锁
    private Lock r = rw.readLock();
    //创建一个写锁
    private Lock w = rw.writeLock();

    /**
     * 读操作
     *
     * @param key
     * @return
     */
    public Object get(String key) {
        r.lock();
        System.out.println(Thread.currentThread().getName() + "读操作开始执行......");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            return map.get(key);
        } finally {
            r.unlock();
            System.out.println(Thread.currentThread().getName() + "读操作执行完成......");
        }
    }

    /**
     * 写操作
     *
     * @param key
     * @param value
     */
    public void put(String key, Object value) {
        try {
            w.lock();
            System.out.println(Thread.currentThread().getName() + "写操作开始执行......");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
        } finally {
            w.unlock();
            System.out.println(Thread.currentThread().getName() + "写操作执行完成......");
        }
    }

    public static void main(String[] args) {
        final ReadWriteLockDemo d = new ReadWriteLockDemo();
        d.put("key1", "value1");
        new Thread(new Runnable() {
            public void run() {
                d.get("key1");
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                d.get("key1");
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                d.get("key1");
            }
        }).start();
    }

}

执行结果如下:

4.4:volatile关键字

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(注意:不保证原子性
  2. 禁止进行指令重排序。(保证变量所在行的有序性

例如int a;a = 1;a = 2,a=3;在不用volatile修饰时,编译器会直接执行第三行,修饰之后,编译器会按步执行;

当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

应用场景如下(其一,在其他多线程共享变量中,尽可能都加volatile):

//双重校验
class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

5:容器

5.1:容器类关系图如下:

具体容器类详情参考:https://blog.csdn.net/qq_38869493/article/details/105015262

5.2:HashMap实现分析

HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;

链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;
HashMap综合应用了这两种数据结构,实现了寻址容易,插入删除也容易。

HashMap结构示意图如下:

注:jdk1.8之后将hashmap的链表进行了优化,当>8时,转为红黑树

5.3:hashmap底层实现

put方法:

// 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。      
    void addEntry(int hash, K key, V value, int bucketIndex) {      
        // 保存“bucketIndex”位置的值到“e”中      
        Entry<K,V> e = table[bucketIndex];      
        // 设置“bucketIndex”位置的元素为“新Entry”,      
        // 设置“e”为“新Entry的下一个节点”      
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);      
        // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小      
        if (size++ >= threshold)      
            resize(2 * table.length);      
    }  
//在hashmap做put操作的时候会调用到以上的方法。现在假如A线程和B线程同时对同一个数组位置调用
//addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,
//那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失

remove方法:

final Entry<K,V> removeEntryForKey(Object key) {      
        // 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算      
        int hash = (key == null) ? 0 : hash(key.hashCode());      
        int i = indexFor(hash, table.length);      
        Entry<K,V> prev = table[i];      
        Entry<K,V> e = prev;      
     
        // 删除链表中“键为key”的元素      
        // 本质是“删除单向链表中的节点”      
        while (e != null) {      
            Entry<K,V> next = e.next;      
            Object k;      
            if (e.hash == hash &&      
                ((k = e.key) == key || (key != null && key.equals(k)))) {      
                modCount++;      
                size--;      
                if (prev == e)      
                    table[i] = next;      
                else     
                    prev.next = next;      
                e.recordRemoval(this);      
                return e;      
            }      
            prev = e;      
            e = next;      
        }      
     
        return e;      
    }  
//当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,
//然后各自去进行计算操作,之后再把结果写会到该数组位置去,
//其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改。

数组大小大于初始值时扩容操作:

//resize操作,代码如下:
// 重新调整HashMap的大小,newCapacity是调整后的容量      
    void resize(int newCapacity) {      
        Entry[] oldTable = table;      
        int oldCapacity = oldTable.length;     
        //如果就容量已经达到了最大值,则不能再扩容,直接返回    
        if (oldCapacity == MAXIMUM_CAPACITY) {      
            threshold = Integer.MAX_VALUE;      
            return;      
        }      
     
        // 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,      
        // 然后,将“新HashMap”赋值给“旧HashMap”。      
        Entry[] newTable = new Entry[newCapacity];      
        transfer(newTable);      
        table = newTable;      
        threshold = (int)(newCapacity * loadFactor);      
    }  
// 这个操作会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,
//之后指向新生成的数组。当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,
//各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,
//其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,
//就会用已经被赋值的table作为原始数组,这样也会有问题。

5.4:hashmap并发问题解决方案

  1. Synchronized关键字
  2. Lock
  3. 同步类容器
  4. 并发类容器

6:同步容器

在Java中,同步容器主要包括2类:

1VectorStackHashTable(可以独立创建)

2Collections类中提供的静态工厂方法创建的类(借助工具类创建)

  • Vector:实现了List接口,Vector实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施。
  • Stack:也是一个同步容器,它的方法也用synchronized进行了同步,它实际上是继承于Vector类。
  • HashTable:实现了Map接口,它和HashMap很相似,但是HashTable进行了同步处理,而HashMap没有。HashTable的remove,put,get等public方法做成了同步方法,保证了HashTable的线程安全性。
  • Collections:Collections类是一个工具提供类,注意,它和Collection不同,Collection是一个顶层的接口。在Collections类中提供了大量的方法,比如对集合或者容器进行排序、查找等操作。最重要的是,在它里面提供了几个静态工厂方法来创建同步容器类,如下图所示:

7:并发容器

7.1:并发容器简介

同步容器将几乎所有方法添加的synchronized进行同步,这样保证了线程的安全性,但代价就是严重降低了并发性能,当多个线程竞争容器时,吞吐量严重降低。Java5.0开始针对多线程并发访问重新设计,提供了并发性能较好的并发容器,引入了java.util.concurrent包。有如下:

  • ConcurrentHashMap
  1. 对应的非并发容器:HashMap
  2. 目标:代替HashtablesynchronizedMap,支持复合操作
  3. 原理:JDK6中采用一种更加细粒度的加锁机制Segment“分段锁JDK8中采用CAS无锁算法。
  • CopyOnWriteArrayList
  1. 对应的非并发容器:ArrayList
  2. 目标:代替VectorsynchronizedList
  3. 原理:利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。
  • CopyOnWriteArraySet
  1. 对应的费并发容器:HashSet
  2. 目标:代替synchronizedSet
  3. 原理:基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayListaddIfAbsent方法,其遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。
  • ConcurrentSkipListMap
  1. 对应的非并发容器:TreeMap
  2. 目标:代替synchronizedSortedMap(TreeMap)
  3. 原理:Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的。Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过空间来换取时间的一个算法。ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表。内部是SkipList(跳表)结构实现,在理论上能够在Ologn))时间内完成查找、插入、删除操作。
  • ConcurrentSkipListSet
  1. 对应的非并发容器:TreeSet
  2. 目标:代替synchronizedSortedSet
  3. 原理:内部基于ConcurrentSkipListMap实现
  • ConcurrentLinkedQueue
  1. 不会阻塞的队列
  2. 对应的非并发容器:Queue
  3. 原理:基于链表实现的FIFO队列(LinkedList的并发版本)
  • LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue
  1. 对应的非并发容器:BlockingQueue
  2. 特点:拓展了Queue,增加了可阻塞的插入和获取等操作
  3. 原理:通过ReentrantLock实现线程安全,通过Condition实现阻塞和唤醒
  4. 实现类:
  • LinkedBlockingQueue:基于链表实现的可阻塞的FIFO队列
  • ArrayBlockingQueue:基于数组实现的可阻塞的FIFO队列
  • PriorityBlockingQueue:按优先级排序的队列

 

下面以ConcurrentHashMap为例讲解并发包的数据结构和保证安全的方法

7.2:ConcurrentHashMap数据结构简介

1.8之前:                                                                        1.8及之后:加入了红黑树

    

7.3:ConcurrentHashMap同步原理

  • 1.7同步原理:主要继承RenntrateLock进行加锁,采用cas模式,及悲观锁的方式进行循环加锁,直至成功;
  • 1.8同步原理:对数组元素使用cas模式加锁,对数组后的元素使用synchornized关键字进行加锁
  • ConcurrentHashMap初始化:
//初始化方法是 initTable():该方法通过sizeCtl实现CAS初始化
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(); //放弃执行权
        // CAS 一下,将 sizeCtl 设置为 -1,代表抢到了锁
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    // DEFAULT_CAPACITY 默认初始容量是 16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 初始化数组,长度为 16 或初始化时提供的长度
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 将这个数组赋值给 table,table 是 volatile 的
                    table = tab = nt;
                    // 如果 n 为 16 的话,那么这里 sc = 12
                    // 其实就是 0.75 * n
                    sc = n - (n >>> 2);
                }
            } finally {
                // 设置 sizeCtl 为 sc,我们就当是 12 吧
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

 

  • ConcurrentHashMap扩容
//扩容方法是tryPresize:该方法通过sizeCtl实现CAS初始化
// 首先要说明的是,方法参数 size 传进来的时候就已经翻了倍了
private final void tryPresize(int size) {
    // c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
 
        // 这个 if 分支和之前说的初始化数组的代码基本上是一样的,在这里,我们可以不用管这块代码
        if (tab == null || (n = tab.length) == 0) {
            n = (sc > c) ? sc : c;
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if (table == tab) {
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        sc = n - (n >>> 2); // 0.75 * n
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        else if (tab == table) {
            int rs = resizeStamp(n);
 
            if (sc < 0) {
                Node<K,V>[] nt;
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                // 2. 用 CAS 将 sizeCtl 加 1,然后执行 transfer 方法
                //    此时 nextTab 不为 null
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 1. 将 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2)
            //  调用 transfer 方法,此时 nextTab 参数为 null
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}
  • ConcurrentHashMap数据迁移
//数据迁移方法是transfer:该方法通过CAS和synchronized关键字实现同步。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
 
    // stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16
    // stride 可以理解为”步长“,有 n 个位置是需要进行迁移的,
    //   将这 n 个任务分为多个任务包,每个任务包有 stride 个任务
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
 
    // 如果 nextTab 为 null,先进行一次初始化
    //    前面我们说了,外围会保证第一个发起迁移的线程调用此方法时,参数 nextTab 为 null
    //       之后参与迁移的线程调用此方法时,nextTab 不会为 null
    if (nextTab == null) {
        try {
            // 容量翻倍
            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 是 ConcurrentHashMap 中的属性
        nextTable = nextTab;
        // transferIndex 也是 ConcurrentHashMap 的属性,用于控制迁移的位置
        transferIndex = n;
    }
 
    int nextn = nextTab.length;
 
    // ForwardingNode 翻译过来就是正在被迁移的 Node
    // 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED
    // 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,
    //    就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了
    //    所以它其实相当于是一个标志。
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
 
 
    // advance 指的是做完了一个位置的迁移工作,可以准备做下一个位置的了
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
 
    /*
     * 下面这个 for 循环,最难理解的在前面,而要看懂它们,应该先看懂后面的,然后再倒回来看
     * 
     */
 
    // i 是位置索引,bound 是边界,注意是从后往前
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
 
        // 下面这个 while 真的是不好理解
        // advance 为 true 表示可以进行下一个位置的迁移了
        //   简单理解结局:i 指向了 transferIndex,bound 指向了 transferIndex-stride
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
 
            // 将 transferIndex 值赋给 nextIndex
            // 这里 transferIndex 一旦小于等于 0,说明原数组的所有位置都有相应的线程去处理了
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                // 看括号中的代码,nextBound 是这次迁移任务的边界,注意,是从后往前
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                // 所有的迁移操作已经完成
                nextTable = null;
                // 将新的 nextTab 赋值给 table 属性,完成迁移
                table = nextTab;
                // 重新计算 sizeCtl:n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75 倍
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
 
            // 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
            // 然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
            // 这里使用 CAS 操作对 sizeCtl 进行减 1,代表做完了属于自己的任务
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 任务结束,方法退出
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
 
                // 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
                // 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        // 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode ”空节点“
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 该位置处是一个 ForwardingNode,代表该位置已经迁移过了
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            // 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 头结点的 hash 大于 0,说明是链表的 Node 节点
                    if (fh >= 0) {
                        // 下面这一块和 Java7 中的 ConcurrentHashMap 迁移是差不多的,
                        // 需要将链表一分为二,
                        //   找到原链表中的 lastRun,然后 lastRun 及其之后的节点是一起进行迁移的
                        //   lastRun 之前的节点需要进行克隆,然后分到两个链表中
                        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);
                        }
                        // 其中的一个链表放在新数组的位置 i
                        setTabAt(nextTab, i, ln);
                        // 另一个链表放在新数组的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
                        //    其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
                        setTabAt(tab, i, fwd);
                        // advance 设置为 true,代表该位置已经迁移完毕
                        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;
                            }
                        }
                        // 如果一分为二后,节点数少于 8,那么将红黑树转换回链表
                        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;
 
                        // 将 ln 放置在新数组的位置 i
                        setTabAt(nextTab, i, ln);
                        // 将 hn 放置在新数组的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
                        //    其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
                        setTabAt(tab, i, fwd);
                        // advance 设置为 true,代表该位置已经迁移完毕
                        advance = true;
                    }
                }
            }
        }
    }
}

线程创建与线程通讯:https://blog.csdn.net/qq_38869493/article/details/105032702

线程池:https://blog.csdn.net/qq_38869493/article/details/105054955

视频地址:https://www.bilibili.com/video/BV1m4411u7xK

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值