并发中线程锁,线程池的应用

本文深入探讨了并发编程中线程锁和乐观锁的应用,通过一个具体的测试案例,揭示了在高并发场景下如何有效避免数据冲突,介绍了悲观锁和乐观锁的原理,以及在Java中如何利用线程池优化资源利用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天想测试一下疼讯云的垃圾服务器,用python脚本测试接口,共计5000次,本来只是想看看服务器的处理速度,但是在脚本中的测试数据是全部写死了的,这5000次请求理论上应该只有1次是成功的,但是结果却是有13次成功,因为我在数据库中的表是把每个数据都设置成了主键,所以最终只增加了1条数据。但是为什么响应成功的次数不是1次呢?嗯,肯定是多个线程同时操作导致的,经查阅,加锁可以解决此类问题,于是决定研究一下线程锁这个东西。

部分代码,只展示实现类

请求体:

/**
 * 控制器中添加用户请求数据.
 * </p>
 *
 * @author xuLiang
 * @since 0.0.1
 */
public class VoAddUserRequest {
	private Long id;
	private String password;
	private String name;
	private String email;

getters and setters..
}

 DAO层:

/**
	 * 
	 * 添加一条用户信息记录.
	 *
	 * @param Users
	 *            待添加的用户信息
	 * @return 添加完成的用户信息对象
	 * @see param.position.dao.UsersMapper#addUser(param.position.pojo.po.mysql.tables.pojos.Users).
	 * @author xuLiang
	 * @since 0.0.1
	 */
	@Override
	public Users postUser(Users users) {
		UsersRecord userRecord = new UsersRecord();
		userRecord = dsl.newRecord(USERS);
		userRecord.from(users);
		int addResult = userRecord.store();
		if (addResult == 1) {
			return userRecord.into(Users.class);
		}
		return null;
	}

 Controller接口

/**
	 * 
	 * 添加一个用户信息记录.
	 *
	 * @param voAddUserRequest
	 *            需要添加的用户信息记录
	 * @return 封装用户信息的统一响应对象
	 * 
	 * @author xuLiang
	 * @since 0.0.1
	 */
	@PostMapping("/users")
	@ResponseStatus(HttpStatus.CREATED)
	@Override
	@CustomAnnotation
	public @ResponseBody GetUserPersonResp postUser(@RequestBody VoAddUserRequest voAddUserRequest) {
		GetUserPersonResp result = new GetUserPersonResp();
		result.setCode(1);
		result.setMessage("添加成功!");
		result.setVoGetUserResponse(VoUserPersonMapper.INSTANCE.fromBoToVoGetUserResponseMap(
				userPersonService.postUser(VoUserPersonMapper.INSTANCE.fromVoToBoAddUserRequestMap(voAddUserRequest))));

		return result;
	}

               先说一说为什么会产生这种现象,原因很简单,当用户每发送一次请求,服务器便会新开一个线程来处理这个用户的请求,当服务器检测到用户发送的请求体与数据库中的主键不冲突的时候,便会将用户发送的数据写入数据库,但是当多个用户同时发送相同的数据的时候(脚本运行速度极快,可以近似认为是同一时间发出请求),此时多个线程同时对数据库进行检测(此时还没有线程对数据库进行操作),主键并未冲突,所以都返回了成功的结果,但事实上只要有一个线程完成了数据库的操作之后,其他线程的操作统统都是失败的。为了解决这个问题,就要引入锁(synchronized)的机制。

public synchronized Users postUser(Users users)

锁的作用,简单点说,就是将数据库的表锁住,当一个线程完成了整个操作流程并且销毁之后,才会允许下一个线程进行操作,这样在理论上可以保证只有第一个请求可以给用户返回成功的信息,但是后果就是其他用户会等待很久才会收到返回结果,因为这些线程都要排队一个一个来操作,这就会导致大量线程处于等待状态,也就是线程阻塞,就是我们平时所说的网站崩了。

这时候就需要使用到线程池,作用就是有请求过来了就到线程池里面取出一条线程去处理它,处理完成就把它收回到线程池里面,Java通过Executors接口提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程;
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待;
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行;
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行;

线程以及线程池的具体使用方法可直接百度,此处不再赘述。

 但是在Java中,synchronized的思想是悲观锁。悲观锁总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁。

这对于处理大量请求来说显然不是最佳方法,这时候就引入了乐观锁:

总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。

 version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

核心SQL代码:

update table set x=x+1, version=version+1 where id=#{id} and version=#{version};  

CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

当然还有一种最直接的方法,就是将数据库换为Redis。与MySQL和Oracle等数据库不同,由于Redis进行操作的数据都是缓存在内存中,处理速度远远高于SSD硬盘,机械硬盘就更不用说了,但是一旦开发人员操作不当就会导致数据丢失,而且无法复原,所以建议使用单独的数据库服务器,并且对程序员的水平要有一定要求。

使用乐观锁+Redis基本上就能应付绝大多数情况了,当然遇到双十一这种情况还是有可能崩的(反正阿里有钱,大不了再修个机房就完了)。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值