并发编程基础_05

举报
kwan的解忧杂货铺 发表于 2024/08/09 00:47:45 2024/08/09
【摘要】 1.CPU 密集型CPU 密集型:CPU 密集型也叫计算密集型,指的是系统的硬盘、内存性能相对 CPU 要好很多,此时,系统运作大部分的状况是 CPU Loading 100%,CPU 要读/写 I/O(硬盘/内存),I/O 在很短的时间就可以完成,而 CPU 还有许多运算要处理,CPU Loading 很高。 2.I/O 密集型I/O 密集型:IO 密集型指的是系统的 CPU 性能相对硬...

1.CPU 密集型

CPU 密集型:CPU 密集型也叫计算密集型,指的是系统的硬盘、内存性能相对 CPU 要好很多,此时,系统运作大部分的状况是 CPU Loading 100%,CPU 要读/写 I/O(硬盘/内存),I/O 在很短的时间就可以完成,而 CPU 还有许多运算要处理,CPU Loading 很高。

2.I/O 密集型

I/O 密集型:IO 密集型指的是系统的 CPU 性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是 CPU 在等 I/O (硬盘/内存) 的读/写操作,此时 CPU Loading 并不高。

3.线程池与密集型关系

线程池与 CPU 密集型的关系:

一般情况下,CPU 核心数 == 最大同时执行线程数.在这种情况下(设 CPU 核心数为 n),大量客户端会发送请求到服务器,但是服务器最多只能同时执行 n 个线程.设线程池工作队列长度为 m,且 m>>n,则此时会导致 CPU 频繁切换线程来执行(如果 CPU 使用的是 FCFS,则不会频繁切换,如使用的是其他 CPU 调度算法,如时间片轮转法,最短时间优先,则可能会导致频繁的线程切换).所以这种情况下,无需设置过大的线程池工作队列,(工作队列长度 = CPU 核心数 || CPU 核心数+1) 即可.

与 I/O 密集型的关系:

1 个线程对应 1 个方法栈,线程的生命周期与方法栈相同.比如某个线程的方法栈对应的入站顺序为:controller()->service()->DAO(),由于 DAO 长时间的 I/O 操作,导致该线程一直处于工作队列,但它又不占用 CPU,则此时有 1 个 CPU 是处于空闲状态的.所以,这种情况下,应该加大线程池工作队列的长度(如果 CPU 调度算法使用的是 FCFS,则无法切换),尽量不让 CPU 空闲下来,提高 CPU 利用率

4.如何设置核心线程数和最大线程数?

  • 需要进行压测

  • 并发访问量是多大

  • 不要用无界队列,且有界队列的最大值要合理

  • 充分利用 cpu

    • 一个线程处理计算型,100%
    • 50%计算型,需要 2 个线程
    • 25%计算型,需要 4 个线程
    • 多任务操作系统,对 CPU 都是分时使用的:比如 A 任务占用 10ms,然后 B 任务占用 30ms,然后空闲 60ms,再又是 A 任务占 10ms, B 任务占 30ms,空闲 60ms;如果在一段时间内都是如此,那么这段时间内的利用率为 40%,因为整个系统中只有 40%的时间是 CPU 处理数据的时间。
  • 任务的性质:CPU 密集型任务、lO 密集型任务和混合型任务。

    • CPU 密集型要设置尽量少的线程数
    • IO 密集型要设置尽量多的线程数
  • 任务的优先级:高、中和低。

    • 使用优先队列
    • 建议使用有界队列,且数量合理
  • 任务的执行时间:

  • 任务的依赖性:

    • 是否依赖其他系统资源,如数据库连接。
    • CPU 空闲时间越长,线程数应该设置的越大,更好的利用 CPU

5.CPU 飙升 100%?

发现程序 CPU 飙升 100%,内存和 I/O 利用正常,是什么原因?如何排查?

原因:死锁

排查: dump 线程数据

image-20220414183245074

6.什么情况下单线程比多线程快?

redis 是单线程的,redis 为什么快?

首先分配 cpu 资源的单位是进程。一个进程所获得到的 cpu 资源是一定的。程序在执行的过程中消耗的是 cpu,比如一个单核 cpu,多个线程同时执行工作时,需要不断切换执行(上下文切换),单个线程时间耗费更多了,而单线程只是一个线程跑。

先来解释一下什么是上下文切换。在多任务处理系统中,作业数通常大于 CPU 数。为了让用户觉得这些任务在同时进行,CPU 给每个任务分配一定时间,把当前任务状态保存下来,当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。之后 CPU 可以回过头再处理之前被挂起任务。上下文切换就是这样一个过程,它允许 CPU 记录并恢复各种正在运行程序的状态,使它能够完成切换操作。在这个过程中,CPU 会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。

总结:线程切换是有开销的,这会导致程序运行变慢。所以单线程比多线程的运行速度更快。

7.伪共享内存顺序冲突?

什么是伪共享内存顺序冲突?如何避免?

由于存放到 CPU 缓存行的是内存块而不是单个变量,所以可能会把多个变量存放到同一个缓存行中,当多个线程同时修改这个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,此时有两个线程同时修改同一个缓存行下的两个不同的变量,这就是伪共享,也称内存顺序冲突。当出现伪共享时,CPU 必须清空流水线,会造成 CPU 比较大的开销。

如何避免:JDK1.8 之前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中,例如如下代码:

public final static class FilledLong{
  public volatile long value=0L;
  public long pl,p2,p3,p4,p5,p6;
}

假如缓存行为 64 字节,那么我们在 FilledLong 类里填充了 6 个 long 类型的变量,一个 long 类型变量占用 8 字节,加上自己的 value 变量占用的 8 个字节,总共 56 字节.另外,这里 FilledLong 是一个类对象,而类对象的字节码的对象头占用 8 字节,所以一个 FilledLong 对象实际会占用 64 字节的内存,这正好可以放入同一个缓存行。

JDK 提供了 sun.misc Contended 注解,用来解决伪共享问题.将上面代码修改为如下。

@sun.misc.Contended
  public final static class FilledLong{
    public volatile longvalue=0L;
  }

特别注意
在默认情况下,@Contended 注解只用于 Java 核心类,比如 rt 包下的类。如果用户类路径下的类需要使用这个注解,需要添加 JVM 参数:- XX:-RestrictContended.填充的宽度默认为 128,要自定义填充宽度则可以通过参数-XX:ContendedPaddingWidth 参数进行设置。

8.双重检查锁的单例模式

public class Juc_book_fang_11_Dcl {
  private static volatile Person instance;
  public static Person getInstance(){
    if (instance == null){//步骤一
      synchronized (Juc_book_fang_11_Dcl.class){//步骤二
        if (instance == null){//步骤三
          instance = new Person();//步骤四
        }
        return instance;
      }
    }
    return instance;
  }
}

看着图中的注释,假设线程 A 执行 getInstance 方法

步骤一: instance 为 null,则进入 if 判断;

步骤二:获取 synchronized 锁,成功,进入同步代码块;

步骤三:继续判断 instance,为 null 则进入 if 判断;

步骤四: instance = new Instance().看似是一句代码,其实是三句代码。

memory=allocate(); //1:分配对象的内存空间
ctorInstance(memory);//2:初始化对象
instance=memory; //3:设置instance指向刚分配的内存地址

上面 2 和 3 两者没有依赖关系,设置 instance 指向刚分配的内存地址和初始化对象会存在重排序.

使用 volatile 并不会解决 2 和 3 的重排序问题,因为 2 和 3 都在一个 new指令里面,内存屏障是针对指令级别的重排序,双重检查锁 volatile 禁止重排序的原理,new 指令是单一指令,也就是前面加 StoreStore 屏障,后面加 StoreLoad 屏障,后面的线程必不会读到 instance 为 null

有 2 种解决方案:

  • 使用 volatile 禁止重排序,原理还是其他线程不可见

  • 允许 2 和 3 重排序,但是不允许其他线程可见

    • 基于类初始化
    • CLASS 对象的初始化锁只能有一个线程访问,对其他线程不可见

基于类的初始化:

public class InstanceFactory {
  private static class InstanceHolder {
    public static Instance instance=new Instance();
  }
  public static Instance getinstance() {
    return InstanceHolder.instance; // 这里将导致InstanceHolder类被初始化
  }
}

java 中,一个类或接口类型 T 将被立即初始化的情况如下

  1. T 是一个类,而且一个 T 类型的实例被创建。

  2. T 是一个类,且 T 中声明的一个静态方法被调用。

  3. T 中声明的一个静态字段被赋值。

  4. T 中声明的一个静态字段被使用,而且这个字段不是一个常量字段。

  5. T 是一个顶级类(TopLevelClass,见 Java 语言规范的§7.6),而且一个断言语句嵌套在 T 内部被执行。

    在示例代码中,首次执行 getlnstance)方法的线程将导致 InstanceHolder 类被初始化(符合情况 4)。

9.long 和 double 的非原子性协定

Java 内存模型要求 lock、unlock、read、load、assign、use、store、write 这 8 个操作都具有原子性,但是对于 64 位的数据类型(double、long)定义了相对宽松的规定:允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次的 32 位操作来进行,即允许虚拟机可以不保证 64 位数据类型的 load、store、read 和 write 操作的原子性。

非原子性协定可能导致的问题:
如果有多个线程共享一个未申明为 volatile 的 long 或 double 类型的变量,并且同时对其进行读取和修改操作,就有可能会有线程读取到"半个变量"的数值或者是一半正确一半错误的失效数据。

在实际应用中的解决:
因为上述可能造成的问题,势必在对 long 和 double 类型变量操作时要加上 volatile 关键字,实际上如下:

1、64 位的 java 虚拟机不存在这个问题,可以操作 64 位的数据

2、目前商用 JVM 基本上都会将 64 位数据的操作作为原子操作实现

所以我们编写代码时一般不需要将 long 和 double 变量专门申明为 volatile

10.CompletableFuture 使用

List<CompletableFuture<UserMessage>> futures = new ArrayList<>();
for (int i = 0; i < 1; i++) {
    CompletableFuture<UserMessage> future = CompletableFuture.supplyAsync(() -> this.submitAnswerByTitle(id, title));
    futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
    .thenAccept(v -> {
        for (CompletableFuture<UserMessage> future : futures) {
            try {
                UserMessage userMessage = future.get();
                // 处理任务的返回结果
                // ...
            } catch (InterruptedException | ExecutionException e) {
                // 处理异常
                // ...
            }
        }
    }).join();
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。