并发与高并发

342 阅读27分钟

一、课程基础

1、基础知识讲解与核心知识准备

1.jpg

2、并发及并发的线程安全处理

2.jpg

3、高并发处理的思路及手段

3.jpg

4、基础知识讲解与核心知识准备

image.png

5、最简单的场景举例

5.1、实现一个计数功能

public class CountExample1 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static int count = 0;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        count++;
    }
}

多次执行结果:

22:54:33.068 [main] INFO com.mmall.concurrency.example.commonUnsafe.HashMapExample - size:4934
22:55:55.975 [main] INFO com.mmall.concurrency.example.count.CountExample1 - count:4978
22:56:06.005 [main] INFO com.mmall.concurrency.example.count.CountExample1 - count:4978
public class HashMapExample {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    private static Map<Integer, Integer> map = new HashMap<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("size:{}", map.size());
    }

    private static void update(int i) {
        map.put(i, i);
    }
}

多次执行结果不同:

22:56:28.710 [main] INFO com.mmall.concurrency.example.commonUnsafe.HashMapExample - size:4941
22:56:48.412 [main] INFO com.mmall.concurrency.example.commonUnsafe.HashMapExample - size:4940
22:56:59.754 [main] INFO com.mmall.concurrency.example.commonUnsafe.HashMapExample - size:4933

思考:并发场景下如何保证结果的正确性?

6、基本概念

  • 并发:同时拥有两个或者多个线程,如果程序在单核处理器上运行,多个线程将交替地换入或者换出内存,这些线程是同时“存在”的,每个线程都处于执行过程中的某个状态,如果运行在多核处理器上,程序中的每个线程都将分配到一个处理器核上,因此可以同时运行。
  • 高并发:高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。

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

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

7、知识技能

  • 总体架构:Spring Boot、Maven、JDK8、MySQL
  • 基础组件:Mybatis、Guava、Lombok、Redis、Kafka
  • 高级组件(类):Joda-Time、Atomic包、J.U.C、AQS、ThreadLocal、RateLimiter、Hystrix、threadPool、shardbatis、curator、elastic-job...

二、并发编程的基础

1、CPU多级缓存

4.jpg

  • 为什么需要CPU cache:CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源。所以cache的出现,是为了缓解CPU和内存之间速度的不匹配问题(结构:cpu->cache->memory)

  • CPU cache有什么意义∶

    • 1)时间局部性︰如果某个数据被访问,那么在不久的将来它很可能被再次访问;
    • 2)空间局部性︰如果某个数据被访问,那么与它相邻的数据很快也可能被访问;

2、CPU 多级缓存–缓存一致性(MESI)

  • 用于保证多个CPU cache之间缓存共享数据的一致

image.png 详解:blog.csdn.net/denglin1231…

3、CPU 多级缓存–乱序执行优化

  • 处理器为提高运算速度而做出违背代码原有顺序的优化。

image.png

4、Java内存模型(Java Memory Model,JMM)

image.png

多CPU image.png

image.png

5、Java内存模型抽象结构图

image.png

6、Java内存模型-同步八种操作

  • lock(锁定)∶作用于主内存的变量,把一个变量标识为一条线程独占状态
  • unlock(解锁)∶作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取)︰作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入)︰作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存的变量,把工作内仔中的一个艾量值传递给执行引擎
  • assign(赋值)∶作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量
  • store(存储)∶作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
  • write (写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中

7、Java内存模型-同步规则

  • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行

  • 不允许read和load、store和write操作之一单独出现

  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中

  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中

  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化( load或assign )的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作

  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现

  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现

  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值

  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量

  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

8、Java内存模型-同步操作与规则

image.png

9、并发的优势与风险

image.png

10、总结

  • CPU多级缓存∶缓存一致性、乱序执行优化
  • Java内存模型:JMM规定、抽象结构、同步八种操作及规则
  • Java并发的优势与风险

三、并发编程与线程安全

1、环境搭建与准备

2、并发模拟

  • Postman : Http请求模拟工具
  • Apache Bench ( AB) : Apache附带的工具,测试网站性能
  • JMeter : Apache组织开发的的压力测试工具
  • 代码: Semaphore、CountDownLatch等

image.png

image.png

@Slf4j
@NotThreadSafe
public class ConcurrencyTest {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static int count = 0;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        count++;
    }
}

Connected to the target VM, address: '127.0.0.1:14930', transport: 'socket'
14:55:01.938 [main] INFO com.mmall.concurrency.ConcurrencyTest - count:4951
Disconnected from the target VM, address: '127.0.0.1:14930', transport: 'socket'

四、线程安全性

  • 定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的

原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作

  • 原子性- Atomic包
    • AtomicXXX : CAS、Unsafe.compareAndSwapInt
@Slf4j
@ThreadSafe
public class AtomicExample2 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static AtomicLong count = new AtomicLong(0);

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    private static void add() {
        count.incrementAndGet();
        // count.getAndIncrement();
    }
}
Connected to the target VM, address: '127.0.0.1:3290', transport: 'socket'
15:14:27.035 [main] INFO com.mmall.concurrency.example.count.CountExample2 - count:5000
Disconnected from the target VM, address: '127.0.0.1:3290', transport: 'socket'

image.png

image.png

image.png

  • AtomicLong、LongAdder
@Slf4j
@ThreadSafe
public class AtomicExample3 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static LongAdder count = new LongAdder();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        count.increment();
    }
}
Connected to the target VM, address: '127.0.0.1:3290', transport: 'socket'
15:14:27.035 [main] INFO com.mmall.concurrency.example.count.CountExample2 - count:5000
Disconnected from the target VM, address: '127.0.0.1:3290', transport: 'socket'

image.png

  • AtomicReference、AtomicReferenceFieldUpdater
@Slf4j
@ThreadSafe
public class AtomicExample4 {

    private static AtomicReference<Integer> count = new AtomicReference<>(0);

    public static void main(String[] args) {
        count.compareAndSet(0, 2); // 2
        count.compareAndSet(0, 1); // no
        count.compareAndSet(1, 3); // no
        count.compareAndSet(2, 4); // 4
        count.compareAndSet(3, 5); // no
        log.info("count:{}", count.get());
    }
}
@Slf4j
@ThreadSafe
public class AtomicExample5 {

    private static AtomicIntegerFieldUpdater<AtomicExample5> updater =
            AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");

    @Getter
    public volatile int count = 100;

    public static void main(String[] args) {

        AtomicExample5 example5 = new AtomicExample5();

        if (updater.compareAndSet(example5, 100, 120)) {
            log.info("update success 1, {}", example5.getCount());
        }

        if (updater.compareAndSet(example5, 100, 120)) {
            log.info("update success 2, {}", example5.getCount());
        } else {
            log.info("update failed, {}", example5.getCount());
        }
    }
}
16:25:17.643 [main] INFO com.mmall.concurrency.example.atomic.AtomicExample5 - update success 1, 120
16:25:17.647 [main] INFO com.mmall.concurrency.example.atomic.AtomicExample5 - update failed, 120

原子性-锁

  • synchronized:依赖JVM
    • 修饰代码块:大括号括起来的代码,作用于调用的对象
    • 修饰方法:整个方法,作用于调用的对象
    • 修饰静态方法:整个静态方法,作用于所有对象
    • 修饰类:括号括起来的部分,作用于所有对象
  • Lock :依赖特殊的CPU指令,代码实现,ReentrantLock

原子性-对比

  • synchronized :不可中断锁,适合竞争不激烈,可读性好
  • Lock : 可中断锁,多样化同步,竞争激烈时能维持常态
  • Atomic:竞争激烈时能维持常态,比Lock性能好;只能同步一个值

可见性:一个线程对主内存的修改可以及时的被其他线程观察到

可见性:导致共享变量在线程间不可见的原因

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存与主存间及时更新

可见性- synchronized

JMM关于synchronized的两条规定∶

  • 线程解锁前,必须把共享变量的最新值刷新到主内存
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值**(注意,加锁与解锁是同一把锁)**

可见性-volatile

通过加入内存屏障和禁止重排序优化来实现

  • 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
  • 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量

可见性-volatile写

image.png

可见性-volatile读

image.png volatile不具有原子性

可见性-volatile使用

image.png

有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序

有序性

  • Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
  • volatile、synchronized、Lock

有序性- happens-before原则

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则∶线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

线程安全性-总结

  • 原子性:Atomic包、CAS算法、synchronized、Lock
  • 可见性: synchronized、volatile
  • 有序性: happens-before

五、发布对象

  • 发布对象:使一个对象能够被当前范围之外的代码所使用
  • 对象逸出:一种错误的发布。当一个对象还没有构造完成时,就使它被其他线程所见
@Slf4j
@NotThreadSafe
public class Escape {
    private int thisCanBeEscape = 0;

    public Escape(){

    }

    private class InnerClass{

        public InnerClass(){
            log.info("{}", Escape.this.thisCanBeEscape);
        }
    }

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

安全发布对象

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的引用保存到volatile类型域或者AtomicReference对象中
  • 将对象的引用保存到某个正确构造对象的final类型域中
  • 将对象的引用保存到一个由锁保护的域中

不可变对象

  • 不可变对象需要满足的条件
    • 对象创建以后其状态就不能修改
    • 对象所有域都是final类型
    • 对象是正确创建的(在对象创建期间,this引用没有逸出)
  • final关键字:类、方法、变量
    • 修饰类:不能被继承
    • 修饰方法:1、锁定方法不被继承类修改;2、效率
    • 修饰变量:基本数据类型变量、引用类型变量
  • Collections.unmodifiableXXx : Collection、List、Set、Map...
  • Guava : ImmutableXXx : Collection、List、Set、Map...

线程封闭

  • Ad-hoc线程封闭:程序控制实现,最糟糕,忽略
  • 堆栈封闭:局部变量,无并发问题
  • ThreadLocal线程封闭:特别好的封闭方法

线程不安全类与写法

  • StringBuilder -> StringBuffer
  • SimpleDateFormat -> JodaTime
  • ArrayList,HashSet, HashMap等Collections
  • 先检查再执行: if(condition(a)){ handle(a);}

线程安全-同步容器

  • ArrayList -> Vector,Stack
  • HashMap -> HashTable (key. value不能为null)
  • Collections.synchronizedXXX (List、Set、Map)

线程安全-并发容器J.U.C

  • ArrayList -> CopyOnWriteArrayList
  • HashSet、TreeSet -> CopyOnWriteArraySet、ConcurrentSkipListSet
  • HashMap、TreeMap -> ConcurrentHashMap、ConcurrentSkipListMap

image.png

安全共享对象策略–总结

  • 线程限制:一个被线程限制的对象,由线程独占,并且只能被白有它的线程修改
  • 共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它
  • 线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它
  • 被守护对象:被守护对象只能通过获取特定的锁来访问

AbstractQueuedSynchronizer - AQS

image.png

  • 使用Node实现FIFO队列,可以用于构建锁或者其他同步装置的基础框架
  • 利用了一个int类型表示状态
  • 使用方法是继承
  • 子类通过继承并通过实现它的方法管理其状态{ acquire和release }的方法操纵状态
  • 可以同时实现排它锁和共享锁模式(独占、共享)

AQS同步组件

  • CountDownLatch
  • Semaphore
  • CyclicBarrier
  • ReentrantLock
  • Condition
  • FutureTask

CountDownLatch

image.png

Semaphore

image.png

CyclicBarrier

image.png

ReentrantLock 与 锁

  • ReentrantLock(可重入锁)和synchronized区别
    • 可重入性
    • 锁的实现
    • 性能的区别
    • 功能区别

ReentrantLock独有的功能

  • 可指定是公平锁还是非公平锁
  • 提供了一个Condition类,可以分组唤醒需要唤醒的线程
  • 提供能够中断等待锁的线程的机制,lock.lockInterruptibly()

锁使用总结(回顾视频7-6 8分钟)

FutureTask

  • Callable 与 Runnable接口对比
  • Future接口
  • FutureTask类

Fork/Join框架

image.png

BlockingQueue

image.png

image.png

  • ArrayBlockingQueue
  • DelayQueue
  • LinkedBlockingQueue
  • PriorityBlockingQueue
  • SynchronousQueue

线程池

  • new Thread弊端
    • 每次new Thread新建对象,性能差
    • 线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM
    • 缺少更多功能,如更多执行、定期执行、线程中断
  • 线程池的好处
    • 重用存在的线程,减少对象创建、消亡的开销,性能佳
    • 可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞
    • 提供定时执行、定期执行、单线程、并发数控制等功能

线程池- ThreadPoolExecutor

  • corePoolSize:核心线程数量
  • maximumPoolSize:线程最大线程数
  • workQueue:阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响
  • keepAliveTime:线程没有任务执行时最多保持多久时间终止
  • unit: keepAliveTime的时间单位
  • threadFactory :线程工厂,用来创建线程
  • rejectHandler :当拒绝处理任务时的策略

image.png

  • execute ( )︰提交任务,交给线程池执行
  • execute ( )︰提交任务,交给线程池执行
  • submit( )︰提交任务,能够返回执行结果
  • execute+Futureshutdown ( )︰关闭线程池,等待任务都执行完
  • shutdownNow( ) ∶关闭线程池,不等待任务执行完
  • getTaskCount ( )︰线程池已执行和未执行的任务总数
  • getCompletedTaskCount ( ) ∶已完成的任务数量
  • getPoolSize ( )︰线程池当前的线程数量
  • getActiveCount ( ) :当前线程池中正在执行任务的线程数量

线程池类图

image.png

线程池-Executor框架接口

  • Executors.newCachedThreadPool
  • Executors.newFixedThreadPool
  • Executors.newScheduledThreadPool
  • Executors.newSingleThreadExecutor

线程池–合理配置

  • CPU密集型任务,就需要尽量压榨CPU,参考值可以设为NCPU+1
  • IO密集型任务,参考值可以设置为2*NCPU

死锁

image.png

死锁-必要条件

  • 互斥条件
  • 请求和保持条件
  • 不剥夺条件
  • 环路等待条件

多线程并发最佳实践

  • 使用本地变量
  • 使用不可变类
  • 最小化锁的作用域范围:S=1/(1-a+a/n)
  • 使用线程池的Executor,而不是直接new Thread执行
  • 宁可使用同步也不要使用线程的wait和notify
  • 使用BlockingQueue实现生产-消费模式
  • 使用并发集合而不是加了锁的同步集合
  • 使用Semaphore创建有界的访问
  • 宁可使用同步代码块,也不使用同步的方法
  • 避免使用静态变量

Spring与线程安全

  • Spring bean : singleton、prototype
  • 无状态对象

HashMap 与ConcurrentHashMap

image.png

image.png

  • DEFAULT_INITIAL_CAPACITY:初始容量
  • MAXIMUM_CAPACITY:最大容量
  • DEFAULT_LOAD_FACTOR:加载因子 当hash表中的条目数量超过了DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR的值,就会调用 resize()方法进行扩容,然后将容量进行分类

image.png

hashmap的寻址方式

 对于一个新插入的数据,或者我们需要读取的数据,hashmap需要将它的key,按照一定的规则计算出它的hash值,并对我们的数组长度进行取模,结果作为它在数组中的索引值index(在计算机中,取模的代价远大于位运算的代价,hashmap要求数组的长度为2的n次方,此时将key的hash值与2的n-1次方进行与运算它的结果与我们的取模操作是相同的);hashmap并不要求我们传入的容量值为2的n次方的一个整数,而是在进行容量初始化时计算出一个满足2的n次方的容量。

image.png

image.png

众所周知,hashmap是线程不安全的,主要体现在调用resize()方法时,容易陷入死循环以及使用迭代器时容易出现fail-fast这上面(当hashmap的size超过DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR的时候就需要对hashmap进行扩容,具体方法时需要创建一个容量为原来两倍的数组,并保证新的数组的容量仍然为2 的n次方,以保证hashmap的寻址方式仍然使用,同时要将原来数组中的元素重新插入到新的数组,这个过程我们称作rehash,这个方法并不保证线程安全,而且在多线程并发调用时,容易陷入死循环)。

单线程情况下的rehash

此时是没有问题的 image.png

多线程并发下的rehash

image.png

image.png

image.png

image.png 出现循环链表,11无法再加入线程1的链表中,一旦下一次访问这个链表就会出现死循环。

  • 快速失败(fail-fast)问题:出现原因:我们在使用迭代器的过程中,如果hashmap被修改了,就会抛出# ConcurrentModificationException。在多线程条件下,我们可以使用Collections的sychornizedMap方法构造出一个同步map,或者直接使用ConcurrentHashMap,来保证不会出现快速失败(fail-fast)策略。

ConcurrentHashMap

image.png

image.png

多线程并发与线程安全总结

image.png

高并发处理思路与手段

扩容

  • 垂直扩容(纵向扩展)∶提高系统部件能力
  • 水平扩容(横向扩展)∶增加更多系统成员来实现

扩容-数据库

  • 读操作扩展: memcache、redis、CDN等缓存
  • 写操作扩展:Cassandra、Hbase等

缓存

image.png

缓存特质

  • 命中率∶命中数/(命中数+没有命中数)
  • 最大元素(空间)
  • 清空策略:FIFO,LFU,LRU,过期时间,随机等

缓存命中率影响因素

  • 业务场景和业务需求
  • 缓存的设计(粒度和策略)
  • 缓存容量和基础设施

缓存分类和应用场景

  • 本地缓存∶编程实现(成员变量、局部变量、静态变量).Guava Cache
  • 分布式缓存:Memcache、Redis

缓存-Guava Cache

image.png

缓存-Memcache

image.png

image.png

缓存-redis

image.png

  • 特点:

缓存一致性

image.png

缓存并发问题

image.png

缓存穿透问题

image.png

缓存的雪崩现象

  • 缓存的颠簸问题(缓存抖动)-一种比雪崩更轻微的故障,但是也会在一定时间内对系统造成性能影响,一般由缓存节点的故障的影响,业内提倡用一致性hash算法来解决
  • 缓存雪崩:由于缓存的原因导致大量的请求到达后端数据库,从而导致数据库崩溃,导致整个系统崩溃,发生灾难。原因有很多种,比如缓存并发,缓存穿透,缓存抖动等的发生,这些问题也可能会被恶意攻击者所利用,还有一种情况,例如某个时间点内,系统预加载的缓存周期性的集中失效,为了避免这种周期性的失效,可以设置不同的过期时间,来错开他们的缓存过期时间,从而避免缓存集中失效。从架构应用角度,我们可以通过限流、降级、熔断等手段来降低影响,可以通过逐级缓存来避免这种灾难。此外,从整个研发体系的角度,应该多加强压力测试,尽量模拟真实场景,更早的暴露问题,从而进行防范。

缓存高并发实战-股票分时线

image.png 手记:

image.png image.png image.png image.png image.png

image.png

消息队列(需单独学习相应的消息队列)

image.png

消息队列特性

  • 业务无关:只做消息分发
  • FIFO:先投递先到达
  • 容灾:节点的动态增删和消息的持久化
  • 性能:吞吐量提升,系统内部通信效率提高

为什么需要消息队列

  • 【生产】和【消费】的速度或稳定性等因素不一致

消息队列好处

  • 业务解耦
  • 最终一致性
  • 广播
  • 错峰与流控

消息队列举例

  • Kafka
  • RabbitMQ

队列-Kafka

image.png

队列-RabbitMQ

image.png 启动: image.png image.png

应用拆分

image.png

应用拆分–原则

  • 业务优先
  • 循序渐进
  • 兼顾技术:重构、分层
  • 可靠测试

应用拆分-思考

  • 应用之间通信:RPC ( dubbo等)、消息队列
  • 应用之间数据库设计:每个应用都有独立的数据库
  • 避免事务操作跨应用

服务化 Dubbo

image.png

微服务Spring Cloud

  • 微服务 image.png

image.png

应用限流

image.png

应用限流–算法

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

image.png

服务降级与服务熔断

  • 服务降级
  • 服务熔断

服务降级分类

  • 自动降级∶超时、失败次数、故障、限流
  • 人工降级∶秒杀、双11大促等
  • 共性:目的、最终表现、粒度、自治
  • 区别∶触发原因、管理目标层次、实现方式

服务降级要考虑的问题

  • 核心服务、非核心服务
  • 是否支持降级,降级策略
  • 业务放通场景,策略

Hystrix

  • 提供失败回退(Fallback )和优雅的服务降级机制

image.png image.png

数据库切库、分库、分表

数据库瓶颈

  • 单个库数据量太大(1T~2T):多个库
  • 单个数据库服务器压力过大、读写瓶颈:多个库

数据库切库

  • 切库的基础及实际运用:读写分离
  • 自定义注解完成数据库切库–代码实现

数据库支持多个数据源与分库

  • 支持多数据源、分库
  • 数据库支持多个数据源–代码实现 image.png

数据库分表

  • 什么时候考虑分表
  • 横向(水平)分表与纵向(垂直)分表
  • 数据库分表:mybatis分表插件shardbatis2.0

高可用的一些手段

  • 任务调度系统分布式:elastic-job + zookeeper
  • 主备切换:apache curator + zookeeper分布式锁实现
  • 监控报警机制
# 简谈从零开始搭建一套业务相关监控报警系统

2017.10.29 21:58 12208浏览

**声明**

手记内容主要为自己在公司从零搭建完整的基于业务的监控报警系统的过程,这个过程中许多决策都不是唯一选择,且主要是基于Java项目来做的。

**正文**

监控对企业级项目特别重要,许多企业都已经拥有了自己的监控报警系统,监控报警系统的重要性就不在这里列举了。那么,从零开始搭建一套完整的业务报警系统要做哪些事情呢?

**客户端**

首先,需要能生成监控。这时候需要写一个客户端,客户端提供方法给业务端调用来生成监控。客户端都要完成哪些内容呢?

通常监控是每分钟生成一个监控点,客户端需要提供的核心包括:发生了一次或多次(计算QPS)、发生一次花费的时间(计算每次平均花费时间,主要用于对接口的监控)、记录影响的个数(主要记录某个方法单位时间影响结果的总数)、记录最后一次影响的个数(主要记录某个方法单位时间内最后一次执行影响结果的监控),不同的接口可以通过生成不同的监控后缀来区分。在此基础上,还要能支持成功、失败及成功和失败的占比。

以上主要是对业务的支持,对于java项目而言,客户端还可以主动抓取tomcat和jvm信息,每分钟生成一个点。

除此,客户端要提供一个指定接口用于被服务器端抓取最近一分钟的监控,业务端配置这个接口可以被指定的服务器顺利访问。这里没有选择客户端主动推送,是为了让服务器配置合法的客户端,这样控制起来更容易,也可以防止某些环境(dev、beta)不需要记录监控的值不进入服务器端。

**服务端**

说完客户端,再说一下服务器端。服务器端都要完成哪些内容呢?

接着客户端最后一个点,首先服务器端需要能自己配置对应的客户端地址。简单的方式是直接配置允许的ip:port, 但是对于在docker上部署的项目,ip可能是动态的,这是可以选择配置域名或者读取部署的动态ip列表的接口解析完成。

确定好待抓取的客户端后,接下来就是每分钟遍历客户端去抓每个客户端生成的监控点了,如果客户端过多,可以考虑多开几个进程来抓。

监控点都被抓到服务器端后,需要对每个客户端抓到的监控点进行解析处理。比如,记录qps的监控需要根据这一分钟的次数计算每秒钟的次数,除此,还有一个特别重要的需要处理,就是相同项目多个客户端端数据的merge,在记录监控时,也要记录下单机的监控和整体的监控。

解析完监控数据后,需要对监控点进行入库。监控点这种数据的保存,不能使用普通的关系型数据库来存储了,而要选择时间序列数据库,这里推荐一个开源软件:Graphite。Graphite 支持 Whisper、Carbon和graphite-api, Whisper为固定大小的时间序列数据库,Carbon为数据接收服务,接收到数据以后存储到Whisper中,而graphite-api 则作为WSGI webapp 接口服务,提供的 REST API 进行数据的获取。

说到这里,就不得不提到另外一个开源软件:Grafana。Grafana 主要做数据展示的,grafana可以很好的支持graphite-api 来进行数据渲染。

这样一来,服务器端抓取到监控数据,解析完交给carbon,carbon接收到数据到存储到whisper中,grafana则通过graphite-api 读取监控数据渲染监控页面。如此,监控系统就ok了。

**(语音)报警**

graphite 这个软件真的是无比强大,除了可以和grafana 搭配使用外,他还可以和zabbix这个报警搭配使用。通过脚本每分钟把数据同步到zabbix后,就可以zabbix中配置报警了,zabbix如何配置报警就不再这里详细说明了。

zabbix报警本身支持邮件通知、SMS通知及外部脚本通知等,这里重点说一下外部脚本通知:通过外部脚本可以引入语音报警。这里重要说一下我们是如何通过外部脚本完成语音报警的。我们在zabbix里配置了使用外部脚本通知,在脚本里首先是做邮件通知,之后调用一个接口,通知的内容作为参数。调用的接口是单独写了一个新项目提供的。接口在处理时,会根据报警的内容做一些基于业务的分析,比如某些时间段不需要通知、包含某些关键字不需要通知等等,在确定要做语音报警后,调起twilio的接口进行通知,然后根据twilio提供的接口检查语音通知状态,如果发现语音通知的人没有接起电话,进行升级处理,尽量让报警能语音通知到人。

关于语音报警,国内的阿里大于也很不错的,但是收费方式不同。twilio和阿里大于的唤起语音的报警的接口使用起来都比较方便,自己是花一天半左右的时间完成了对api的理解和对业务的封装。

**总结**

最后总结一下我们公司搭建的监控报警系统。首先是自己手写了客户端和服务端,这个过程会涉及到许多并发的处理和对监控数据细节处理,之后引入了Grafana + Graphite + Zabbix 这套可以完成监控数据处理、展示、报警的开源组件,这三个组件更多涉及到的是安装和配置,以及个别脚本的变写,最后又手写了一个语言报警通知系统,通过调用twilio接口完成语音通知。

课程总结

image.png

image.png

image.png

j.U.C相关类可去阅读相关类的源码 image.png