高并发学习笔记

▼魔方 西西 提交于 2020-10-28 11:44:58

并发

概念

高并发

并发: 多个线程操作相同的资源,保证线程安全,合理使用资源

高并发: 服务能同时处理很多请求,提高程序性能

 

java 内存模型

Thread Stack 存储基本 变量:比如 int , long 等,或者对象句柄 或者方法

本地变量存在 栈上即 Thread Stack ,  对象存在  堆上即Heap

一个本地变量也可能指向一个 对象引用。即引用存储在栈上, 对象是 存在堆上,

一个对象的方法和本地变量是存储在 Thread stack 上的,就算该对象存在 Heap堆上,

一个对象的成员变量 可能会随着这个对象存在 堆上,不管这个成员变量是 原始类型还是引用类型,

静态成员变量,跟随类的定义一起存在堆上,

堆上对象可以被持有该对象引用的线程访问, 一个线程可以访问一个对象,也可以访问该对象的成员变量

 

如果两个线程 持有 同一个对象的引用,或者调用了同一个对象的方法,线程都会持有对象的成员变量,

但是这两个线程都拥有这个对象的成员变量的 私有拷贝(所以并发时候就容易造成数据不一致)

CPU与 Java Memory Model 内存模型

CPU Registers 即 CPU 寄存器

Main Memory 即 主存

同步八种操作

同步规则

 

 

 

总结

 

并发测试工具

postman , 

Apache Bench  https://www.cnblogs.com/Ryana/p/6279232.html

 Jmeter

 

CountDownLatch 计数器

即达到某种条件才可以 才可以执行

Semaphore 信号量

可以阻塞线程,控制同一时刻的请求并发量

 

线程安全性

 

原子性

atomic 包下的类, 通过 CAS 来保障的

LongAdder (jdk 1.8)

在高并发下比 AtomicLong 性能高,因为没有 使用 while 循环做 CAS ,

缺点:如果统计的时候有并发更新,可能会导致误差

LongAdder类与AtomicLong类的区别在于高并发时前者将对单一变量的CAS操作分散为对数组cells中多个元素的CAS操作,取值时进行求和;而在并发较低时仅对base变量进行CAS操作,与AtomicLong类原理相同。

 

AtomicBoolean 

compareAndSet 可以用来控制只自执行一次,执行一次之后就执行 compareAndSet(false,true) 

设置可以控制只有一个线程执行, 判断 AtomicBoolean 的值进行控制

 

 

可见性

 

 

volatile

 

volatitle 可以用于多线程的状态标记

 

有序性

比如 volatitle , synchronized , Lock 可以保证有序性

 

 

安全发布对象

对象不安全例子

public class UnsafePublish {

    private String[] states = {"a", "b", "c"};

    //不安全的,可以通过 getStates 获取数组引用,修改数组的值
    public String[] getStates() {
        return states;
    }

    public static void main(String[] args) {
        UnsafePublish unsafePublish = new UnsafePublish();
        log.info("{}", Arrays.toString(unsafePublish.getStates()));

        unsafePublish.getStates()[0] = "d";
        log.info("{}", Arrays.toString(unsafePublish.getStates()));
    }
}

对象 逸出例子

public class Escape {

    private int thisCanBeEscape = 10;

    public Escape () {
        new InnerClass();
    }

    private class InnerClass {
        // Escape 构造函数还没有完全,就已经 获取 到 Escape.this 了,this造成逸出,会造成 对象逸出
        //对象未完全构造完成之前,不可对其进行发布
        public InnerClass() {
            log.info("{}", Escape.this.thisCanBeEscape);
        }
    }

    public static void main(String[] args) {
        new Escape();
    }
}

单例也是安全的发布

枚举单例

**
 * 枚举模式:最安全
 */
public class SingletonExample7 {

    // 私有构造函数
    private SingletonExample7() {

    }

    public static SingletonExample7 getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;

        private SingletonExample7 singleton;

        // JVM保证这个方法绝对只调用一次
        Singleton() {
            singleton = new SingletonExample7();
        }

        public SingletonExample7 getInstance() {
            return singleton;
        }
    }
}

枚举单例,最安全,而且是懒汉模式,不会造成资源的浪费

不可变 对象

final

注意: 一个类的 private 私有方法,会被隐式的 变为 final 方法。 目前最新版的JDK 已经不需要 在方法上面 加上 final来提高效率了,已经没有这个作用了。

如果是旧版的话,方法太大也是 final 不起效的

final 修饰变量:

基本数据类型,赋值之后就不能再次赋值了

引用类型变量: 赋值之后,就不能再 引用 其他 变量

工具类 提高的不可变 使用

 

线程封闭

即将对象封闭在一个线程内,其他线程访问不到它,或者对其不可见

 

线程安全的同步容器

Stack 即栈 ,继承了 Vector

Vector 也是有可能出现线程不安全的,例子:

public class VectorExample2 {

    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {

        while (true) {

            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }

            Thread thread1 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.remove(i);
                    }
                }
            };

            Thread thread2 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.get(i);
                    }
                }
            };
            thread1.start();
            thread2.start();
        }
    }
}

remove 引起 get 越界

容器操作不当引起的 错误

public class VectorExample3 {

    // java.util.ConcurrentModificationException
    private static void test1(Vector<Integer> v1) { // foreach
        for(Integer i : v1) {
            if (i.equals(3)) {
                v1.remove(i);
            }
        }
    }

    // java.util.ConcurrentModificationException
    private static void test2(Vector<Integer> v1) { // iterator
        Iterator<Integer> iterator = v1.iterator();
        while (iterator.hasNext()) {
            Integer i = iterator.next();
            if (i.equals(3)) {
                  // iterator.remove(); //这个就没有问题
                v1.remove(i);
            }
        }
    }

    // success
    private static void test3(Vector<Integer> v1) { // for
        for (int i = 0; i < v1.size(); i++) {
            if (v1.get(i).equals(3)) {
                v1.remove(i);
            }
        }
    }

    public static void main(String[] args) {

        Vector<Integer> vector = new Vector<>();
        vector.add(1);
        vector.add(2);
        vector.add(3);
        test1(vector);
    }
}

一般使用并发容器来取代同步容器

 

并发容器 J.U,C

CopyOnWriteArrayList

1, 写操作的时候会消耗内存,如果集合很多,写入的时候会导致 Full gc  或者 Minor GC

2,不能用于实时读取场景, 适合读多写少, 因为 copyOnWriteArrayList 写会可能比较耗时,copyOnWriteArrayList 读写分离,能实现最终一致性

3. 不能确定 大小,就尽量不要使用,对于要求高性能的场景不适合

4, 其写操作时候会加锁, 读操作无锁,读的是原List .并发时候 会  对 加锁, 复制一份 数据,写完之后执行 原来的List 数组内容

注意 addAll 类似的 方法 不是线程安全的

ConcurrentSkipListMap  原理参考 https://blog.csdn.net/chenssy/article/details/75000701

ConcurrentSkipListMap 相比 ConcurrenthashMap :

1,  key 有序

2. 支持更高的并发

3, 存取时间 与线程数关系不大: 数据量一样下, 并发越大 ConcurrentSkipListMap 越优

 

安全共享对象策略

AQS

Sync queue 同步队列

Condition queue 单向链表,可能有多个

继承,即使用的模板方法, int 就是 state 状态

 

countDownLatch

同步辅助类 , 它的计数器是不能重置的, 计数为0的时候,才会执行 await 的对应的线程

场景: 并行计算(将任务拆为小任务,然后汇总结果)

也可以进行时间控制 , 超过时间没完成就算了

countDownLatch.await(10, TimeUnit.MILLISECONDS);

Semaphore 信号量

场景: 常用语仅能提交有限资源的场景:比如数据库连接数

并发访问控制

CyclicBarrier

可重用,也叫循环屏障, 可以用于多线程去并行执行,汇总结果

即多个线程循环等待,都 准备好之后才会一起执行

ReentrantLock 与锁

性能都差不多,功能性上, Lock 粒度更细, 推荐使用 同步锁

 

ReentrantReadWriteLock 读写锁,在没有任何读写锁的情况下,才可以获取 写锁。 如果读多写少的情况下,
可能会导致写入的线程 迟迟写入不了,而陷入饥饿状态, 因为读的情况太多了
StampedLock 三种锁模式: 写、读、乐观读。 由版本与模式 两个部分组成。 1.8 才新增的,因此比ReentrantReadWriteLock 性能高

 

非公平锁,即 唤醒所有等待的线程队列,然后去竞争 锁

公平锁,就从 等待的线程队列里面取 等待最久的一个线程去唤醒 ,拿到锁去 执行

 

所获取方法,返回的是一个数字,代表 stamp 票据,用响应的锁状态来控制和表示相关的访问,

0 表示没有写锁被授权访问,读锁上 又分为 悲观读,与乐观读。

乐观读,认为读多写少,乐观认为写入与读取发送的几率很少,不悲观使用完全的读写锁定,

程序可以  在读取之后,查看是否遭到写入的变更,再采取后续的措施,可以提高程序的吞吐量

参考 https://segmentfault.com/a/1190000015808032?utm_source=tag-newest

总结

1.竞争少,用 同步锁 syc

2. 竞争不少, 线程增长的趋势是可以预估的,用ReentrantReadWriteLock

 

Condition

public class LockExample6 {

    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition condition = reentrantLock.newCondition();

        new Thread(() -> {
            try {
                reentrantLock.lock();//线程加入等待队列 aqs
                log.info("wait signal"); // 1  
                condition.await();// 线程从队列 移除 , 锁的释放,然后马上加入 condition 等待队列里面
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("get signal"); // 4 被唤醒,继续执行
            reentrantLock.unlock(); //释放锁
        }).start();

        new Thread(() -> {
            reentrantLock.lock();
            log.info("get lock"); // 2  获取锁 ,加入 asq 等待队列
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            condition.signalAll(); //给condition发送信号,  condition 等待队列 会放入 AQS 等待队列
            log.info("send signal ~ "); // 3
            reentrantLock.unlock(); // 释放锁, 之前的第一个线程就被唤醒去执行了
        }).start();
    }
}

类似 object 的 wait 与 notify 

 

FutureTask

可以在 线程执行完任务之后,获取到结果

Callable 与 Runnalbe 接口对比,  callable 返回一个 值,并且可以抛出异常

Future 接口 ,futrue 会监听 线程的 call 的 返回值

FutrueTask 类 , 父类是 RunnableFuture

RunnableFuture  extends   Runnable, Future

FutrueTask  最终执行的就是 callable 运行的任务。

如果构造函数参数是 runable  会转换成 callable 类型。 

所以 FutrueTask  即可以被线程执行执行, 也可以 得到 callable 返回值

以上组合,可以异步去执行任务,然后需要结果的时候,就可以去获得。

Fork/Join 框架

用于并行执行的框架, 大任务,分为小任务并行执行

join 合并结果,原理使用 工作窃取算法

参考 https://blog.csdn.net/pange1991/article/details/80944797

任务有一定的局限性,只能使用 Fork/Join 的同步机制,如果使用其他同步机制,工作线程就不能执行任务了

任务不能执行IO操作,任务不能抛出检查异常, 

BlockingQueue 阻塞队列,线程安全

以下情况会阻塞:

1, 队列满的时候,一个线程,入队

2 , 当队列空,一个线程, 出队、

 

场景: 生产者,消费者

以下不能马上执行的时候 出现的表现

比如 add(0) 不能马上执行,抛出异常, poll 就会返回一个特殊值, take() 阻塞, poll(timeout,timenunit) 超时之后,还不能执行,就返回一个特殊值

ArrayBlockingQueue

有界的,内部是一个 数组实现

DelayQueue

阻塞内部元素 ,内部使用的 是 锁和排序 

LinkedBlockingQueue

初始化 没有指定值就是无界,否则就是有界

内部是一个 链表

PriorityBlockingQueue

具有优先级的阻塞队列 ,无边界,具有排序规则,可以插入空对象

SynchronousQueue 

同步队列,无解,非缓存的队列。放入元素之后,只有取走之后才可以再放入

 

总结: BlockingQueue 不仅实现了一个完整队列所具有的基本功能,同时在多线程下,还可以 管理多线程直接的 自动等待,唤醒功能

从而开发人员可以忽略这些细节

 

线程池

 

 

ThreadPoolExecutor

如果当前线程数小于 corePoolSize  ,执行时候会新建新的线程,即使 线程池里面的线程是空闲的

如果当前线程数大于 corePoolSize , 且小于 maxmumPoolSize , 且 workQueue满了的时候,才会新建线程 去执行

如果 corePoolSize=maxmumPoolSize ,且 workQueue 没有满,则把 请求放入 workQueue 里面,等待有空闲的线程去执行

如果 当前线程数=maxmumPoolSize ,且workQueue 满,则根据拒绝策略去处理

workQueue 

保存等待队列的 阻塞队列,当 一个任务提交时候,会根据线程池的情况响应处理

1, 直接切换使用 无界队列(此时能够创建的最大线程数就是 corePoolSize,maxmumPoolSize 无效),或者使用有界队列

2.  当线程池中所有的线程都在运行中时候,一个新任务提交就会放在等待队列里面

如果想降级系统的消耗,CPU使用率,操作系统资源的消耗,上下文环境的切换开销等等,可以设置一个较大的队列容量或者较小的线程池容量,

这样会减低 处理任务的吞吐量。 如果 执行的任务经常阻塞,可以增加 maxmumPoolSize,

如果 队列 容量较小,就要将 corePoosize 设置大一些,这样CPU使用率会增加

如果 maxmumPoolSize 比较大,队列也比较大, 这样并发量会比较高,线程之间的调度就是一个要考虑的问题,反而可能减低处理任务的吞吐量

stop 将 中断正在执行的线程,而shudown 会让正在执行的线程继续执行,直到完成

 

即 IO 是 cpu的 2倍, 否则就是  CPU+1 

 

死锁

死锁必要条件

1, 互斥条件

锁对获取的资源具有排他性,某段时间内,只有一个进程占用

2, 请求和保持条件

进程已经保持了至少一个资源,但又提出了新的资源 请求,而该资源以被其他进程占用,

此时进程阻塞,而又对自己占有的资源保持不放

3. 不剥夺条件

进程已获得资源,在未使用完之前不能被剥夺,只能自己释放

4. 环路等待条件

存在资源的环型的链

 

如何避免死锁? 

1. 死锁检查,比如 线程获取一个锁加记录下来,然后 检查 是否存在 循环 等待链路,环路等待

2. 死锁之后,可以回退 ,然后重新开始,也可以 给线程随机的 执行级别,避免 资源的互相等待

 

并发最佳实践

1, 使用本地变量或者方法内定义变量

2, 使用不可变类, 可以减低代码中 的同步数量

3。 最小化锁的作用域范围

安达尔定理: S=1/(1-a + a/n)

https://baike.baidu.com/item/%E9%98%BF%E5%A7%86%E8%BE%BE%E5%B0%94%E5%AE%9A%E5%BE%8B/10386960?fr=aladdin

 

4, 使用线程池的 Executor, 而不是直接 new Thread 执行

5.  宁可使用同步,也不要使用线程的 wait 和 notify 方法

6. 使用 BlockingQueue实现生产-消费模式

7, 使用并发集合,而不是加了锁的同步集合

8. 使用Semahpore 创建有界的访问

9, 宁可使用同步代码块,也不使用 同步的方法

10, 避免使用静态变量(并发下容易出现问题,除非变为 final 变量)

 

spring 与线程安全

 

无状态的对象 都是线程安全的

spring 线程安全,是因为无状态的设计

HashMap 与 ConcurrentHashMap

https://www.cnblogs.com/wang-meng/p/9b6c35c4b2ef7e5b398db9211733292d.html

https://blog.csdn.net/visant/article/details/80045154

 

1.7 图

 

1.8 图

https://www.jianshu.com/p/f9b3e76951c2

高并发扩容

 

多机房,防止单一机房出问题  , 多机房,异地多活(最大情况的容灾)

缓存

缓存特征

 

 

FIFO: 先进去的数据,不够内存,先清空。(旧的数据先清空)

LFU: 清空最少使用次数的数据 , 保留高频使用数据 比较合适

LRU: 根据元素最后一次被使用的时间来进行 清空, 热点数据 比较合适

命中率 

 

读多写少适合缓存, 

同时 并发越高,缓存的收益就越高

考虑 如何提高缓存的命中率

缓存分类和场景

 

Guava Cache

思想来源 ConcurrenHahMap

memcache

redis

redis 读写非常高效, 所有操作都具有原子性,或者组合操作原子

应用场景: 计数器, 排行榜, 最新N个数据, 过期时间应用,做唯一性检查, 实时消息系统,队列系统,缓存

高并发缓存问题

缓存一致性

依赖缓存过期时间与更新策略

 

缓存并发问题

 

使用锁机制, 当 没有缓存时候,去查询数据时候,就加上 locke 数,防止很多并发去查询DB

其他请求只需要牺牲一定的等待时间即可

缓存穿透

即查询 某个 key ,但是该key 没有, 然后去查询数据库,而且数据库也没有这个数据

那么很多并发请求都跑去数据库查询了

 1.  因此没有数据, 如果缓存的是集合,就给 空对象,如果是 单个数据类型,就给标识。

避免 这个问题

2. 对可能为 null 的 key进行统一的存放, 并在请求数据时候做拦截,避免过多请求跑到数据

缓存雪崩

缓存抖动(类似缓存雪崩,一般过段时间就好了), 缓存节点故障 ,一般是采用 一致性hash 解决

雪崩: 即大量缓存数据不存在,都跑去查询数据,而系统挂了

比如 很多数据 缓存过期时间 一样,都集中失效了, 可以 给 数据不同的过期时间

或者 加上 熔断或者 限流去做 这个

 

可以做 二级缓存, 一级缓存是 java 做的缓存, redis 做分布式缓存,

定时将java的 缓存 输入 redis 里面,这样取数据直接从redis 取 计算好的数据即可。

 

消息队列MQ

可以控制发现消息的 频率, 解耦,异步

特性

好处: 解耦,最终一致性,广播, 错峰与流控

 

最终一致性与分布式事务: 

1, 先记录不确定 事件消息

2, 如果操作失败,或者不确定 ,则可以使用定时器 重复 加入消息队列里面,重复去处理直到成功

3, 如果操作成功,则将消息 给去掉即可

4. 做好幂等

 

对于要求实时数据,数据一致性下, RPC 是比消息处理高效的

 

kafka

高吞吐量,高效,但是有可能丢数据

 

rabbitMQ

 

根据 exchange type 有不同的使用场景

应用拆分

 

拆分原则

1, 业务优先, 每个业务,模块拆解

2, 循序渐进(边拆变测,小步前进)

3, 兼顾技术: 重构,分层

4, 可靠测试

思考

应用直接通信: RPC(dubbo), 消息队列

应用直接数据库设计:每个应用都有独立的数据库,如果有公用的部分,可以考虑一起 连一个 comment 公用数据库

避免事务操作跨应用:分布式事务很消耗资源

技术

spring cloud

 

限流

比如控制一段时间内,某段代码的执行次数

 

限流算法

 计数器法, 滑动窗口,漏桶算法,令牌桶算法

有临界突发请求问题

 

参考https://blog.csdn.net/weixin_34283445/article/details/87050922

https://www.jianshu.com/p/9f7df2ebbb82

 

计数器法 VS 滑动窗口 : 滑动窗口更稳定,但是也比较耗空间

漏桶算法 VS 令牌桶算法: 令牌桶允许一定的突发请求,对用户更友好,实现更简单,用得更多

 

限流实战

 Guava RateLimiter 做到是 单机版的限流

分布式限流: 只要大概的限制即可,比如 如果单台不能大于400请求每秒,

如果换成了分布式多台的话,比如有2台,2台都可以负载均衡,那么每台限流为200请求每秒,大概限制即可。 

如果要精准限流: 使用 Redis: incrby key num 

即每次 来一个请求,就 执行一次,根据当前时间秒 生成一个 key ,得到返回的值,

然后看看 返回值是否大于 允许的每秒最大值就可以进行控制了,大于这个值就阻塞或者丢弃。

如果是 每秒 几万,十几万W请求,还是使用 Guava RateLimiter ,否则 会导致 redis 压力太大了。

服务降级与服务熔断

以上是动态配置架构 图,

比如  降级时候,不读数据库的主库了,而是读从库,可以在配置里面修改,这样就可以动态生效了

Hystrix

 

 

 

数据库切库,分库,分表

 

切库

主库: 写, 实时数据的查看, 从库 : 非实时数据的查询

参考 https://www.imooc.com/article/22556

 

分库

 

 

即 服务没有拆分, 但是分库了, 所以要多数据源

参考 https://www.imooc.com/article/22609

分表

当一个表数据很大,就是做了SQL优化和索引之后还是 影响了插入 速度时候,影响使用的时候,

就应该考虑分表了,也可以提取考虑 好,做分表处理。到了千万级别数据时候,做什么操作都会慢很多的。

好处: 单表并发提高,读写和磁盘IO 都会提高,同时读写影响的数据量变小,插入性能提高

 

垂直分表,一般按照活跃数据或者字段进行处理,提高单表处理速度

参考http://www.imooc.com/article/25256

高可用的一些手段

https://www.imooc.com/article/20891

 

 

 

 

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!