java提高之JUC相关

JUC

1. JMM

什么是JMM

JMM(java内存模型Java Memory Model)本身是一种抽象的概念,描述的是一组规则或规范。通过这组规范定义了程序中各个变量的访问方式。

由于JVM运行程序的实体是线程,而每个线程创建JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而Java内存模型规定中的变量都存储在主内存。主内存是共享数据,所有线程都能访问,但线程对变量的操作(读写值)都必须在工作内存中完成。简单说,就是先读取,再操作,再写回。工作内存存放的是主内存中的副本,线程的通信都需要通过主内存来完成。

内存可见性问题:工作内存间互相不知道对方已经把数据进行了修改。

如何解决?

在这里插入图片描述

多线程什么情况下会触发安全问题?

  1. 当多个线程在操作同一个共享数据时,就有可能发生线程安全问题。

2. volatile

题目:谈谈你对volatile的理解

是什么?

轻量级的同步方案

  1. 可以解决内存可见性问题

  2. volatile关键不能解决原子性问题 (CAS来解决)

    原子性问题:要么都成功,要么都失败

  3. volatile禁止了重排序

什么是原子性操作

示例代码

public class T1 {
    int count=0;
    public  void add(){
        count++;
    }

    public static void main(String[] args) {
        new T1().add();
    }
}

将其class字节码文件用javap -c T1.class编译得到可读格式,摘取add()方法部分

public void add();
    Code:
       0: aload_0   //把对象的引用装载到操作数栈中
       1: dup		// 
       2: getfield      #2                // Field count:I  //获取指定类的实例域,并将其值压入栈顶(读)
       5: iconst_1  //int型常量值1进栈
       6: iadd     // 栈顶两int型数值相加,并且结果进栈 (改)
       7: putfield      #2                  // Field count:I  //为指定的类的实例域赋值(写)
      10: return  //当前方法返回void

其原子性操作包括三步,读、改、写。

什么是指令重排

是什么?有多种,聚焦编译器的

编译器在不改变运行结果的情况下,按照自己的喜好对指令进行重排,重排的效果是提高代码执行效率。

main(){
  // ...
  int i=10;
  int j=20;
  int flag=true;
  sop(i,j,flag);
  //...
}
//多线程模型重排序
class JmmExample{
  private boolean flag=false;
  int i=5;
  int j=5;
  //...
  
  read(){
    i=10;  //1
    j=20;  //2
    flag=true;  //3
  }
  // ...
  write(){
    while(flag){
      int temp = i*j;
    }
  }
}

代码解读:正常情况,read()方法,先执行1,再执行2,再执行3,write()方法temp为10*20=200;多线程情况,如果发生指令重排,read()方法先执行3,write()方法flag=true,temp=5*5=25

java针对这种情况,制定了一套规则,happen-before原则,保证多线程操作可见性,在这套规则下,默认不能指令重排。

这套规则,共有8个条件,常见的synchronized、volatile、lock操作顺序都属于happen-before

VolatileDemo

public class VolatileDemo {
    public static void main(String[] args) {
        Resource resource = new Resource();
        new Thread(resource).start();
        while(true){
            if(resource.isFlag()){
                System.out.println("OK...main is over");
                break;
            }
        }
    }
}
class Resource implements Runnable{

    private boolean flag = false;// 在此处加volatile关键词解决该问题

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
            setFlag(true);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

按理说,程序进入while循环会判断flag是否为true,线程先睡1s,然后true,打印,跳出循环。

但是,程序进入了死循环,没有打印,也没有退出

解决方案:

加volatile,private volatile boolean flag=false;

在这里插入图片描述

线程实现的三种方式

  • 继承thread
  • 实现Runnable
  • 实现Callable

实现Callable的好处?

  • 可以抛出异常
  • 任务执行后可返回值
  • 可以拿到一个Future对象,表示异步计算的结果。通过Future了解任务执行的情况。

3. CAS

volatile部分说cas可以解决原子性问题

什么是CAS?

compare and swap,比较并替换,如果这个值是它的期望值,就进行修改

CAS的底层原理是什么?

  1. CAS依赖于unsafe类,unsafe相当于一个后门,可以像操作C的指针一样直接操作内存,直接获得内存中的偏移量,并且用volatile保证获取到的值是最新值。确保执行效率非常高。
  2. 底层是做一个do while循环,源码部分getAndAddInt解读,
public final boolean compareAndSet(int expect, int update) {
  // this 自己,valueOffset内存偏移量 
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

内存偏移量如何得到的?AtomicInteger类的静态代码块中,unsafe类有个objectFieldOffset方法得到,里面有个值value是volatile修饰的,修改值的时候,一是能拿到内存中的最新值,二是拿到内存中的修改地址,就可以直接修改数据。

static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

private volatile int value;

先来段代码解释原子性问题带来的线程安全

public class CasDemo {
    public static void main(String[] args) {
        Resources2 mr2 = new Resources2();
        for (int i = 0; i < 4; i++) {
            new Thread(mr2).start();
        }
    }
}
class Resources2 implements Runnable{
    int count=0;
    public void run() {
       while(true){
           try {
               Thread.sleep(100);
               if(count>=100){
                   break;
               }
               System.out.println(Thread.currentThread().getName()+"...."+ ++count);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    }
}

如果给count加上volatile关键字也不行,因为++count分为三步走,volatile只能解决内存不可见。

如何解决呢?JUC包里面有个atomic系列,用的就是CAS

public class CasDemo {
    public static void main(String[] args) {
        Resources2 mr2 = new Resources2();
        for (int i = 0; i < 10; i++) {
            new Thread(mr2).start();
        }
    }
}
class Resources2 implements Runnable{
//    int count=0;
    AtomicInteger atomic = new AtomicInteger(0);
    public void run() {
           try {
               Thread.sleep(1000);
               System.out.println(Thread.currentThread().getName()+"...."+ atomic.getAndIncrement());
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
    }
}

这样就不会发生线程安全问题

getAndIncrement()方法,底层是

 public final int getAndIncrement() {
   // this 自己  valueOffset 内存偏移量 1 要修改的值 count++就是+1
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

getAndAddInt()方法,底层

// var1:this  var2:内存偏移量  var4:修改值    
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
          // 获得主内存中的数据-->在这个偏移量上对应的这个对象的volatile修饰的值
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  		// 模拟一个成功案例 var this var2 0x666  var4 1

        return var5;
    }

解读:var1是this,var2是内存偏移量 var4是修改值1 。首先获得var2内存偏移量上的var1对象所对应的主内存中的值var5,是5;拿到之后,在操作之前再用var1和var2抓取主内存的地址,两次值相同,没有被改过,安全,假设抓取的还是5,相等,因为CAS是一种系统原语级别的支持,判断相等后一定会执行将主内存的值加一的操作,将修改后的值写进去,5+1得到6,修改成功,为true,取反false,跳出循环;如果比较失败,false,取反true,继续进入循环,直到成功

注意:CAS是一种系统原语(硬件支持),整个过程是不允许被打断。

CAS的缺点

有可能自旋时间过长,对CPU造成极大的负担。

ABA问题

什么是ABA问题?

其他线程修改了值,让另一个线程觉得没有被修改,从而完成了cas操作,这就是ABA问题。

ABA问题出现在多线程或多进程计算环境中。

首先描述ABA。假设两个线程T1和T2访问同一个变量V,当T1访问变量V时,读取到V的值为A;此时线程T1被抢占了,T2开始执行,T2先将变量V的值从A变成了B,然后又将变量V从B变回了A;此时T1又抢占了主动权,继续执行,它发现变量V的值还是A,以为没有发生变化,所以就继续执行了。这个过程中,变量V从A变为B,再由B变为A就被形象地称为ABA问题了。

问题的主要还是因为“值是一样的”等同于“没有发生变化”的认知。

ABA问题的隐患

形象描述:你拿着一个装满钱的手提箱在飞机场,此时过来了一个火辣性感的美女,然后她很暖昧地挑逗着你,并趁你不注意的时候,把用一个一模一样的手提箱和你那装满钱的箱子调了个包,然后就离开了,你看到你的手提箱还在那,于是就提着手提箱去赶飞机去了。

如何解决

使用JDK的并发包中的AtomicStampedReference和 AtomicMarkableReference来解决。通过版本号(时间戳)来解决ABA问题,时间戳信息要做成自增的。

// 用int做时间戳

AtomicStampedReference<QNode> tail = new AtomicStampedReference<CompositeLock.QNode>(null, 0);

int[] currentStamp = new int[1];

// currentStamp中返回了时间戳信息

QNode tailNode = tail.get(currentStamp);

tail.compareAndSet(tailNode, null, currentStamp[0], currentStamp[0] + 1)

接下来看atomic底层,先上案例

public class CasDemo02 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,2019));//true

        System.out.println(atomicInteger.get());//2019

        System.out.println(atomicInteger.compareAndSet(5,4000));//false
        System.out.println(atomicInteger.get());//2019
    }
}

Unsafe类

是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地native方法来访问。

unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于sun.misc包,其内部方法可以像操作C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。

Unsafe类中的方法都是native修饰的,也就是说UNsafe类中的方法都直接调用了操作系统底层资源执行任务。

总结:使用了unsafe类,此类是通过内存的偏移地址,直接去操作这个变量,并且此变量又通过volatile来修饰,可获得内存中的最新值并直接修改。并且,CAS是一种系统原语(硬件支持),整个过程是不允许被打断。

有两种,方法栈和本地方法栈。本地方法栈支持对本地方法的调用,执行的非java代码。unsafe执行是在本地方法栈中执行。

在这里插入图片描述

4. 乐观锁

在这里插入图片描述

乐观锁:制定了一个版本号,每次操作这个数据,对应版本号+1,提交数据时,要比安全的版本号大一,否则提交失败。

悲观锁:

  • 读锁/共享锁:只要是读的线程都能获得这把锁–> 读时不会触发安全问题 lock in share mode
  • 写锁/排他锁:一个人持有锁,其他人都不能拿到锁。for update

lock和synchronized区别

  1. 原始组成:synchronized属于关键字,jvm层面;lock属于api级别的锁

  2. 使用方法:synchronized不需要手动释放;lock需要finally释放,可能导致死锁

  3. synchronized不可中断;lock可中断,更灵活

    • 方案1:trylock(long timeout,TimeUnit unit)
    • 方案2:lockInterpatibly 可中断
  4. synchronized是非公平锁;lock可以是公平锁,也可以是非公平锁,根据传入的参数决定

  5. 精确唤醒:synchronized无此方法;lock通过condition可以

public ReentrantLock() {
  sync = new NonfairSync();//默认非公平锁
}
public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}// 公平锁

公平锁:多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到

在并发情况中,会先查看锁维护的等待队列,如果为空,当前线程是等待队列中第一个就占有锁,否则就加入等待队列,等待线程召唤。

非公平锁:指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程优先获得锁,在高并发的情况下,有可能出现优先级反转或者饥饿现象

可重入锁

可重入锁又称为递归锁

指的是:同一线程外层函数获得锁之后,内层递归函数仍能获得该锁的代码

在同一线程在外层方法获得锁的时候,在进入内层方法会自动获得锁。

也就是说,线程可以进入任何一个它已经拥有锁的同步代码块。

自旋锁:cas就是一个自旋锁。

手写一个自旋锁

public class Demo02 {
  public static void main(String[] args) {
    MyResource mr = new MyResource();
    new Thread(mr,"线程A").start();
    new Thread(mr,"线程B").start();

  }
}

class MyResource implements Runnable{
  int count=0;
  MySpinLock lock = new MySpinLock();
  public void run() {
    while(true){
      lock.lock();
      try {
        Thread.sleep(100);
        if(count>=100){
          break;
        }
        System.out.println(Thread.currentThread().getName()+"..."+ ++count);
      } catch (Exception e) {
        e.printStackTrace();
      } finally {
        lock.unlock();//lock需要finally释放
      }
    }
  }
}

class MySpinLock{

  AtomicReference<Thread> reference = new AtomicReference<Thread>();
  public void lock(){
    Thread thread = Thread.currentThread();
    while(!reference.compareAndSet(null,thread)){

    }
  }
  public void unlock(){
    Thread thread = Thread.currentThread();
    reference.compareAndSet(thread,null);
  }
}

AtomicReference是针对对象的atomic。

A:a首先进入到lock方法,此时reference为null,把thread赋值给了reference,while循环跳出;

B:b再次进入lock方法,此时reference为threadA,修改失败,while为true,B线程一直在里面循环。

精确唤醒

题目:多线程之间调用A->B->C A打印5次,B打印10次,C打印15次,来10轮

public class LockNotifyDemo {
    public static void main(String[] args) {
        // 多线程之间调用A->B->C  A打印5次,B打印10次,C打印15次,来10轮
        MyResource01 mr = new MyResource01();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        mr.print5();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"A").start();
        }

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        mr.print10();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"B").start();
        }

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        mr.print15();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"C").start();
        }
    }
}
class MyResource01{
    Lock lock = new ReentrantLock();
    int count=1;
    Condition con1 = lock.newCondition();
    Condition con2 = lock.newCondition();
    Condition con3 = lock.newCondition();
    public void print5() throws InterruptedException {
        try {
            lock.lock();
            while(count!=1){
                con1.await();
            }
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName()+"..."+i);
            }
            count=2;
            con2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print10() throws InterruptedException {
        try {
            lock.lock();
            while(count!=2){
                con2.await();
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName()+"..."+i);
            }
            count=3;
            con3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15() throws InterruptedException {
        try {
            lock.lock();
            while(count!=3){
                con3.await();
            }
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName()+"..."+i);
            }
            count=1;
            con1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

object类有什么方法

toString notify notifyAll finalize equals hashCode wait

覆盖了hashMap的equals hashCode;多线程的notify notifyAll wait;垃圾回收机制的finalize;基本的toString。

5. 安全集合类

题目:我们都知道ArrayList是线程不安全的,请编写一个不安全的案例,并给出解决方案

其实写个多线程就好了,添加一些垃圾对象,哪些容易产生垃圾对象?字符串的拼接

字符串的拼接

代码

String str = "ab";
String str2 = "abc";
String s3 = str + "c";
System.out.println(str2 == s3);

解读:二者不相等,为什么?

字符串串联("+")是通过 StringBuilder(或 StringBuffer)类及其 append 方法实现的。字符串转换是通过 toString 方法实现的

String01

如图,str和str2都在常量池中,str+“c”,添加是通过StringBuilder类的append,底层1.8是char数组,1.9底层为byte数组,得到"abc",字符串转换是通过toString实现,如下,底层为new String,因此,str2是在常量池,而s3是在堆中。

public String toString(){
  return new String(this.value,o,this.count);
}

arrayList不安全的案例

public class ArrayListDemo {
    public static void main(String[] args) {
        final ArrayList<String> arrayList = new ArrayList<String>();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    arrayList.add((UUID.randomUUID()+"").substring(0,8));
                    System.out.println(arrayList);
                }
            }).start();
        }
    }
}

报错,并发修改异常。

解决方法

  1. synchronized上锁
  2. 用vector替代ArrayList
  3. Collections.synchronizedList(arrayList),将其转变为线程安全的集合
  4. CopyOnWriteArrayList
public class ArrayListDemo {
    public static void main(String[] args) {
        final List<String> arrayList = new CopyOnWriteArrayList<String>();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    arrayList.add((UUID.randomUUID()+"").substring(0,8));
                    System.out.println(arrayList);
                }
            }).start();
        }
    }
}

结果为:

[bd7e96e3, 6674da69, 6fa727e0]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252, fe123edd]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252, fe123edd, a93c684b]
[bd7e96e3, 6674da69, 6fa727e0]
[bd7e96e3, 6674da69, 6fa727e0, 99396440]
[bd7e96e3, 6674da69, 6fa727e0]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252, fe123edd, a93c684b, 6411c994, ab8117ec, 48c7d831]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252, fe123edd, a93c684b, 6411c994, ab8117ec]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252, fe123edd, a93c684b, 6411c994]

原因:只有集合是线程安全的,而线程本身不是线程安全的。

6. ConcurrentHashMap

1.7 底层用的是分段锁segment,1.8利用CAS+synchronized来保证并发安全,底层用数组+链表+红黑树的存储结构。

首先看put方法,最有可能发生线程安全的就是put,看如何解决

public V put(K key, V value) {
  return putVal(key, value, false);
}

同HashMap一样,也是putVal

final V putVal(K key, V value, boolean onlyIfAbsent) {
  // key和value不能为null,和HashMap的区别
  if (key == null || value == null) throw new NullPointerException();
  // hash值的计算
  int hash = spread(key.hashCode());
  int binCount = 0;
  // table就是链表+数组
  for (Node<K,V>[] tab = table;;) {
    Node<K,V> f; int n, i, fh;
    if (tab == null || (n = tab.length) == 0)
      // 初始化,易发生线程安全问题
      tab = initTable();
    // (n-1)&hash计算hash值落在数组的索引位置
    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
    }
    else if ((fh = f.hash) == MOVED)
      tab = helpTransfer(tab, f);
    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;
}

spread计算key的hash值,HASH_BITS为0x7ffffff,32bit的最大值,保证hash>=0

static final int spread(int h) {
  return (h ^ (h >>> 16)) & HASH_BITS;
}

初始化如何保证线程安全?

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(); // lost initialization race; just spin
    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")
          Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
          table = tab = nt;
          sc = n - (n >>> 2);
        }
      } finally {
        sizeCtl = sc;
      }
      break;
    }
  }
  return tab;
}

解读:多个线程都满足(tab = table) == null || tab.length == 0条件,进入循环。sc初始化为0,而sizeCtl private transient volatile int sizeCtl;初始化也是0,volatile也确保sizeCtl一定是内存中最新值。第一个进来的线程会进入else if逻辑。U 就是Unsafe类,CAS的核心类,compareAndSwapInt(this, SIZECTL, sc, -1) ,this就是table本身,SIZECTL和sc比较,如果相同,修改sc-1,得到sc为-1。没有进来的线程会继续循环,走了if部分的逻辑。Thread.yield()放手,释放CPU的执行权,不再初始化。继续看else if部分的代码,判断未初始化后,设置默认容量为16。new底层的node数组,也就是table。紧接着sc = n - (n >>> 2) sc就是16-4得到12,最后再赋值给sizeCtl,初始化完毕。

阈值的计算:在HashMap中是容量*装载因子,在ConcurrentHashMap直接用n减去n右移两位算出,更快。

如何知道SIZECTL是内存偏移量呢?底层private static final long SIZECTL;,静态代码块中SIZECTL = U.objectFieldOffset(xxx);,拿到的就是内存偏移量!

初始化完毕后put方法走else if ((f = tabAt(tab, i = (n - 1) & hash)) == null)逻辑,tabAt是做什么呢?

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);
}

tabAt调用的是unsafe类的volatile,由于多线程,必须确保内存中的最新值。假设i=3,发现3号位置最新值为null,进行放值。放值的时候可能也会发生线程安全,又用casTabAt看第一次为null,放值的时候再检查i还是null,将该值放到i位置。

如果table的索引i位置有值,走else逻辑,用synchronized,锁对象是f,就是索引i对应的区域,锁细化,只锁住这块区域,确保线程安全。然后对这个索引位置的链表进行遍历,和HashMap一样,如果遍历走到最后,尾插。

扩容:多线程一起扩容。

  1. NCPU java类和CPU打交道的类–>获取CPU的核数–>以及性能如何

扩容resizing会先初始化transfer,里面就有NCPU,如何划分每个CPU扩容区域?用stride步伐。默认最小步伐为16。

猜你喜欢

转载自blog.csdn.net/wjl31802/article/details/91361030