深入解析高并发技术:线程安全、同步与异步、并发容器及J.U.C详解

Java处理高并发的常用策略: 1、高并发涉及的知识点

深入解析高并发技术:线程安全、同步与异步、并发容器及J.U.C详解

线程安全、线程限制、线程调度、同步容器、并发容器、AQS、JUC。

2. 同步与异步

所谓同步可以理解为执行完函数或方法后等待系统返回值或消息。此时程序被阻塞,只有收到返回值或消息后才会执行其他命令。

异步,执行完函数或方法后,不必以阻塞的方式等待返回值或消息。您只需将异步流程委托给系统即可。然后当系统收到返回值或消息时,系统会自动触发委托的异步流程来完成该流程。一个完整的过程。

同步在一定程度上可以看作是单线程。这个线程请求一个方法后,会等待该方法回复它,否则不会继续执行。

异步在某种程度上可以看成是多线程。请求一个方法后,会被忽略,继续执行其他方法。

2.1 示例

同步性:吃饭和说话一次只能发生一个,因为只有一张嘴。

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

2.2 脏数据

脏读是指当一个事务正在访问数据并且已经修改了该数据,但修改尚未提交到数据库时,另一个事务也访问了该数据,然后使用了该数据

数据。因为这个数据还没有提交,其他事务读取到的数据就是脏数据,基于脏数据的操作可能会不正确。

2.3 不可重复读取

不可重复读是指在一个事务内多次读取相同的数据。在该事务结束之前,另一个事务也访问相同的数据。那么,在第一个事务的两次数据读取之间,由于第二个事务的修改,导致第一个事务两次读取的数据可能不同。这样,一个事务内两次读取的数据是不同的,因此称为不可重复读。

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. 并发的优点和风险 4.1 并发的优点

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

(2)在设计方面,编程在某些情况下更简单,并允许更多的选择。

(3)在资源利用率方面,CPU在等待IO的同时可以做其他事情。

4.2 并发风险

(1)安全性:当多个线程共享数据时,可能会出现与预期不符的结果。

(2) Liveness:当操作无法继续时就会出现Liveness问题。比如死锁、饥饿等问题。

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

4.3 如何处理并发和同步 4.3.1 Java代码级别

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

4.3.2 数据库级别

悲观锁定:

悲观锁,顾名思义,是指对外界修改的数据(包括系统当前的其他事务和来自外部系统的事务处理)采取保守的态度。所以,

在整个数据处理过程中保持数据锁定。

悲观锁的实现往往依赖于数据库提供的加锁机制(只有数据库层提供的加锁机制才能真正保证数据访问的排他性。否则,即使在这个系统中,

锁定机制是在系统内部实现的,不能保证外部系统不会修改数据)。

典型的依赖于数据库的悲观锁调用:

select * from account where name="javagongfu" 进行更新

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

在这个事务提交之前(事务期间的锁会在事务提交时释放),外界无法修改这些记录。

乐观锁:

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

什么是数据版本?即给数据添加版本标识。在基于数据库表的版本解决方案中,这通常是通过向数据库表添加“版本”字段来实现的。

当数据被读出的时候,这个版本号也被读出,以后更新的时候,这个版本号就加一。此时,将提交数据的版本数据与数据库表中对应记录的当前版本信息进行比较。如果提交数据的版本号大于数据库表当前版本号,则进行更新,否则视为过期数据。对于上面修改用户账户信息的例子,假设数据库中的账户信息表中有一个版本字段,当前值为1;当前帐户余额字段(余额)为 100 美元。

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

操作员 B 完成操作,同时将版本号加一(version=2)并尝试向数据库提交数据(balance=$80)。但对比数据库记录版本时发现,操作者B提交的数据版本号为2,数据库记录当前版本也是2,不符合“提交版本必须”的乐观锁策略大于记录的当前版本才能执行更新”。因此,拒绝运营商B的提交。

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

5. 常见并发问题列表

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

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

解决方案:

锁同步更多指的是应用层面。多个线程进来,只能一一访问。在Java中,它指的是syncrinized关键字。锁也有两个层次,一是Java中提到的对象锁,用于线程同步;二是Java中提到的对象锁,用于线程同步。另一级是数据库锁;如果是分布式系统,显然只能通过数据库端的锁来实现。

假设我们使用同步机制或者数据库物理锁机制,如何保证一万人仍然可以同时看到票,显然会牺牲性能,这在高并发网站中是不可取的。

这个问题可以通过使用乐观锁来解决。乐观锁是指在不锁表的情况下,利用业务控制来解决并发问题。这确保了数据的并发可读性和所保存数据的排他性。保证性能的同时解决并发带来的脏数据问题。

现有表添加冗余字段,version版本号,long类型

原则:

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

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

5.1 解决该问题需要考虑的因素

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

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

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

(4)数据库查询优化、读写分离、表分区等。

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

6. 线程安全

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

Vector中的方法是用synchronized修饰的,并且是线程安全的。

Stack继承自Vector

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

HashTable使用synchronized修改方法

Collections.synchronizedXXX(列表、集合、映射)

ConcurrentHashMap 和 ConcurrentLinkedList 都是线程安全的。

6.1 并发容器

ArrayList -> CopyOnWriteArrayList:与ArrayList相比,CopyOnWriteArrayList是线程安全的,并且在写操作期间进行复制。也就是说,当向CopyOnWriteArrayList添加新元素时,首先从原数组中复制该元素,然后对新数组进行Write操作。写入后,将原数组指向新数组。 CopyOnWriteArrayList的整个操作都是在锁(ReentrantLock锁)的保护下进行的。这主要是为了避免多线程并发添加操作。制作了多个副本,这会弄乱数据。第一个缺点是,在进行写操作时,需要复制数组,这会消耗内存。如果元素含量较大,会引起youngGC或fullGc;第二个缺点是不能用于实时读取的场景,比如复制数组、添加新数组等。元素需要时间,因此调用 set 操作后,读取的数据可能仍然是旧的。 CopyOnWriteArrayList虽然可以达到最终一致性,但是无法满足实时性要求,所以CopyOnWriteArrayList更适合读多写少的场景。 。

CopyOnWriteArrayList设计思路: 1、读写分离 2、最终一致性 3、开辟额外的空间,解决使用过程中的并发冲突。

HashSet -> CopyOnWriteArraySet

TreeSet -> ConcurrentSkipListSet

CopyOnWriteArraySet:底层实现是CopyOnWriteArrayList。

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

HashMap -> ConcurrentHashMap:不允许空值,对读操作做了很多优化,并发性特别高。

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

6.2AQS同步组件

1 CountDownLatch:加锁,计数以确保线程是否需要一直阻塞

2 信号量:控制同时并发线程数

3 CyclicBarrier:与CountDownLatch类似,可以阻塞阻塞线程

4可重入锁

5 条件

6 未来任务

7.使用线程池

新线程的缺点:

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

2 线程缺乏统一管理,愿意创建无限的新线程相互竞争,可能会占用过多的系统资源,导致崩溃或OOM。

3 缺少更多执行次数、周期性执行、线程中断等更多功能

线程池的好处:

1 复用现有线程,降低对象创建和销毁的成本,获得良好的性能

2、可以有效控制最大并发线程数,提高系统资源利用率,避免资源过度竞争和阻塞。

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

7.1线程池执行器

ThreadPoolExecutor参数:

1 corePoolSize:核心线程数

2 MaximumPoolSize:最大线程数

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

如果系统当前运行的线程数小于corePoolSize,则会直接创建一个新线程来执行处理任务,即使线程池中的其他线程空闲。如果系统当前运行的线程数大于等于corePoolSize且小于maximumPoolSize,只有当workQueue已满时才会创建新的线程来处理任务。如果corePoolSize和maximumPoolSize设置相同,则创建的线程池的大小是固定的。如果有新任务提交,当workQueue未满时,会将请求放入workQueue中,等待有空闲线程从workQueue中取出任务进行处理。如果运行线程数大于maximumPoolSize且workQueue已满,则按照拒绝策略进行处理。

4 keepAliveTime:线程没有任务执行时被终止的最长时间。

5unit:keepAliveTime的时间单位

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

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

线程池方法:

8.分布式缓存8.1Redis

8.2内存缓存

8.3 缓存一致性

8.4 缓存并发

9. 应用拆分

使用MyCat实现库和表分离,读写分离。

10.使用微服务

使用Spring Cloud阿里巴巴、网关组件、Sentinel进行隔离、熔断、降级、限流。

这个博客有很多介绍具体微服务的文章。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

26阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

26阅读