vlambda博客
学习文章列表

Java高并发常见应对之策吐血整理

一、高并发涉及到的知识点

线程安全,线程封闭,线程调度,同步容器,并发容器,AQS,J.U.C。

二、同步与异步

        所谓同步,可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其它的命令。

        异步,执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。

         同步在一定程度上可以看做是单线程,这个线程请求一个方法后就待这个方法给他回复,否则他不往下执行。

        异步在一定程度上可以看做是多线程的,请求一个方法后,就不管了,继续执行其他的方法。

2.1举例

同步:吃饭和说话,只能一件事一件事的来,因为只有一张嘴。

异步:但吃饭和听音乐是异步的,因为,听音乐并不引响我们吃饭。

2.2脏数据

      脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这

个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据(Dirty Data),依据脏数据所做的操作可能是不正确的。

2.3不可重复读

       不可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

三、如何处理高并发问题

3.1硬件上

扩容:水平扩容、垂直扩容

3.2中间件上

缓存:Redis、Memcache、GuavaCache等

队列:Kafka、RabitMQ、RocketMQ等

3.3分布式拆分

应用拆分:服务化Dubbo与微服务Spring Cloud

限流:Guava RateLimiter使用、常用限流算法、自己实现分布式限流等

服务降级与服务熔断:服务降级的多重选择、Hystrix

3.4数据库层面

数据库切库,分库分表:切库、分表、多数据源

高可用的一些手段:任务调度分布式elastic-job、主备curator的实现、监控报警机制

3.5使用微服务

使用微服务拆分,划分模块、网关,熔断、限流、降级

四、并发的优势与风险

4.1并发的优势

(1)速度上可以同时处理多个请求,响应更快;复杂的操作可以分成多个进程同时进行。

(2)设计上程序设计在某些情况下更简单,也可以更多的选择。

(3)资源利用上CPU能够在等待IO的时候做一些其他的事情。

4.2并发的风险

(1)安全性:多个线程共享数据时可能会产生于期望不相符的结果。

(2)活跃性:某个操作无法继续进行下去时,就会发生活跃性问题。比如死锁、饥饿等问题。

(3)性能:线程过多时会使得CPU频繁切换,调度时间增多;同步机制;消耗过多内存。

4.3如何处理并发和同步

4.3.1Java代码层面

java中的同步锁,典型的就是同步关键字synchronized。还有一个Atomic包,里面是基于乐观锁版本号CAS自旋实现并发控制。

4.3.2数据库层面

悲观锁(Pessimistic Locking):       

       悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自 外部系统的事务处理)修改持保守态度,因此,

       在整个数据处理过程中,将数据处于锁定状态。

       悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能 真正保证数据访问的排他性,否则,即使在本系统

       中实现了加锁机制,也无法保证外部系 统不会修改数据)。 

       一个典型的倚赖数据库的悲观锁调用: 

       select * from account where name=”javagongfu” for update

       这条 sql 语句锁定了 account 表中所有符合检索条件( name=”javagongfu” )的记录。

       本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。 

乐观锁:

      大多是基于数据版本   Version )记录机制实现。

      何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。 

      读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

       操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。在操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并 从其帐户余额中扣除 $20 ( $100-$20 )。操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣 除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大 于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。

       操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数 据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的 数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记 录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

       这样,就避免了操作员 B 用基于version=1 的旧数据修改的结果覆盖操作 员 A 的操作结果的可能。从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系 统整体性能表现。需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局 限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户 余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在 系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如 将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途 径,而不是将数据库表直接对外公开)。

五、常见并发问题列举

案例一:订票系统案例,某航班只有一张机票,假定有1w个人打开你的网站来订票,问你如何解决并发问题。

问题描述:1w个人同时点击购买,到底谁能成交?总共只有一张票。

解决方案:

锁同步同步更多指的是应用程序的层面,多个线程进来,只能一个一个的访问,java中指的是syncrinized关键字。锁也有2个层面,一个是java中谈到的对象锁,用于线程同步;另外一个层面是数据库的锁;如果是分布式的系统,显然只能利用数据库端的锁来实现。

假定我们采用了同步机制或者数据库物理锁机制,如何保证1w个人还能同时看到有票,显然会牺牲性能,在高并发网站中是不可取的。

采用乐观锁即可解决此问题。乐观锁意思是不锁定表的情况下,利用业务的控制来解决并发问题,这样即保证数据的并发可读性又保证保存数据的排他性,保证性能的同时解决了并发带来的脏数据问题。

在现有表当中增加一个冗余字段,version版本号, long类型

原理:

1)只有当前版本号》=数据库表版本号,才能提交

2)提交成功后,版本号version ++

5.1解决此问题还需考虑的因素

(1)可以考虑增加缓存,分布式缓存,实现读写分离,采用Redis作为缓存端,基础数据、常用业务数据直接从缓存取。

(2)增加网络带宽,DNS域名解析分发多台服务器。

(3)负载均衡,配置前置代理服务器nginx、apache。

(4)数据库查询优化,读写分离,分表等等.

(5)优化数据库结构,多做索引,提高查询效率。

六、线程安全

基于数组结构:ArrayList -> 线程安全:Vector, Stack

Vector中的方法使用synchronized修饰过,线程安全

Stack继承Vector

非线程安全:HashMap -> 线程安全:HashTable(key、value不能为null)

HashTable使用synchronized修饰方法

Collections.synchronizedXXX(List、Set、Map)

ConcurrentHashMap、ConcurrentLinkedList都是线程安全的。

6.1并发容器

ArrayList -> CopyOnWriteArrayList:相比ArrayList,CopyOnWriteArrayList是线程安全的,写操作时复制,即当有新元素添加到CopyOnWriteArrayList时,先从原有的数组里拷贝一份出来,然后在新的数组上写操作,写完之后再将原来的数组指向新的数组,CopyOnWriteArrayList整个操作都是在锁(ReentrantLock锁)的保护下进行的,这么做主要是避免在多线程并发做add操作时复制出多个副本出来,把数据搞乱了。第一个缺点是做写操作时,需要拷贝数组,就会消耗内存,如果元素内容比较多会导致youngGC或者是fullGc;第二个缺点是不能用于实时读的场景,比如拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到的数据可能还是旧的,虽然CopyOnWriteArrayList能够做到最终的一致性,但是没法满足实时性要求,因此CopyOnWriteArrayList更适合读多写少的场景。

CopyOnWriteArrayList设计思想:1读写分离 2最终一致性 3使用时另外开辟空间解决并发冲突。

HashSet -> CopyOnWriteArraySet

TreeSet -> ConcurrentSkipListSet

CopyOnWriteArraySet:底层实现是CopyOnWriteArrayList。

ConcurrentSkipListSet:和TreeSet 一样支持自然排序,基于map集合,但是批量操作不是线程安全的。

HashMap -> ConcurrentHashMap :不允许空值,针对读操作做了大量的优化,具有特别高的并发性。

TreeMap  -> ConcurrentSkipListMap :内部使用SkipList跳表结构实现的,key是有序的,支持更高的并发。

6.2AQS同步组件

1 CountDownLatch:闭锁,通过计数来保证线程是否需要一直阻塞

2 Semaphore:控制同一时间并发线程的数目

3 CyclicBarrier:和CountDownLatch相似,都能阻阻塞线程

4 ReentrantLock

5 Condition

6 FutureTask

七、使用线程池

new Thread弊端:

1 每次new Thread新建对象,性能差

2 线程缺乏统一的管理,肯无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或者OOM

3 缺少更多功能,如更多执行、定期执行、线程中断

线程池的好处:

1 重用存在的线程,减少对象创建、消亡的开销,性能佳

2 可有效控制最大并发的线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞

3 提供定时执行、定期执行、单线程、并发数控制等功能

7.1ThreadPoolExecutor

ThreadPoolExecutor参数:

1 corePoolSize:核心线程数

2 maximumPoolSize:最大线程数

3 workQueue:阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响

如果当前系统运行的线程数量小于corePoolSize,直接新建线程执行处理任务,即使线程池中的其他线程是空闲的。如果当前系统运行的线程数量大于或等于corePoolSize,且小于maximumPoolSize,只有当workQueue满的时候才创建新的线程去处理任务,如果设置corePoolSize和maximumPoolSize相同的话,那么创建的线程池大小是固定的,这时如果有新任务提交,当workQueue没满时,把请求放进workQueue中,等待有空闲的线程从workQueue中取出任务去处理。如果运行的线程数量大于maximumPoolSize,这时如果workQueue满,根据拒绝策略去处理。

4 keepAliveTime:线程没有任务执行时最多保持多久的时间终止

5 unit:keepAliveTime的时间单位

6 threadFactory:线程工厂,用来创建线程

7 rejectHandler:当拒绝处理任务时的策略

线程池方法:


Java高并发常见应对之策吐血整理

八、分布式缓存

8.1Redis

Java高并发常见应对之策吐血整理


8.2memcache

Java高并发常见应对之策吐血整理


8.3缓存一致性

Java高并发常见应对之策吐血整理


8.4缓存并发


九、应用拆分


采用MyCat分库分表,读写分离。

十、使用微服务

使用Spring Cloud Alibaba,网关组件,Sentinel隔离、熔断、降级、限流。

具体微服务本博客有很多文章介绍。