跨境互联网 跨境互联网
首页
  • AI 工具

    • 绘图提示词工具 (opens new window)
    • ChatGPT 指令 (opens new window)
  • ChatGPT

    • ChatGP T介绍
    • ChatGPT API 中文开发手册
    • ChatGPT 中文调教指南
    • ChatGPT 开源项目
  • Midjourney

    • Midjourney 文档
  • Stable Diffusion

    • Stable Diffusion 文档
  • 其他

    • AIGC 热门文章
    • 账号合租 (opens new window)
    • 有趣的网站
  • Vue

    • Vue3前置
  • JAVA基础

    • Stream
    • Git
    • Maven
    • 常用第三方类库
    • 性能调优工具
    • UML系统建模
    • 领域驱动设计
    • 敏捷开发
    • Java 测试
    • 代码规范及工具
    • Groovy 编程
  • 并发编程&多线程

    • 并发编程
    • 高性能队列 Disruptor
    • 多线程并发在电商系统下的应用
  • 其他

    • 面试题
  • 消息中间中间件

    • Kafka
    • RabbitMQ
    • RocketMQ
  • 任务调度

    • Quartz
    • XXL-Job
    • Elastic-Job
  • 源码解析

    • Mybatis 高级使用
    • Mybatis 源码剖析
    • Mybatis-Plus
    • Spring Data JPA
    • Spring 高级使用
    • Spring 源码剖析
    • SpringBoot 高级使用
    • SpringBoot 源码剖析
    • Jdk 解析
    • Tomcat 架构设计&源码剖析
    • Tomcat Web应用服务器
    • Zookeeper 高级
    • Netty
  • 微服务框架

    • 分布式原理
    • 分布式集群架构场景化解决方案
    • Dubbo 高级使用
    • Dubbo 核心源码剖析
    • Spring Cloud Gateway
    • Nacos 实战应用
    • Sentinel 实战应用
    • Seata 分布式事务
  • 数据结构和算法的深入应用
  • 存储

    • 图和Neo4j
    • MongoDB
    • TiDB
    • MySQL 优化
    • MySQL 平滑扩容实战
    • MySQL 海量数据存储与优化
    • Elasticsearch
  • 缓存

    • Redis
    • Aerospike
    • Guava Cache
    • Tair
  • 文件存储

    • 阿里云 OSS 云存储
    • FastDF 文件存储
  • 基础

    • Linux 使用
    • Nginx 使用与配置
    • OpenResty 使用
    • LVS+Keepalived 高可用部署
    • Jekins
  • 容器技术

    • Docker
    • K8S
    • K8S
  • 01.全链路(APM)
  • 02.电商终极搜索解决方案
  • 03.电商亿级数据库设计
  • 04.大屏实时计算
  • 05.分库分表的深入实战
  • 06.多维系统下单点登录
  • 07.多服务之间分布式事务
  • 08.业务幂等性技术架构体系
  • 09.高并发下的12306优化
  • 10.每秒100W请求的秒杀架构体系
  • 11.集中化日志管理平台的应用
  • 12.数据中台配置中心
  • 13.每天千万级订单的生成背后痛点及技术突破
  • 14.红包雨的架构设计及源码实现
  • 人工智能

    • Python 笔记
    • Python 工具库
    • 人工智能(AI) 笔记
    • 人工智能(AI) 项目笔记
  • 大数据

    • Flink流处理框架
  • 加密区

    • 机器学习(ML) (opens new window)
    • 深度学习(DL) (opens new window)
    • 自然语言处理(NLP) (opens new window)
AI 导航 (opens new window)

Revin

首页
  • AI 工具

    • 绘图提示词工具 (opens new window)
    • ChatGPT 指令 (opens new window)
  • ChatGPT

    • ChatGP T介绍
    • ChatGPT API 中文开发手册
    • ChatGPT 中文调教指南
    • ChatGPT 开源项目
  • Midjourney

    • Midjourney 文档
  • Stable Diffusion

    • Stable Diffusion 文档
  • 其他

    • AIGC 热门文章
    • 账号合租 (opens new window)
    • 有趣的网站
  • Vue

    • Vue3前置
  • JAVA基础

    • Stream
    • Git
    • Maven
    • 常用第三方类库
    • 性能调优工具
    • UML系统建模
    • 领域驱动设计
    • 敏捷开发
    • Java 测试
    • 代码规范及工具
    • Groovy 编程
  • 并发编程&多线程

    • 并发编程
    • 高性能队列 Disruptor
    • 多线程并发在电商系统下的应用
  • 其他

    • 面试题
  • 消息中间中间件

    • Kafka
    • RabbitMQ
    • RocketMQ
  • 任务调度

    • Quartz
    • XXL-Job
    • Elastic-Job
  • 源码解析

    • Mybatis 高级使用
    • Mybatis 源码剖析
    • Mybatis-Plus
    • Spring Data JPA
    • Spring 高级使用
    • Spring 源码剖析
    • SpringBoot 高级使用
    • SpringBoot 源码剖析
    • Jdk 解析
    • Tomcat 架构设计&源码剖析
    • Tomcat Web应用服务器
    • Zookeeper 高级
    • Netty
  • 微服务框架

    • 分布式原理
    • 分布式集群架构场景化解决方案
    • Dubbo 高级使用
    • Dubbo 核心源码剖析
    • Spring Cloud Gateway
    • Nacos 实战应用
    • Sentinel 实战应用
    • Seata 分布式事务
  • 数据结构和算法的深入应用
  • 存储

    • 图和Neo4j
    • MongoDB
    • TiDB
    • MySQL 优化
    • MySQL 平滑扩容实战
    • MySQL 海量数据存储与优化
    • Elasticsearch
  • 缓存

    • Redis
    • Aerospike
    • Guava Cache
    • Tair
  • 文件存储

    • 阿里云 OSS 云存储
    • FastDF 文件存储
  • 基础

    • Linux 使用
    • Nginx 使用与配置
    • OpenResty 使用
    • LVS+Keepalived 高可用部署
    • Jekins
  • 容器技术

    • Docker
    • K8S
    • K8S
  • 01.全链路(APM)
  • 02.电商终极搜索解决方案
  • 03.电商亿级数据库设计
  • 04.大屏实时计算
  • 05.分库分表的深入实战
  • 06.多维系统下单点登录
  • 07.多服务之间分布式事务
  • 08.业务幂等性技术架构体系
  • 09.高并发下的12306优化
  • 10.每秒100W请求的秒杀架构体系
  • 11.集中化日志管理平台的应用
  • 12.数据中台配置中心
  • 13.每天千万级订单的生成背后痛点及技术突破
  • 14.红包雨的架构设计及源码实现
  • 人工智能

    • Python 笔记
    • Python 工具库
    • 人工智能(AI) 笔记
    • 人工智能(AI) 项目笔记
  • 大数据

    • Flink流处理框架
  • 加密区

    • 机器学习(ML) (opens new window)
    • 深度学习(DL) (opens new window)
    • 自然语言处理(NLP) (opens new window)
AI 导航 (opens new window)
  • 基础

  • 设计模式

  • 并发编程

    • 并发编程

    • 多线程并发在电商系统下的应用

      • 多线程 J.U.C
      • 并发深入
      • 性能调优
        • 1 锁优化
          • 1.1 Synchronized优化
          • 1.2 Lock 锁优化
          • 1.3 CAS乐观锁优化
          • 1.3.1 案例一:直接加synchronized锁
          • 1.3.2 案例二:基于CAS思想,compare再set
          • 1.4 一些经验
          • 减少锁的时间
          • 减少锁的粒度
          • 锁的粒度
          • 减少加减锁的次数
          • 使用读写锁
          • 善用volatile
        • 2 线程池参数调优
          • 2.1 代码调试
          • 2.2 Executors剖析
          • 2.2.1 newCachedThreadPool
          • 2.2.2 newFixedThreadPool
          • 2.2.3 newSingleThreadExecutor
          • 2.2.4 newScheduledThreadPool
          • 2.3 一些经验
          • 2.3.1 corePoolSize
          • 2.3.2 workQueue
          • 2.3.3 maximumPoolSize
          • 2.3.4 keepaliveTime
          • 2.3.5 threadFactory(自定义展示实例)
          • 2.3.6 handler
        • 3 协程
          • 3.1 概念
          • 3.2 使用方式
        • 4 并发容器选择
          • 4.1 案例一:电商网站中记录一次活动下各个商品售卖的数量。
          • 4.2 案例二:在一次活动下,为每个用户记录浏览商品的历史和次数。
          • 4.3 案例三:在活动中,创建一个用户列表,记录冻结的用户。一旦冻结,不允许再下单抢购,但是可以浏览。
        • 5 上下文切换优化
          • 5.1 基本操作
          • 5.2 竞争锁
          • 5.3 wait/notify
          • 5.3.1 过时通知
          • 5.3.2 额外唤醒
          • 5.4 线程池
          • 5.5 虚拟机
          • 5.6 协程
      • 电商实际应用
    • 高性能队列 Disruptor
    • 资料
  • JVM与性能调优

  • 字节码增强技术

  • java
  • 并发编程
  • 多线程并发在电商系统下的应用
Revin
2023-07-17
目录

性能调优

# 1 锁优化

# 1.1 Synchronized优化

synchronized使用起来非常简单,但是需要注意的是synchronized加锁的是什么维度

对象级别:

public synchronized void test(){
    // TODO
}
 
public void test(){
    synchronized (this) {
        // TODO
    }
}
1
2
3
4
5
6
7
8
9

类级别:

public static synchronized void test(){
    // TODO
}
 
public void test(){
    synchronized (TestSynchronized.class) {
        // TODO
    }
}
1
2
3
4
5
6
7
8
9

两个的区别:

对象级别:

BadSync sync = new BadSync();

锁的是sync

类级别:

锁的是TestSynchronized.class,不管你new多少个BadSync,都是一把锁

如下案例理解

案例:互斥锁用不好可能会失效,看一个典型的锁不住现

package com.itheima;
 
public class ObjectLock {
    public static Integer i=0;
    public void inc(){
        synchronized (this){
            int j=i;
            try {
                Thread.sleep(100);
                j++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            i=j;
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                  //重点!
                    new ObjectLock().inc();
                }
            }).start();
        }
        Thread.sleep(3000);
        //理论上10才对。可是....
        System.out.println(ObjectLock.i);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

结果分析:每个线程内都是new对象,所以this不是同一把锁,结果锁不住,输出1

this,换成static的 i 变量试试?

换成ObjectLock.class 试试? 可以

换成String.class 可以

去掉synchronized块,外部方法上加 static synchronized

案例:看一个加锁粒度的案例

  package com.itheima.thread.opt;
 
import java.util.concurrent.atomic.AtomicLong;
 
public class BadSync implements Runnable{
 
    long start = System.currentTimeMillis();
    AtomicLong atomicLong = new AtomicLong(0);
    volatile int i=0;
    public  void inc(){
        i++;
    }
 
    @Override
    public synchronized void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        inc();
        atomicLong.getAndAdd(System.currentTimeMillis() ‐ start);
    }
 
 
    public static void main(String[] args) throws InterruptedException {
        BadSync sync = new BadSync();
 
        for (int i = 0; i < 5; i++) {
            new Thread(sync).start();
        }
 
        Thread.sleep(3000);
 
        System.out.println("最终计数:i="+ sync.i);
        System.out.println("最终耗时:time="+sync.atomicLong.get());
 
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  • 看一下最后的结果和耗时
最终计数:i=5
最终耗时:time=1519
1
2
  • 将synchronized换到inc方法上,再试试最后的结果和耗时
最终计数:i=5
最终耗时:time=555
1
2

为什么上面的这个计数没有混乱,原因是因为对象实例是在多线程之前实例的,所以都是用的同一个对象,所以锁可以锁住。

结论是什么?

# 1.2 Lock 锁优化

看一个小需求:电商系统中记录首页被用户浏览的次数,以及最后一次操作的时间(含读或写)。

package com.itheima;
 
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
 
public class TotalLock {
    //类创建的时间
    final long start = System.currentTimeMillis();
    //总耗时
    AtomicLong totalTime = new AtomicLong(0);
    //缓存变量
    private Map<String,Long> map = new HashMap(){{put("count",0L);}};
    ReentrantLock lock = new ReentrantLock();
 
    //查看map被写入了多少次
    public Map read(){
        lock.lock();
        try {
            Thread.currentThread().sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        //最后操作完成的时间
        map.put("time",end);
        lock.unlock();
        System.out.println(Thread.currentThread().getName()+",read="+(end‐start));
        totalTime.addAndGet(end ‐ start);
        return map;
    }
    //写入
    public Map write(){
        lock.lock();
        try {
            Thread.currentThread().sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //写入计数
        map.put("count",map.get("count")+1);
        long end = System.currentTimeMillis();
        map.put("time",end);
        lock.unlock();
        System.out.println(Thread.currentThread().getName()+",write="+(end‐start));
        totalTime.addAndGet(end ‐ start);
        return map;
    }
 
 
    public static void main(String[] args) throws InterruptedException {
       TotalLock count = new TotalLock();
 
        //读
        for (int i = 0; i < 4; i++) {
            new Thread(()‐>{
                count.read();
            }).start();
        }
        //写
        for (int i = 0; i < 1; i++) {
            new Thread(()‐>{
                count.write();
            }).start();
        }
 
        Thread.sleep(3000);
        System.out.println(count.map);
        System.out.println("读写总共耗时:"+count.totalTime.get());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

结果

Thread-0,read=332
Thread-1,read=436
Thread-2,read=559
Thread-3,read=660
Thread-4,read=760
{count=1,time=1596025770578}
读写总共耗时:2747
1
2
3
4
5
6
7

仔细看读的时间变化和执行的总时间,思考一下,从业务和技术角度有没有可优化空间?

仔细分析业务:查看次数这里其实是可以并行读取的,我们关注的业务是写入次数,也就是count,至于读取发生的时间time的写入操作,只是一个单步put,每次覆盖,不需要原子性保障,对这个加互斥锁没有必要。

改成读写锁试试......

读写锁,读读是并行的,写是互斥的,当写的时候会被保护起来,让读进行阻塞。

package com.itheima;
 
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
public class ReadAndWrite {
    //类创建的时间
    final long start = System.currentTimeMillis();
    //总耗时
    AtomicLong totalTime = new AtomicLong(0);
    //缓存变量,注意!因为read并发,这里换成ConcurrentHashMap
    private Map<String,Long> map = new ConcurrentHashMap(){{put("count",0L);}};
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
 
    //查看map被写入了多少次
    public Map read(){
        lock.readLock().lock();
        try {
            Thread.currentThread().sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        //最后操作完成的时间
        map.put("time",end);
        lock.readLock().unlock();
        System.out.println(Thread.currentThread().getName()+",read="+(end‐start));
        totalTime.addAndGet(end ‐ start);
        return map;
    }
    //写入
    public Map write(){
        lock.writeLock().lock();
        try {
            Thread.currentThread().sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //写入计数
        map.put("count",map.get("count")+1);
        long end = System.currentTimeMillis();
        map.put("time",end);
        lock.writeLock().unlock();
        System.out.println(Thread.currentThread().getName()+",write="+(end‐start));
        totalTime.addAndGet(end ‐ start);
        return map;
    }
 
 
    public static void main(String[] args) throws InterruptedException {
 ReadAndWrite rw = new ReadAndWrite();
 
        //读
        for (int i = 0; i < 4; i++) {
            new Thread(()‐>{
                rw.read();
            }).start();
        }
        //写
        for (int i = 0; i < 1; i++) {
            new Thread(()‐>{
                rw.write();
            }).start();
        }
 
        Thread.sleep(3000);
        System.out.println(rw.map);
        System.out.println("读写总共耗时:"+rw.totalTime.get());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

再来看读的时间变化和总执行时间。

结果

Thread-0,read=269
Thread-1,read=370
Thread-2,read=470
Thread-3,read=571
Thread-4,read=671
{count=1,time=1596026177303}
读写总共耗时:2351
1
2
3
4
5
6
7

当read远大于write时,这个差距会更明显(改成9:1试试……),测试差了一倍

# 1.3 CAS乐观锁优化

回顾上面的计数器,我们用synchronized实现了准确计数,本节我们看执行时间,追究性能问题。

# 1.3.1 案例一:直接加synchronized锁

package com.itheima;
public class NormalSync implements Runnable{
    Long start = System.currentTimeMillis();
    int i=0;
    public synchronized void run() {
        int j = i;
        //实际业务中可能会有一堆的耗时操作,这里等待100ms模拟
        try {
            //做一系列操作
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //业务结束后,增加计数
        i = j+1;
        System.out.println(Thread.currentThread().getId()+
                " ok,time="+(System.currentTimeMillis() ‐ start));
    }
    public static void main(String[] args) throws InterruptedException {
        NormalSync test = new NormalSync();
        new Thread(test).start();
        new Thread(test).start();
        Thread.currentThread().sleep(1000);
        System.out.println("last value="+test.i);
    }
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

线程二最终耗时会在200ms+,总耗时300ms,原因是悲观锁卡在了read后的耗时操作上,但是保证了最终结果是 2

11 ok,time=102
12 ok,time=203
last value=2
1
2
3

# 1.3.2 案例二:基于CAS思想,compare再set

package com.itheima;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class CasSync implements Runnable{
    long start = System.currentTimeMillis();
    int i=0;
    public void run() {
        int j = i;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //CAS处理,在这里理解思想,实际中不推荐大家使用!
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            long offset = unsafe.objectFieldOffset(CasSync.class.getDeclaredField("i"));
            while (!unsafe.compareAndSwapInt(this,offset,j,j+1)){
                j = i;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //实际开发中,要用atomic包,或者while+synchronized自旋
//        synchronized (this){
//            //注意这里!
//            while (j != i){
//                j = i;
//            }
//            i = j+1;
//        }
        System.out.println(Thread.currentThread().getName()+
                " ok,time="+(System.currentTimeMillis() ‐ start));
    }
    public static void main(String[] args) throws InterruptedException {
        CasSync test = new CasSync();
        new Thread(test).start();
        new Thread(test).start();
        Thread.currentThread().sleep(1000);
        System.out.println("last value="+test.i);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

线程一、二均在100ms+,总耗时200ms,最终结果还是2

# 1.4 一些经验

  • # 减少锁的时间

不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放

  • # 减少锁的粒度

将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争,典型如分段锁

  • # 锁的粒度

拆锁的粒度不能无限拆,最多可以将一个锁拆为当前cup数量相等

  • # 减少加减锁的次数

假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都要加锁

  • # 使用读写锁

业务细分,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写,参考计数器案例

  • # 善用volatile

volatile的控制比synchronized更轻量化,在某些变量上可以加以运用,如单例模式中

# 2 线程池参数调优

# 2.1 代码调试

  • 创建线程池,无限循环添加task,debug看works和queue数量增长规律
  • 等待一段时间后,查看works数量是否回落到core
public class ExecutorTest {
    public static void main(String[] args) {
        BlockingQueue queue = new ArrayBlockingQueue(2);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                5,
                20,
                TimeUnit.SECONDS,
                queue,
                new RejectedExecutionHandler() {
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        System.out.println("reject");
                    }
                }
        );
        for (;;){
            executor.execute(new Runnable() {
                public void run() {
                    try {
                        Thread.currentThread().sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 2.2 Executors剖析

Executors只是一个工具类,协助你创建线程池。Executors对特定场景下做了参数调优。

# 2.2.1 newCachedThreadPool

//core=0
//max=Integer
//timeout=60s
//queue=1
//也就是只要线程不够用,就一直开,不用就全部释放。线程数0‐max之间弹性伸缩
//注意:任务并发太高且耗时较长时,造成cpu高消耗,同时要警惕OOM
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                              60L, TimeUnit.SECONDS,
                              new SynchronousQueue<Runnable>());
1
2
3
4
5
6
7
8
9

# 2.2.2 newFixedThreadPool

//core=max=指定数量
//timeout=0
//queue=无界链表
//也就是说,线程数一直保持制定数量,不增不减,永不超时
//如果不够用,就沿着队列一直追加上去,排队等候
//注意:并发太高时,容易造成长时间等待无响应,如果任务临时变量数据过多,容易OOM
return new ThreadPoolExecutor(nThreads, nThreads,
                              0L, TimeUnit.MILLISECONDS,
                              new LinkedBlockingQueue<Runnable>(),
                              threadFactory);
1
2
3
4
5
6
7
8
9
10

# 2.2.3 newSingleThreadExecutor

//core=max=1
//timeout=0
//queue=无界链表
//只有一个线程在慢吞吞的干活,可以认为是fix的特例
//适用于任务零散提交,不紧急的情况
new ThreadPoolExecutor(1, 1,
                            0L, TimeUnit.MILLISECONDS,
                            new LinkedBlockingQueue<Runnable>()));
1
2
3
4
5
6
7
8

# 2.2.4 newScheduledThreadPool

//core=制定数
//max=Integer
//timeout=0
//queue=DelayedWorkQueue(重点!)
//用于任务调度,DelayedWorkQueue限制住了任务可被获取的时机(getTask方法),也就实现了时间控制
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
      new DelayedWorkQueue(), threadFactory);
1
2
3
4
5
6
7

# 2.3 一些经验

# 2.3.1 corePoolSize

基本线程数,一旦有任务进来,在core范围内会立刻创建线程进入工作。所以这个值应该参考业务并发量在绝大多数时间内的并发情况。同时分析任务的特性。

高并发,执行时间短的,要尽可能小的线程数,如配置CPU个数+1,减少线程上下文的切换。因为它不怎么占时间,让少量线程快跑干活。

并发不高、任务执行时间长的要分开看:如果时间都花在了IO上(写磁盘),那就调大CPU,如配置两倍CPU个数+1。不能让CPU闲下来,线程多了并行处理更快。如果时间都花在了运算上,运算的任务还很重,本身就很占cpu,那尽量减少cpu,减少切换时间。参考第一条

如果高并发,执行时间还很长……

# 2.3.2 workQueue

任务队列,用于传输和保存等待执行任务的阻塞队列。这个需要根据你的业务可接受的等待时间。是一个需要权衡时间还是空间的地方,如果你的机器cpu资源紧张,jvm内存够大,同时任务又不是那么紧迫,减少coresize,加大这里。如果你的cpu不是问题,对内存比较敏感比较害怕内存溢出,同时任务又要求快点响应。那么减少这里。

# 2.3.3 maximumPoolSize

线程池最大数量,这个值和队列要搭配使用,如果你采用了无界队列,那很大程度上,这个参数没有意义。同时要注意,队列盛满,同时达到max的时候,再来的任务可能会丢失(下面的handler会讲)。

如果你的任务波动较大,同时对任务波峰来的时候,实时性要求比较高。也就是来的很突然并且都是着急的。那么调小队列,加大这里。如果你的任务不那么着急,可以慢慢做,那就扔队列吧。

队列与max是一个权衡。队列空间换时间,多花内存少占cpu,轻视任务紧迫度。max舍得cpu线程开销,少占内存,给任务最快的响应。

#

# 2.3.4 keepaliveTime

线程存活保持时间,超出该时间后,线程会从max下降到core,很明显,这个决定了你养闲人所花的代价。如果你不缺cpu,同时任务来的时间没法琢磨,波峰波谷的间隔比较短。经常性的来一波。那么实当的延长销毁时间,避免频繁创建和销毁线程带来的开销。如果你的任务波峰出现后,很长一段时间不再出现,间隔比较久,那么要适当调小该值,让闲着不干活的线程尽快销毁,不要占据资源。

# 2.3.5 threadFactory(自定义展示实例)

线程工厂,用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。如果需要自己定义线程的某些属性,如个性化的线程名,可以在这里动手。一般不需要折腾它。

# 2.3.6 handler

线程饱和策略,当线程池和队列都满了,再加入线程会执行此策略。默认不处理的话会扔出异常,打进日志。这个与任务处理的数据重要程度有关。如果数据是可丢弃的,那不需要额外处理。如果数据极其重要,那需要在这里采取措施防止数据丢失,如扔消息队列或者至少详细打入日志文件可追踪。

# 3 协程

# 3.1 概念

面试官:你知道协程吗?
你:知道,出差或旅游经常用。
面试官:……
1
2
3

很大一部分的程序员不知道协程是啥,项目中也没用到协程。

先看概念:计算机有进程,线程和协程。前两者大家都很熟。而协程,则是基于线程之上自主开辟的异步任务。

  • 线程的切换由操作系统负责调度,协程由用户自己进行调度
  • 线程的默认Stack大小是1M,而协程更轻量,接近1K。因此可以在相同的内存中开启更多的协程。
  • 多个协程在同一个线程上,因此不必使用锁,也减少了上下文切换。

再说结论:

  • 一般需要使用第三方框架来实现
  • java里相对非主流,go和python相对用的多
  • 实际web开发中用的较少,还是要把主要精力放到线程上

# 3.2 使用方式

使用Kilim框架看协程与线程的编码

解压kilim.zip包,导入工程,按说明执行

package com.itheima.thread;


import kilim.Mailbox;
import kilim.Pausable;
import kilim.Task;


public class SimpleTask extends Task {
    static Mailbox<String> mb = new Mailbox<String>();




    /**
     * 1.mvn compile
     * 2.java 命令运行,注意分隔符,linux为:,windows为 ;
     *
     * linux下执行:
     * java -cp ./lib/kilim.jar:./target/classes:$CLASSPATH com.itheima.thread.SimpleTask
     *
     * winows下执行:
     * java -cp ./lib/kilim.jar;./target/classes;$CLASSPATH com.itheima.thread.SimpleTask
     */
    public static void main(String[] args) throws Exception {
        new SimpleTask().start();
        Thread.sleep(10);
        mb.putnb("Hello ");
        mb.putnb("World \n");
        mb.putnb("done");
    }


    public void execute() throws Pausable {
        while (true) {
            String s = mb.get();
            if (s.equals("done")) break;
            System.out.print(s);
        }
        System.exit(0);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 4 并发容器选择

# 4.1 案例一:电商网站中记录一次活动下各个商品售卖的数量。

  • **场景分析:**需要频繁按商品id做get和set,但是商品id(key)的数量相对稳定不会频繁增删
  • **初级方案:**选用HashMap,key为商品id,value为商品购买的次数。每次下单取出次数,增加后再写入
  • **问题:**HashMap线程不安全!在多次商品id写入后,如果发生扩容,在 JDK1.7 之前,在并发场景下HashMap 会出现死循环,从而导致 CPU 使用率居高不下。JDK1.8 中修复了 HashMap 扩容导致的死循环问题,但在高并发场景下,依然会有数据丢失以及不准确的情况出现。
  • **选型:**Hashtable 不推荐,锁太重,选 ConcurrentHashMap 确保高并发下多线程的安全性

# 4.2 案例二:在一次活动下,为每个用户记录浏览商品的历史和次数。

  • **场景分析:**每个用户各自浏览的商品量级非常大,并且每次访问都要更新次数,频繁读写
  • **初级方案:**为确保线程安全,采用上面的思路,ConcurrentHashMap
  • **问题:**ConcurrentHashMap 内部机制在数据量大时,会把链表转换为红黑树。而红黑树在高并发情况下,删除和插入过程中有个平衡的过程,会牵涉到大量节点,因此竞争锁资源的代价相对比较高
  • **选型:**用跳表,ConcurrentSkipListMap将key值分层,逐个切段,增删效率高于ConcurrentHashMap

课件_Page61_01

**结论:**如果对数据有强一致要求,则需使用 Hashtable;在大部分场景通常都是弱一致性的情况下,使用ConcurrentHashMap 即可;如果数据量级很高,且存在大量增删改操作,则可以考虑使用ConcurrentSkipListMap。

# 4.3 案例三:在活动中,创建一个用户列表,记录冻结的用户。一旦冻结,不允许再下单抢购,但是可以浏览。

  • **场景分析:**违规被冻结的用户不会太多,但是绝大多数非冻结用户每次抢单都要去查一下这个列表。低频写,高频读。
  • **初级方案:**ArrayList记录要冻结的用户id
  • **问题:**ArrayList对冻结用户id的插入和读取操作在高并发时,线程不安全。Vector可以做到线程安全,但并发性能差,锁太重。
  • 选型:综合业务场景,选CopyOnWriteArrayList,会占空间,但是也仅仅发生在添加新冻结用户的时候。绝大多数的访问在非冻结用户的读取和比对上,不会阻塞。

# 5 上下文切换优化

# 5.1 基本操作

CPU通过时间片分配算法来循环执行任务,时间片一般是几十毫秒(ms)。切换就要保存旧状态,完成恢复时就要读取存储的内容。这个操作过程就是上下文的切换。

(现实例子:参考日常需求开发与应急bug处理开发任务被挂起的场景 -_-! )

# 5.2 竞争锁

1)锁的持有时间越长,就意味着有越多的线程在等待该竞争资源释放。上下文的切换代价就越多。

2)将锁贴近需要加锁的地方,越近越好!在synchronized性能优化中有案例

public void f(){
    synchronized (this){
        f1();
        f2();
        f3();
    }
}
1
2
3
4
5
6
7

优化后

public void f(){
    f1();
    synchronized (this){
        f2();
    }
    f3();
}
1
2
3
4
5
6
7

3)结论:类锁 < 静态锁 < 方法锁 < 代码块锁 , 能共享锁的地方尽量不要用独享锁

# 5.3 wait/notify

# 5.3.1 过时通知

看一个典型错误,猜一猜结果……

package com.itheima;
public class WaitInvalid {
    volatile int total = 0;
    byte[] lock = new byte[0];
    //计算1‐100的和,算完后通知print
    public void count(){
        synchronized (lock){
            for (int i = 1; i < 101; i++) {
                total += i;
            }
            lock.notify();
        }
        System.out.println("count finish");
    }
//打印,等候count的通知    
    public void print(){
        synchronized (lock){
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(total);
    }
    public static void main(String[] args) {
        WaitInvalid waitInvalid = new WaitInvalid();
        new Thread(()‐>{
            waitInvalid.count();
        }).start();
        new Thread(()‐>{
            waitInvalid.print();
        }).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

将count和wait的顺序交换,再看一下结果,思考一下为什么??

分析:

count先执行时,提前释放了notify通知,这时候,print还没进入wait,收不到这个信号。

等print去wait的时候(阻塞),再等通知等不到了,典型的通知过时现象。

仅仅因为一行代码的顺序问题,如果不注意,造成整个程序卡死

# 5.3.2 额外唤醒

跑一下看看结果……

package com.itheima;
import java.util.ArrayList;
import java.util.List;
public class NotifyInvalid {
    List list = new ArrayList();
    byte[] lock = new byte[0];
    public void del() {
        synchronized (lock){
            //没值就等,有值就删
            if (list.isEmpty()){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 二次判断
           // if (list.isEmpty()){ 
              list.remove(0);
          //  }
        }
    }
    public void add(){
        synchronized (lock){
            //加个值后唤醒
            list.add(0,0);
            lock.notifyAll();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        NotifyInvalid notifyInvalid = new NotifyInvalid();


        //启动两个线程等候删除
        for (int i = 0; i < 2; i++) {
            new Thread(()‐>{
                notifyInvalid.del();
            }).start();
        }


        //新线程添加一个
        new Thread(()‐>{
            notifyInvalid.add();
        }).start();
        Thread.sleep(1000);
        System.out.println(notifyInvalid.list.size());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

分析:

出异常了!因为等候的两个线程第一个删除后,第二个唤醒时,等待前的状态已失效。

方案:

线程唤醒后,要警惕睡眠前后状态不一致,要二次判断

# 5.4 线程池

1)线程池的线程数量设置不宜过大,因为一旦线程池的工作线程总数超过系统所拥有的处理器数量,就会导致过多的上下文切换。

2)慎用Executors,尤其如newCachedThreadPool。这个方法前面分析过。如果任务过多会无休止创建过多线程,增加了上下文的切换。最好根据业务情况,自己创建线程池参数。

# 5.5 虚拟机

1)很多 JVM 垃圾回收器(serial 收集器、ParNew 收集器)在回收旧对象时,会产生内存碎片

2)碎片内存整理中就需要移动存活的对象。而移动内存对象就意味着这些对象所在的内存地址会发生变化

3)内存地址变化就要去移动对象前暂停线程,在移动完成后需要再次唤醒。无形中增加了上下文的切换

4)结论:合理搭配JVM内存调优,减少 JVM 垃圾回收的频率可以有效地减少上下文切换

# 5.6 协程

1)协程不需要切换上下文,更轻量化。

2)平时用的相对较少。用不好会出问题。

上次更新: 2025/04/03, 11:07:08
并发深入
电商实际应用

← 并发深入 电商实际应用→

最近更新
01
tailwindcss
03-26
02
PaddleSpeech
02-18
03
whisper
02-18
更多文章>
Theme by Vdoing | Copyright © 2019-2025 跨境互联网 | 豫ICP备14016603号-5 | 豫公网安备41090002410995号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式