并发编程知识整理

并发的基本概念
在这里插入图片描述

高并发
通过设计保证系统能够同时并行处理很多请求。
在这里插入图片描述

我们讨论并发时候多是考虑的保证线程安全,合理使用资源。
而高并发,是指服务能同时处理很多请求,提高程序性能。否则并发量过高,将会出现降低用户体验度,请求时间变长,导致系统宕机。还有会导致OOM异常,系统停止工作。
要想解决高并发问题要从多个方面解决。
比如硬件,网络,系统架构,语言,数据结构,数据库优化,算法优化等。

课程大纲
三部分
见下图吧
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

主存 和高速缓存都连在系统主线上。

现在已经有了二级/三级缓存了。
在这里插入图片描述
缓存内存很小,不能将所有的CPU内存都进行缓存,那么意义在哪里?
在这里插入图片描述在这里插入图片描述

定义了四种状态,CPU对缓存的操作可能会造成数据不一致。
在这里插入图片描述

状态转换和转换关系
在这里插入图片描述在这里插入图片描述

在这里插入图片描述
用内存屏障解决
在这里插入图片描述
Java内存模型jmm是一种规范,规范了Java虚拟机和计算机内存是如何协同工作的,规定了一个线程如何 和 何时可以看到其他线程修改过的共享变量的值。以及在必须时如何同步的访问共享变量。
在这里插入图片描述
Heap:堆 运行时数据区,由垃圾收集器来负责,动态分配内存大小,存取速度稍微慢一些
Stack:栈 存取速度比堆快,仅次于寄存器。数据共享,存储的数据的大小要提前声明好,缺乏灵活性。存储的时基本数据类型。
调用栈和本地变量存放在栈中,对象存在堆中。引用存在栈中。
静态成员变量,跟类一起存放在堆上。
两个线程同时访问一个对象中的方法,里边的成员变量都会在自己的区域开辟空间,进行数据的私有拷贝,值互不干扰。
在这里插入图片描述
当一个CPU需要读取主存的时候,他会先将主存的部分读取到缓存中,甚至读取到寄存器中,在寄存器中执行操作。完毕后将结果从寄存器刷新到缓存中,在某个时间点将值刷新回主存。
这样处理器就不用等待缓慢的内存读写了
在这里插入图片描述
Java的内存模型和计算机硬件内存模型的关系
在这里插入图片描述
两个线程通信一定要通过主内存
所以容易出现并发问题,需要用同步方法取解决
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述![在这里插入图片描述](https://img-blog.csdnimg.cn/20200115233055218.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NsYW1KMDky,size_16,color_FFFFFF,t_70

线程安全

有多个线程在同时运行同一段代码,如果每次运行的结果和单线程运行的是一样的,其他的变量的值也和预期是一样的,我们就认为是线程安全的。就是并发环境下,得到我们期望的结果。

线程不安全

就是不提供数据的访问保护,有可能出现多个线程先后更改数据,造成所得到的数据跟预期不符。是脏数据,或者出现错误。

并发模拟

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

阻塞线程,并且线程在某种特定的情况下才会继续执行。能保证线程执行完毕后才能进行其他的处理。
在这里插入图片描述

可以阻塞进程,并且控制同一时间的请求的并发量,控制同时并发的线程数。

以上两种使用的时候会和线程池一起使用。
我们模拟并发测,并在所有线程执行完输出一些结果,这两个线程结合一起使用是比较合适的。
线程安全性
当多个线程访问某个类的时候,不管运行时环境采用何种调度方式,或者这些进程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

线程安全性
原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。
可见性:一个线程对主内存的修改可以及时被其它线程观察到。
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排的存在,该观察结果一般杂乱无序。
在这里插入图片描述在这里插入图片描述在这里插入图片描述
源码分析
在这里插入图片描述

传过来的第一个值当前对象count,第二个值var2是当前的值,第三个值var4是变化的值,比如你是2+1,第二个值是2,第三个值是1.
Var5是通过调用底层方法,得到的底层当前的值,
原理就是传过来的值跟底层的值相同,那么就进行运算,更新成新值。如果跟底层的值不一样,那么就循环判断,,直到值一样,才进行运算,把底层的值覆盖掉。compareAndSwapInt方法就是CAS的核心。
在这里插入图片描述
Native 标识的方法标识是Java底层的方法,不是通过Java去实现的。
值不一样的原因就是线程得工作内存和主内存的工作机制造成的。Count就是工作内存,getIntVolatile的值就是主内存的值。

在这里插入图片描述在这里插入图片描述在这里插入图片描述

区别:atomic Long底层是死循环,如果一直获取不到一样的值,就有一直循环,影响性能。
LongAdder,使用特殊的算法,将单节点上的运算压力,分散到一个各个节点上,在低并发的时候,通过对base的直接更新,效率跟atomic Long一样,高并发的时候分散热点提高了性能。
缺点是在统计的时候,如果有并发更新,可能导致统计的数据有些一误差。
在线程竞争很低的时候,还是使用atomic Long。效率也稍高一点点。生成id号等需要准确并且全局唯一的数值时,就要使用atomic Long。
在这里插入图片描述

这个方法可以保证我们需要控制的那一段代码只执行一次。就是保证只有一个线程可以执行这个代码。可以控制,将标识改为false,这样既可以确保这一段代码同一时间只有一个线程可以进行操作。

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述

相比之前的方法,多了一个版本号的比较,如果修改过,版本号自动+1,就不会出ABA问题了。
还有对数组进行原子性操作的类。多了一个索引值让我们去更新。给定一个索引,告诉期望值,更新成哪个值。
在这里插入图片描述在这里插入图片描述

AtomicBoolean
在工作中,某些代码只执行一遍,就可以用这个方法。
在这里插入图片描述

原子性操作除了atomic还有锁机制。
在这里插入图片描述
作用对象的作用范围内保证原子性操作
在这里插入图片描述
Jdk提供的代码层面的锁。
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

就是说,读写volatile修饰的变量的时候,都是直接从主内存中取值,并刷新到主内存中。下面两个图都是在CPU级别进行操作的。
在这里插入图片描述在这里插入图片描述

使用volatile并不能保证线程安全,他不能保证原子性
如果想使用,必须保证:
1:对变量的写操作,不依赖于当前值。
2:该变量没有包含在具有其他变量的不变式子中
所以volatile特别适合作为状态标记量。
还有一个就是做双重检测的时候使用。懒汉单例模式,禁止指令重排

在这里插入图片描述
指令重排
在这里插入图片描述

在这里插入图片描述
后两个通过保证只能有一个线程进行操作来实现有序性。
第一个是内存屏障
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

如果两个操作的执行顺序,不能根据以上原则推导出来,那么就不能保证他们的有序性。虚拟就就可以随意的对他们进行重排序。

在这里插入图片描述
发布对象:使一个对象能够被当前范围之外的代码所使用。
对象逸出:一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程看见。
我们经常通过类的非私有方法返回对象的引用,或者通过共有静态变量发布对象。
有代码
在这里插入图片描述在这里插入图片描述

在这里插入图片描述

不可变对象需要满足的条件
1:对象创建以后,其状态就不能修改
2:对象所有的域都是final类型
3:对象是正确创建的(在对象创建期间,this引用没有逸出)
将类声明为final,就不能被继承了,所有的成员声明为私有的,就不允许直接访问这些成员了,对变量不提供set方法,将所有可变成员设置为final,只能对他们赋值一次,通过构造器初始化所有成员,进行深度拷贝。在get方法中,不直接返回对象本身,而是克隆对象,返回对象的拷贝。比如string。
在这里插入图片描述
Final修饰的类不能被继承,final中的成员方法都会被隐式的指定为final方法,final类中的成员变量可以根据需要用final修饰。
Final修饰方法,锁定方法不能被继承类修改它的含义。不希望被子类重写。一个private方法会被隐式的指定为final方法。早期版本中final修饰的方法会被转为内嵌调用,会提高效率,但是方法过于庞大,也会看不出内嵌带来的效率提升。不过现在不需要通过内嵌来实现提升效率了。
Final修饰变量:基本数据类型在初始化之后就不能再修改了。引用类型变量,在初始化之后就不能让他再指向另外一个对象了。

其他方法定义不可变对象

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

原理就是新建了一个map对象,数值全部拷贝过来,然后把所以更新的方法都做了异常抛出。

在这里插入图片描述在这里插入图片描述

线程封闭:
实现好的并发是一件困难的事情,所以很多时候我们都想躲避并发。避免并发最简单的方法就是线程封闭。什么是线程封闭呢?
就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。
1:ad-hoc线程封闭
这是完全靠实现者控制的线程封闭,他的线程封闭完全靠实现者实现。Ad-hoc线程封闭非常脆弱,没有任何一种语言特性能将对象封闭到目标线程上。
2:栈封闭
栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问一个方法,此方法中的局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。
3:ThreadLocal封闭
使用ThreadLocal是实现线程封闭的最好方法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。
所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。

Threadlocal
我例子写的就是在用户线程到达控制层之前就先将线程的数据封存到Threadlocal中,这样在以后需要用户信息的时候,直接在Threadlocal中取出来就可以了。如果不这样做,用户的信息在request中,我们就要把用户的信息不停的从controller往下传,可能还会传到util类中去。代码看起来也不舒服。使用过滤器和Threadlocal,就可以先把数据取出来,什么时候用,什么时候从Threadlocal中取就行了。

经典的线程封闭 数据库连接对应JDBC的 connection对象,将这个对象封闭在线程里边,虽然它本身不是线程安全的,但是通过线程封闭,也做到了线程安全。

在这里插入图片描述

什么是线程不安全的类
如果一个类的对象可以同时被多个对象访问,如果你不做同步或者并发处理,那么这个类就会表现出线程不安全的现象。比如抛异常,逻辑处理错误。就是不安全的类。
在这里插入图片描述
在方法内部,使用string buffer就可以了,因为线程封闭,只有单个对象操作资源。所以肯定是安全的。

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述

都是不安全的。因为它们都是:分两步操作的,即使前后安全。间隙也不安全。
在这里插入图片描述

在这里插入图片描述
第一类 提供的
在这里插入图片描述
第二类 工具类提供的静态工厂方法创建的类

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

底层add方法加了lock。
写操作时复制,当有新元素添加到copyonwritearraylist中的时候,它先从原有的数据拷贝一份出来,在新的数组做写操作,写完之后再将旧数组指向到新的数组,整个add操作都是在锁的保护下进行的,避免在多线程并发做add操作的时候,复制出多个副本出来,把数据搞乱了,导致最终的数据不是我们期望的。
缺点,
1:由于它在做写操作的时候,需要拷贝数组,就会消耗内存,如果数据量比较大的时候,就会触发GC。
2:不能用于实时读的场景,因为他要在新数组ADD,所以我们在set后读,得到的数据可能是旧的。它虽然能保证最终一致性,但是不能满足实时一致性的要求。
它适合读多写少的操作。
在数据量不大的时候,可以使用copyonwritearraylist容器代替array list。
设计思想:读写分离,最终一致性,使用时另外开辟空间来解决并发冲突。读操作在原数组,不加锁,写操作的时候需要加锁,避免多个线程并发修改,复制出多个副本出来。
在这里插入图片描述在这里插入图片描述在这里插入图片描述

普通的ADD 一样,但是AddAll,各种All操作(containsAll,removeAll等),并不能保证以原子方式执行,他们底层还是调用的Add操作,每个单独都是原子性的,但是不能保证每一次都不会被其他线程打断。所有的All操作还是要自己同步。比如加上锁,同一时间只允许一个线程调用操作。
concurrentSkipListSet不能存null元素,因为它不能将null和不存在的值区分开。

在这里插入图片描述
concurrentHashMap不允许空值,线程安全,对于读操作做了大量的优化,所以在高并发场景下有特别好的表现。
concurrentSkipListMap,(treemap)的安全版本。

concurrentHashMap在一定的线程数内,效率是大于concurrentSkipListMap。
concurrentSkipListMap:key有序,支持更高的并发,存取时间跟线程数几乎没有关系,并发的线程数越多,越能体现优势。

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

在这里插入图片描述
使用的双向链表,是队列的一种实现。
在这里插入图片描述
First in。First out。(先进先出)
在这里插入图片描述
有一个叫state的成员变量,有一个同步组件ReentrantLock,表示获取锁的线程数,0表示没有线程获取锁,1表示有线程获取锁,大于1表示重入锁的数量。
在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述

AQS功能主要分为两类,独占和共享。子类要么实现独占,要么实现共享,而不会同时实现两个API,即使最有名的子类ReentrantReadWriteLock也是通过两个内部类,读锁和写锁分别实现了两套API来实现的。记住AQS在功能上有独占控制和共享控制两种功能。
同步器AQS内部的实现是依赖同步队列(一个FIFO的双向队列,其实就是数据结构双向链表)来完成同步状态的管理。
当前线程获取同步状态失败时,同步器AQS会将当前线程和等待状态等信息构造成为一个节点(node)加入到同步队列,同时会阻塞当前线程;
当同步状态释放的时候,会把首节点中的线程唤醒,使首节点的线程再次尝试获取同步状态。AQS是独占锁和共享锁的实现的父类。

Countdown latch 是一个闭锁,通过计数来判断当前线程是否需要一直阻塞。
Semaphore:控制同一时间并发线程的数目。
CyclicBarrier:和countdown latch一样,都能阻塞进程
ReentrantLock:可重入锁
Condition:
FutureTask:
在这里插入图片描述

是一个同步辅助类,使用它可以完成阻塞当前线程的功能。
使用一个给定的计数器初始化,该计数器的操作是原子操作,就是同时只能有一个线程操作该计数器。
调用await的时候阻塞,每调用一次countdown方法计数器的值就会减一,计数器直到为0,解除阻塞。计数次数不能重置。如果考虑重置计数次数,那么可以考虑CyclicBarrier。
使用场景:程序执行需要某个条件执行完毕后才能继续执行,比如并行计算,某个运算的计算量很大的时候,可以将任务拆分成多个子任务,等所有子任务运算结束后,副任务拿到所有子任务的运算结果,进行汇总。确保所有的请求处理完,再去输出最终的结果。做并发测试的时候,每条线程都是一个子任务,加在一起就是预期值。

在这里插入图片描述在这里插入图片描述

信号量,可以控制某一个资源被同时访问的个数。
和countdown latch一样,有两个核心方法,分别是acquire和release(获取一个许可,如果没有就等待。和释放,在操作完成后释放一个许可)。通过同步机制控制同时访问的个数。semaphore可以实现有限大小的链表。
使用场景:仅能提供有限访问的资源,比如数据库的链接数,假设为20,但是上层应用的并发数远远大于20。如果不控制,就会因为连接不上数据库导致的异常,这个时候我们就可以通过信号量做并发访问控制。

在这里插入图片描述
一次多个许可
在这里插入图片描述
上边设置的允许并发3.下边设置的每次拿三个许可。相当于一上来就被拿走了。
丢弃无用的线程
在这里插入图片描述
源码分析
在这里插入图片描述
传入int 表示一次可以尝试获取多少个许可,如果获取不到,就丢弃。
传入超时时间,和时间单位,实时获取太过匆忙,可以等1秒再获取一次。如果没有获取到,那就丢弃吧。
第三个,传入上述三个参数,既有许可数,又有超时时间。
在这里插入图片描述
在这里插入图片描述
同步辅助类,[人满发车]
允许一组线程相互等待,直到到达某个公共的屏障点。通过它可以实现多个线程之间的相互等待,只有所有线程都就绪后,各自的线程才能继续往下执行后面的操作。
跟countdown latch一样都是通过计数器来实现的。
不过要从下向上看,相反的,调用的await以后,就进入等待状态,当计数器达到我们设置的初始值的时候,等待的线程就会被唤醒,继续他们的任务。
因为释放之后可以重用,所以我们称他为循环屏障。
业务场景:多线程计算数据,最后合并结果。
比如用一个excel保存了用户的银行流水,一页保存了一年的每一笔流水,需要统计用户的日均银行流水,就可以先用多线程处理每页的流水,再统计到一起,算日均银行流水。
Countdown latch 和cyclic Barrier的区别
Countdown latch的计数器只能使用一次。
cyclic Barrier的计数器可以使用release 方法重置,就是释放,所以可以多次使用。
Countdown latch是一个或者多个线程,等待其他线程完成某个操作之后,再开始执行。
cyclic Barrier是实现多个线程相互之间等待,直到所有线程都满足条件之后,才能继续执行后续的操作。描述的是各个线程之间相互等待的关系。比如设置为5,只有达到有5个线程都在await的时候才能进行后续操作。所以他能处理更发杂的业务场景,比如,如果计算发生错误了,我可以重置计数器,并且让线程重新执行一次。
cyclic Barrier还提供了其他很多方法,比如getNumberWaiting():获取正在CyclicBarrier上等待的线程数量。
isBroken(),表示阻塞的线程是否被中断了。
获取是否破损标志位broken的值,此值有以下几种情况:
CyclicBarrier初始化时,broken=false,表示屏障未破损。
如果正在等待的线程被中断,则broken=true,表示屏障破损。
如果正在等待的线程超时,则broken=true,表示屏障破损。
如果有线程调用CyclicBarrier.reset()方法,则broken=false,表示屏障回到未破损状态。

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Java一共有两方面的锁,一种是synchronized,一种是JUC提供的锁。

JUC的核心锁就是ReentrantLock(可重入锁)。
在这里插入图片描述
1:都是可重入锁。
2:锁的实现:
Synchronized是jvm实现(操作系统实现)
ReentrantLock是jdk实现的(代码实现)
3:性能:
在优化之前,Synchronized的性能很差,不过现在Synchronized提供了偏向锁,轻量锁(自旋锁)后,两者的性能就差不多了,官方建议Synchronized。实际也是借用了CAS技术,在用户态就把加锁解决,避免进入线程态的内核阻塞。
4:功能
Synchronized由编译器解决加锁和释放,方便简洁。
ReentrantLock需要手动声明加锁和释放锁。为了避免忘记释放锁,要在finally中声明释放锁。
细腻度和灵活度ReentrantLock更胜一筹。
在这里插入图片描述
1:可以指定公平锁还是非公平锁,
Synchronized只能是非公平锁。
(公平锁是每次都在队列队首取值,非公平锁随机的)
2:提供了一个condition类,可以分组唤醒需要唤醒的线程。
Synchronized只能随机唤醒一个或者全部唤醒。
3:提供了一种能够中断等待锁的线程机制,lock.lockInterruptiliy()
ReentrantLock是一种自旋锁,通过循环调用CAS操作实现加锁。性能好也是因为避免了线程进入内核态(这是设计的关键)的阻塞状态。
场景:需要ReentrantLock的特性,就要使用这个。
Synchronized不需要手动释放锁,而且还有synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定。

在这里插入图片描述
实现LOCK接口。
源码分析
默认给一个不公平锁
在这里插入图片描述
也可以通过传入true或者false来设定是公平锁还是非公平锁。
在这里插入图片描述
方法
在这里插入图片描述
a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
c)tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或得锁定,或者当前线程被别的线程中断。
e) isLocked :查询此锁定是否由任意线程保持。
f) isHeldByCurrentThread:查询当前线程是否保持锁定状态。
g) isFair:判断是不是公平锁
通过丰富的函数,使得功能特别强大。
在这里插入图片描述
ReentrantReadWriteLock上图
在没有任何读写锁得时候,才可以取得写入锁。
悲观读取。
在这里插入图片描述
StampedLock
控制锁有三种模式,写,读,乐观读
1、StampedLock是做什么的?
----- 它是ReentrantReadWriteLock 的增强版,是为了解决ReentrantReadWriteLock的一些不足。
2、ReentrantReadWriteLock有什么不足之处呢?
------ 我们都知道,ReentrantReadWriteLock是读写锁,在多线程环境下,大多数情况是读的情况远远大于写的操作,因此可能导致写的饥饿问题。(换人话来说的话,读操作一直都能抢占到CPU时间片,而写操作一直抢不了)
3、为什么会导致写操作会出锁饥饿问题呢?
----- ReentrantReadWriteLock写锁的互斥的
(读和读—不互斥,读和写—互斥,写和写----互斥),懂了吗?
4、正因为ReentrantReadWriteLock出现了读和写是互斥的情况,这个地方需要优化,因此就出现了StampedLock!
5、StampedLock是读锁并不会阻塞写锁。这里就有朋友会问,如果这样设计的话,那么怎样保证读和写的一致性呢?
----- StampedLock的设计思路也比较简单,就是在读的时候发现有写操作,再去读多一次。(思想上来说)
6、那下一个问题就是StampedLock是怎样知道读的时候发生了写操作呢?
----- 我们的StampedLock有两种锁,一种是悲观锁,另外一种是乐观锁。如果线程拿到乐观锁就读和写不互斥,如果拿到悲观锁就读和写互斥。
悲观锁:每次拿数据的时候就去锁上。
乐观锁:每次去拿数据的时候,都没锁上,而是判断标记位是否有被修改,如果有修改就再去读一次。
(就像很多个人去桌子上看今天老师布置的作业题,另外桌子旁边有一个牌子,红色面代表“作业题已经被修改过了”,白色面代表“题目是最新的”。第一个人去看作业题,再看了下牌子,牌子是白色的,作业题是最新的。但是有一个人去看作业题,看完之后,再看隔壁的牌子,牌子变成红色了,于是他赶紧回去看了一下题目,果然题目已经被改过了,于是他再看了一次,再确认一次牌子颜色,都没问题之后,就放心走了。)

在这里插入图片描述

关于锁的选择
当只有少量竞争者的时候,synchronized是一个很好的选择。
线程不少,但是线程增长的趋势我们能够预估,ReentrantLock。
选适合自己的。Synchronized不会引发死锁,jvm会自动解锁。
其他对象锁都要自己手动解锁。

Condition

在这里插入图片描述在这里插入图片描述

创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。
这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。
而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
今天我们就来讨论一下Callable、Future和FutureTask三个类的使用方法.
Callable和Runnable
先说一下java.lang.Runnable吧,它是一个接口,在它里面只声明了一个run()方法
Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call(),这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。
Future接口:Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

在Future接口中声明了5个方法,下面依次解释每个方法的作用:
• cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
• isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
• isDone方法表示任务是否已经完成,若任务完成,则返回true;
• get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
• get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
  也就是说Future提供了三种功能:
  1)判断任务是否完成;
  2)能够中断任务;
  3)能够获取任务执行结果。
  因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

FutureTask:
FutureTask类实现了RunnableFuture接口。RunnableFuture继承了Runnable接口和Future接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
事实上,FutureTask是Future接口的一个唯一实现类。
支持多种构造函数 写期望一个线程去做一些事情的时候,而且还关注他的结果,以及是否正常执行,可以使用future task。方便跟多。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

Fork(分叉) /Join (合并)框架
并行任务的框架,把一个大任务分割成许多小任务,在所有小任务执行完毕后,将结果汇总得到大任务结果的框架。
工作窃取算法。
就是某个线程在其他队列中窃取任务来执行。
在这里插入图片描述
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。
干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。
而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
局限性
1:任务只能通过fork和join来实现同步机制,使用其他同步机制,那么在其他同步操作时,工作线程就不能执行其他项目了。比如,在forkjoin框架中你使用了睡眠,在睡眠期间将不会执行其他任务了。
2:我们拆分的任务不会去执行IO操作,如:读和写其他文件。
3:任务不能抛出和检查异常,必须通过必要的代码来处理他们
Fork join框架的核心是:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

Blocking Queue 阻塞队列 —联想一下生产者消费者模式
当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
同时,当一个线程对一个空的队列进行出队列操作的时候,也会被阻塞,除非有一个线程进行进队列操作。才会唤醒出队列线程。
核心工作方法:4套方法—注意每套方法对应的操作的方法名都不一样。
在这里插入图片描述

  1. ThrowsException:如果操作不能马上进行,则抛出异常
  2. SpecialValue:如果操作不能马上进行,将会返回一个特殊的值,一般是true或者false
  3. Blocks:如果操作不能马上进行,操作会被阻塞
  4. TimesOut:如果操作不能马上进行,操作会被阻塞指定的时间,如果指定时间没执行,则返回一个特殊值,一般是true或者false
    BlockingQueue的实现类

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

线程池
OOM为out of memory的简称,称之为内存溢出。
New Thread 弊端
1:每次new Thread 新建对象,性能差
2:线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多的系统资源,导致死机或者OOM。
3:缺少更多功能,如更多执行,定期执行,线程中断。
不建议直接使用New Thread去操作自己使用的线程。
线程池的好处:
1:重用存在的线程,减少对象的创建,消亡的开销,性能佳;
2:可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。
3:提供定时执行,定期执行,单线程,并发数控制等功能。

相关的类
在这里插入图片描述
三个参数:
1:corePoolSize:核心线程数量(线程池的基本大小,即在没有任务需要执行的时候线程池的大小)
2:maximumPoolSize:线程最大线程数(线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。)
3:Work Queue:阻塞队列,储存等待执行的任务,很重要,会对线程池运行过程产生重大影响。
新提交一个任务时的处理流程很明显:
1、如果线程池的当前大小还没有达到基本大小(poolSize < corePoolSize),那么就新增加一个线程处理新提交的任务;
2、如果当前大小已经达到了基本大小,就将新提交的任务提交到阻塞队列排队,等候处理workQueue.offer(command);
3、如果队列容量已达上限,并且当前大小poolSize没有达到maximumPoolSize,那么就新增线程来处理任务;
4、如果队列已满,并且当前线程数目也已经达到上限,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务。至于如何拒绝处理新增的任务,取决于线程池的饱和策略RejectedExecutionHandler。

根据线程的数量,来选择处理的方式,方式有三种,
直接切换:使用无序队列,或使用有序队列。
直接切换就是syncqueue。
使用无序队列一般是基于链表的阻塞队列,LinkedBlockingQueue,如果使用这种方式,那可以使用的最大线程数就是corePoolSize,而maximumPoolSize就不会起作用了。当线程池中所有的核心线程都是运行状态的时候,这时一个新的任务提交之后就会放到等待队列中去。
workQueue有序队列。一般使用ArrayBlockingQueue,使用这种方式可以将最大线程数量限制为maximumPoolSize。降低资源的消耗。但是这种方式实现线程池,对线程的调度更加困难。因为线程池和容量都是有限的。所以要想线程池处理任务和吞吐量达到一个合理的范围,又希望线程调度相对简单,并且还要尽可能降低线程池对资源的消耗,我们就要合理的设置这两个数量。

如果想降低系统资源的消耗,包括CPU的使用率,操作系统资源的消耗,上下文环境的开销等等,可以设置一个较大的队列容量,和较小的线程池容量,这样会降低线程任务的吞吐量。如果我们提交的任务经常发生阻塞,我们可以调用设置线程最大数方法,来重现设置线程池容量。如果队列的容量设置的较小,通常把线程池设置点大一点,这样CPU的使用率会高一点。但是如果容量设置的过大,在提交任务数量太多的情况下,并发量会增加,那么线程之间的调度,就是一个要考虑的问题了,这样反而会降低处理任务的吞吐量。
在这里插入图片描述
线程池维护线程所允许的空闲时间。就是线程数超过最大线程数之后不会立即销毁,会允许它等候一段时间。
在这里插入图片描述在这里插入图片描述

有一个默认的生产线程的工厂,并且会有相同的优先级,同时也设置了线程的名称
在这里插入图片描述
如果阻塞队列满了,并且没有空闲的线程,
这时如果有了新任务,线程池就会采取策略,默认采取四种策列。
策略1:直接抛出异常(默认) 抛出java.util.concurrent.RejectedExecutionException异常
策略2:调用者所在的线程执行任务
策略3: 丢弃队列中最靠前的任务,并执行当前任务
策略4:直接丢弃该任务

源码
构造方法有4种,找一个参数最多的,上边有
在这里插入图片描述
拒绝策略源码

在这里插入图片描述

ThreadPoolExecutor

线程池实例的状态
在这里插入图片描述
1、RUNNING
(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(02) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

2、 SHUTDOWN
(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
3、STOP
(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4、TIDYING
(1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5、 TERMINATED
(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

线程池的方法
在这里插入图片描述
监控的方法
在这里插入图片描述
可以每分钟获取线程的上述数据,进行图表展示,来监控线程池的状态。
在这里插入图片描述
Executors
在这里插入图片描述
创建线程池的方式:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

在这里插入图片描述

死锁
在这里插入图片描述

死锁发生的必要条件
1:互斥条件(同一时间只能一个线程访问资源,其他线程只能等待)
2:请求和保持条件(线程已经持有一个资源但是又提出了一个新的资源请求,并且改资源已经被其他新线程占有,请求线程阻塞,但是又对自己的资源保持不放)
3:不剥夺条件(进程已经获得资源,不能被剥夺,线程执行完毕会自动释放资源)
4:环路等待条件

避免死锁:
1:注意顺序,先给其中一个资源加锁。
2:加锁时限:超过多久就放弃该资源,释放锁,比如不用synchronized。用reentrantlock,设置超时时间。
3:死锁检测:

多线程并发的最佳实践
1:使用本地变量,不是创建一个类或者实例变量
2:使用不可变类,可以降低代码中需要同步的数量
3:最小化锁的作用域范围 s=1/(1-a+a/n)
a为并行计算部分所占比例,
n为并行处理结点个数。
“阿姆达尔定律”
4:使用线程池的executor 而不是直接new Thread执行。
5:宁可使用同步也不要使用线程的wait和notify方法。1.5之后并发工具包也有很多同步的工具。
6:使用blocking Queue实现生产者-消费者模式。不只可以处理单个生产和消费也可以处理多个生产和消费。
7:使用并发集合,而不是加了锁的同步集合。
8:使用semaphore创建有界的访问,就是限制资源开销,控制并发数。
9:宁可使用同步代码块也不要使用同步方法,如果要更改共同的变量或者类的字段,首先要选择原子性变量,然后用volatile,如果用互斥锁可以使用reentrantlock。
10:避免使用静态变量,如果要用,就用final修饰,如果是要保存集合,可以用只读集合,否则要做很多同步,并发处理。

Spring的线程安全
Spring提供了容器管理这些bean但并没有保证这些对象的线程安全,它对每个bean提供了bean提供了scop(作用域)属性,就是bean的生命周期。
Springbean :singleton,prototype
Singleton是默认的作用域,在第一次被注入时,会创建一个单例对象,该对象会一直被作用到应用结束。生命周期于与springIoC一致,但是只会在第一次被注入时创建。
Prototype:每次注入时都会被创建一个新的对象。
无状态的
1、线程安全
  要搞清楚有状态对象和无状态对象,首先需要弄清楚线程安全的问题。如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,那么就是线程安全的。
  或者说,一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
  线程安全问题都是由全局变量及静态变量引起的。 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

2、关于线程安全
1) 常量始终是线程安全的,因为只存在读操作。
2) 每次调用方法前都新建一个实例是线程安全的,因为不会访问共享的资源(共享堆内存)。
3) 局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量和方法内变量。

3、有状态和无状态对象
  有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象 ,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。其实就是有数据成员的对象。

无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象。不能保存数据,是不变类,是线程安全的。具体来说就是只有方法没有数据成员的对象,或者有数据成员但是数据成员是可读的对象。

HashMap与ConcurrentHashMap

HashMap
在这里插入图片描述

1:初始容量
在这里插入图片描述
2;加载因子
在这里插入图片描述
3:2倍扩容
4:最大容量
在这里插入图片描述
HashMap寻址方式
对于新插入的数据或者待读取的数据,HashMap将Key的哈希值对数组长度取模,结果作为该Entry在数组中的index。在计算机中,取模的代价远高于位操作的代价,因此HashMap要求数组的长度必须为2的N次方。此时将Key的哈希值对2^N-1进行与运算,其效果即与取模等效。HashMap并不要求用户在指定HashMap容量时必须传入一个2的N次方的整数,而是会通过Integer.h ighestOneBit算出比指定整数小的最大的2^N值
在这里插入图片描述
Hashmap的线程不安全在于
当HashMap的size超过Capacity*loadFactor时,需要对HashMap进行扩容。具体方法是,创建一个新的,长度为原来Capacity两倍的数组,保证新的Capacity仍为2的N次方,从而保证上述寻址方式仍适用。同时需要通过如下transfer方法将原来的所有数据全部重新插入(rehash)到新的数组中。
该方法并不保证线程安全,而且在多线程并发调用时,可能出现死循环。其执行过程如下。从步骤2可见,转移时链表顺序反转。

  1. 遍历原数组中的元素
  2. 对链表上的每一个节点遍历:用next取得要转移那个元素的下一个,将e转移到新数组的头部,使用头插法插入节点
  3. 循环2,直到链表节点全部转移
  4. 循环1,直到所有元素全部转移
    在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

Fast-fail
产生原因
在使用迭代器的过程中如果HashMap被修改,那么ConcurrentModificationException将被抛出,也即Fast-fail策略。
多线程条件下,可使用Collections.synchronizedMap方法构造出一个同步Map,或者直接使用线程安全的ConcurrentHashMap。

ConcurrentHashMap
Java 7中的ConcurrentHashMap的底层数据结构仍然是数组和链表。与HashMap不同的是,ConcurrentHashMap最外层不是一个大的数组,而是一个Segment(分段)的数组。每个Segment包含一个与HashMap数据结构差不多的链表数组。
每个Segment都是继承JUC里的reentrantLock,所以可以很方便的对每个Segment进行上锁和做锁相关的处理。

在这里插入图片描述
Concurrent Hash Map 和hashMap的区别
• ConcurrentHashMap线程安全,而HashMap非线程安全
• HashMap允许Key和Value为null,而ConcurrentHashMap不允许
• HashMap不允许通过Iterator遍历的同时通过HashMap修改,而ConcurrentHashMap允许该行为,并且该更新对后续的遍历可见
Java 8基于CAS的ConcurrentHashMap
Java 7为实现并行访问,引入了Segment这一结构,实现了分段锁,理论上最大并发度与Segment个数相等。Java 8为进一步提高并发性,摒弃了分段锁的方案,而是直接使用一个大的数组。同时为了提高哈希碰撞下的寻址性能优化,Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))。其数据结构如下图所示
在这里插入图片描述
寻址方式
Java 8的ConcurrentHashMap同样是通过Key的哈希值与数组长度取模确定该Key在数组中的索引。同样为了避免不太好的Key的hashCode设计,它通过如下方法计算得到Key的最终哈希值。不同的是,Java 8的ConcurrentHashMap作者认为引入红黑树后,即使哈希冲突比较严重,寻址效率也足够高,所以作者并未在哈希值的计算上做过多设计,只是将Key的hashCode值与其高16位作异或并保证最高位为0(从而保证最终结果为正整数)。

在这里插入图片描述

高并发处理的思路和手段

1:扩容
垂直扩容(纵向扩展):增加内存,提高系统部件
水平扩容(横向扩展):增加服务器,增加更多系统成员

扩容-数据库
读操作扩展:memcache,redis,CDN等。
写操作扩展:Cassandra,Hbase。非关系型数据库,加个节点实惠啊。

2缓存,提高吞吐量,有限资源服务更多用户。缓存可以出现在每一个步骤中。各有特点

在这里插入图片描述

缓存特征
1:命中率:直接通过缓存获取到需要的数据。无命中,无法通过缓存得到想要的数据。
可能是缓存中没有这个数据,或者缓存过期了。
命中率越高,我们用缓存的收益越高,应用的性能越好,响应时间越短,吞吐量越高,抗并发能力越强。
在这里插入图片描述
2:最大元素,最大空间,表示缓存中可以存储的最大元素的数量。数据到达最大元素个数将会触发缓存清空策略。根据不同的场景设置最大元素,可以提高命中率,从而更有效的使用缓存。

3:清空策略:FIFO先进先出,LFU最少使用,LRU最近最少使用,过期时间,随机。
FIFO,数据实时性要求场景中使用,比较的是进入缓存的时间。
LFU,清除使用次数最少的元素来释放空间,比较元素命中次数,在保证高频数据有效场景下使用。
LRU,根据最后一次时间戳,清楚最远时间戳。热点数据场景下使用。

影响缓存命中率的因素
1:业务场景和业务需要,缓存适合读多写少的业务场景。业务需要关系着过期时间和更新策略。
2:缓存的设计,粒度和策略。粒度越小,命中率越高。比如当单个用户数据,和多个用户在一个集合中。会更加灵活,命中率会更高。缓存的更新策略,直接更新比过期或者移除缓存的命中率更高。
(1)固定过期时间,被动失效;
(2)感知数据变更,主动更新;
(3)感知数据变更,主动更新。并设置过期时间被动失效兜底;
(4)按照数据冷热性制定策略,如热数据主动失效并 reload,冷数据只失效不 reload 等。

3:缓存的容量和基础设施。
缓存的容量有限,则容易引起缓存失效和被淘汰(目前多数的缓存框架或中间件都采用了 LRU 算法)。同时,缓存的技术选型也是至关重要的,比如采用应用内置的本地缓存就比较容易出现单机瓶颈,而采用分布式缓存则比较容易扩展。所以需要做好系统容量规划,并考虑是否可扩展。此外,不同的缓存框架或中间件,其效率和稳定性也是存在差异的。

4缓存故障处理:当缓存节点发生故障时,需要避免缓存失效并最大程度降低影响,业内比较典型的做法就是通过一致性 Hash 算法,或者通过节点冗余的方式。

想要提高缓存收益,需要应用尽可能的通过缓存直接获取数据,并避免缓存失效。需要在业务需求,缓存粒度,缓存策略,技术选型等各个方面去通盘考虑并做权衡。尽可能的聚焦在高频访问且时效性要求不高的热点业务上,通过缓存预加载(预热)、增加存储容量、调整缓存粒度、更新缓存等手段来提高命中率。
在这里插入图片描述在这里插入图片描述在这里插入图片描述

灵感来源于concurrentHashMap,使用多个Segment细粒度锁。
在这里插入图片描述

内存结构

在这里插入图片描述
在这里插入图片描述
Redis是一个远程内存数据库,它不仅性能强劲,而且还具有复制特性以及为解决问题而生的独一无二的数据模型。它可以存储键(key)与5种不同类型的值(value)之间的映射(mapping)。它可以将内存中的数据持久化到硬盘,可以使用复制特性扩展读性能、还可以使用客户端分片扩展写性能,用户可以很方便地将Redis扩展成一个能够包含数百GB数据、每秒处理上百万次请求的系统。
使用场景:https://www.cnblogs.com/xiaoxi/p/7007695.html

高并发场景下缓存的常见问题
1:缓存一致性
当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象。这就比较依赖缓存的过期和更新策略。一般会在数据发生更改的时,主动更新缓存中的数据或者移除对应的缓存。
在这里插入图片描述
2:缓存并发问题
缓存过期后将尝试从后端数据库获取数据,这是一个看似合理的流程。但是,在高并发场景下,有可能多个请求并发的去从数据库获取数据,对后端数据库造成极大的冲击,甚至导致 “雪崩”现象。此外,当某个缓存key在被更新时,同时也可能被大量请求在获取,这也会导致一致性的问题。那如何避免类似问题呢?我们会想到类似“锁”的机制,在缓存更新或者过期的情况下,先尝试获取到锁,当更新或者从数据库获取完成后再释放锁,其他的请求只需要牺牲一定的等待时间,即可直接从缓存中继续获取数据。
在这里插入图片描述
3:缓存穿透问题
缓存穿透在有些地方也称为“击穿”。很多朋友对缓存穿透的理解是:由于缓存故障或者缓存过期导致大量请求穿透到后端数据库服务器,从而对数据库造成巨大冲击。
这其实是一种误解。真正的缓存穿透应该是这样的:
在高并发场景下,如果某一个key被高并发访问,没有被命中,出于对容错性考虑,会尝试去从后端数据库中获取,从而导致了大量请求达到数据库,而当该key对应的数据本身就是空的情况下,这就导致数据库中并发的去执行了很多不必要的查询操作,从而导致巨大冲击和压力。
可以通过下面的几种常用方式来避免缓存传统问题:
1缓存空对象
对查询结果为空的对象也进行缓存,如果是集合,可以缓存一个空的集合(非null),如果是缓存单个对象,可以通过字段标识来区分。这样避免请求穿透到后端数据库。同时,也需要保证缓存数据的时效性。这种方式实现起来成本较低,比较适合命中不高,但可能被频繁更新的数据。
2单独过滤处理
对所有可能对应数据为空的key进行统一的存放,并在请求前做拦截,这样避免请求穿透到后端数据库。这种方式实现起来相对复杂,比较适合命中不高,但是更新不频繁的数据。
在这里插入图片描述在这里插入图片描述

4:缓存雪崩现象
缓存雪崩就是指由于缓存的原因,导致大量请求到达后端数据库,从而导致数据库崩溃,整个系统崩溃,发生灾难。导致这种现象的原因有很多种,上面提到的“缓存并发”,“缓存穿透”,“缓存颠簸”等问题,其实都可能会导致缓存雪崩现象发生。这些问题也可能会被恶意攻击者所利用。还有一种情况,例如某个时间点内,系统预加载的缓存周期性集中失效了,也可能会导致雪崩。为了避免这种周期性失效,可以通过设置不同的过期时间,来错开缓存过期,从而避免缓存集中失效。
从应用架构角度,我们可以通过限流、降级、熔断等手段来降低影响,也可以通过多级缓存来避免这种灾难。
此外,从整个研发体系流程的角度,应该加强压力测试,尽量模拟真实场景,尽早的暴露问题从而防范。
在这里插入图片描述
消息队列,异步解耦,减少并发。

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述

最终一致性
最终一致性指的是两个系统的状态保持一致,要么都成功,要么都失败。当然有个时间限制,理论上越快越好,但实际上在各种异常的情况下,可能会有一定延迟达到最终一致状态,但最后两个系统的状态是一样的。
业界有一些为“最终一致性”而生的消息队列,如Notify(阿里)、QMQ(去哪儿)等,其设计初衷,就是为了交易系统中的高可靠通知。
以一个银行的转账过程来理解最终一致性,转账的需求很简单,如果A系统扣钱成功,则B系统加钱一定成功。反之则一起回滚,像什么都没发生一样。
然而,这个过程中存在很多可能的意外:
(1) A扣钱成功,调用B加钱接口失败。
(2) A扣钱成功,调用B加钱接口虽然成功,但获取最终结果时网络异常引起超时。
(3) A扣钱成功,B加钱失败,A想回滚扣的钱,但A机器down机。
在这里插入图片描述

应用场景:
1异步处理,
2应用解耦
3流量削锋
4日志处理
5消息通讯
在这里插入图片描述

Kafka是Apache下的一个子项目,是一个高性能跨语言分布式发布/订阅消息队列系统

在这里插入图片描述在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

高并发解决方案

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

微服务它的主要作用是将功能分解到离散的各个服务当中,从而降低系统的耦合性,并提供更加灵活的服务支持。
概念:把一个大型的单个应用程序和服务拆分为数个甚至数十个的支持微服务,它可扩展单个组件而不是整个的应用程序堆栈,从而满足服务等级协议。
定义:围绕业务领域组件来创建应用,这些应用可独立地进行开发、管理和迭代。在分散的组件中使用云架构和平台式部署、管理和服务功能,使产品交付变得更加简单。
架构的本质,是用一些功能比较明确、业务比较精练的服务去解决更大、更实际的问题。

w在这里插入图片描述
微服务和SOA
https://www.cnblogs.com/imyalost/p/6792724.html

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述

1.计数器法
有时我们还会使用计数器来进行限流,主要用来限制一定时间内的总并发数,比如数据库连接池、线程池、秒杀的并发数;计数器限流只要一定时间内的总请求数超过设定的阀值则进行限流,是一种简单粗暴的总数量限流,而不是平均速率限流。
在这里插入图片描述
这个方法有一个致命问题:临界问题——当遇到恶意请求,在0:59时,瞬间请求100次,并且在1:00请求100次,那么这个用户在1秒内请求了200次,用户可以在重置节点突发请求,而瞬间超过我们设置的速率限制,用户可能通过算法漏洞击垮我们的应用。

在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述

这个算法很简单。首先,我们有一个固定容量的桶,有水进来,也有水出去。对于流进来的水,我们无法预计共有多少水流进来,也无法预计流水速度,但
对于流出去的水来说,这个桶可以固定水流的速率,而且当桶满的时候,多余的水会溢出来。
在这里插入图片描述
从上图中可以看出,令牌算法有点复杂,桶里存放着令牌token。桶一开始是空的,token以固定的速率r往桶里面填充,直到达到桶的容量,多余的token会
被丢弃。每当一个请求过来时,就会尝试着移除一个token,如果没有token,请求无法通过。
服务降级和服务熔断

服务降级:
服务压力剧增的时候根据当前的业务情况及流量对一些服务和页面有策略的降级,以此环节服务器的压力,以保证核心任务的进行。
同时保证部分甚至大部分任务客户能得到正确的相应。也就是当前的请求处理不了了或者出错了,给一个默认的返回。
服务熔断:在股票市场,熔断这个词大家都不陌生,是指当股指波幅达到某个点后,交易所为控制风险采取的暂停交易措施。相应的,服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施,所以很多地方把熔断亦称为过载保护。
在这里插入图片描述
降级分类
降级按照是否自动化可分为:自动开关降级和人工开关降级。
降级按照功能可分为:读服务降级、写服务降级。
降级按照处于的系统层次可分为:多级降级。

自动降级分类
(1)、超时降级:主要配置好超时时间和超时重试次数和机制,并使用异步机制探测回复情况
(2)、失败次数降级:主要是一些不稳定的api,当失败调用次数达到一定阀值自动降级,同样要使用异步机制探测回复情况
(3)、故障降级:比如要调用的远程服务挂掉了(网络故障、DNS故障、http服务返回错误的状态码、rpc服务抛出异常),则可以直接降级。降级后的处理方案有:默认值(比如库存服务挂了,返回默认现货)、兜底数据(比如广告挂了,返回提前准备好的一些静态页面)、缓存(之前暂存的一些缓存数据)
(4)、限流降级
当我们去秒杀或者抢购一些限购商品时,此时可能会因为访问量太大而导致系统崩溃,此时开发者会使用限流来进行限制访问量,当达到限流阀值,后续请求会被降级;降级后的处理方案可以是:排队页面(将用户导流到排队页面等一会重试)、无货(直接告知用户没货了)、错误页(如活动太火爆了,稍后重试)。
在这里插入图片描述
服务熔断和服务降级比较:
两者其实从有些角度看是有一定的类似性的:

  1. 目的很一致,都是从可用性可靠性着想,为防止系统的整体缓慢甚至崩溃,采用的技术手段;
  2. 最终表现类似,对于两者来说,最终让用户体验到的是某些功能暂时不可达或不可用;
  3. 粒度一般都是服务级别,当然,业界也有不少更细粒度的做法,比如做到数据持久层(允许查询,不允许增删改);
  4. 自治性要求很高,熔断模式一般都是服务基于策略的自动触发,降级虽说可人工干预,但在微服务架构下,完全靠人显然不可能,开关预置、配置中心都是必要手段;
    而两者的区别也是明显的:
  5. 触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;
  6. 管理目标的层次不太一样,熔断其实是一个框架级的处理,每个微服务都需要(无层级之分),而降级一般需要对业务有层级之分(比如降级一般是从最外围服务开始)
  7. 实现方式不太一样
    服务降级要考虑的问题:
    1.核心和非核心服务
    2.是否支持降级,降级策略
    3.业务放通的场景,策略

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

Hystrix,该库旨在通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备拥有回退机制和断路器功能的线程和信号隔离,请求缓存和请求打包(request collapsing,即自动批处理,译者注),以及监控和配置等功能。

https://blog.csdn.net/XYWNRF/article/details/80984518

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值