java 并发问题分析解决

Wesley13
• 阅读 568

    项目中我们经常能见到一些并发问题,现对一些常见并发问题进行总结,知识结构不会很全,但比较实用。

  • 基本概念
  1. 什么是并发问题

我们以记录网站的访问量为例,先看一下并发问题是如何产生的。

private Integer count=1;

private AtomicInteger atomicCount = new AtomicInteger(1);

/**
 *
非线程安全方式
 * @param
request * @return */
@RequestMapping("/getCount")
public BaseResponse visitCount(HttpServletRequest request){ return new BaseResponse<>(true, count++);
}

我们通过jmeter模拟并发1000个请求,会发现我们得到的的值并不一定是1000。这里因为count++ 并不是线程安全的,所以在并发情况下会存在线程安全问题。

  1. 几组重要概念

同步VS异步

同步和异步通常用来形容一次方法调用。同步方法调用开始后,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。

并发与并行

并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,使用多线程时,在真实系统环境下不能并行,只能通过切换时间片的方式交替进行,从而并发执行任务。真正的并行只能出现在拥有多个CPU的系统中。  

阻塞和非阻塞

阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。  

临界区

临界区用来表示公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

  • 常见并发问题解决方案
  1. Java代码层面
  • 使用synchronized

_/**

 *_ _使用synchronized

 * @param_ request * @return */ @RequestMapping("/getCountWithSyn") public BaseResponse visitCountWithSynchro(HttpServletRequest request){ synchronized (this){ count++; } return new BaseResponse<>(true, count); }

  • 使用Lock
  • /**  * 使用synchronized  * @param request * @return */ @RequestMapping("/getCountWithSyn") public BaseResponse visitCountWithSynchro(HttpServletRequest request){ synchronized (this){ count++; } return new BaseResponse<>(true, count); }
  • 使用原子操作类
  • /**  * 使用原子类  * @param request * @return */ @RequestMapping("/getCountWithAct") public BaseResponse visitCountWithAct(HttpServletRequest request){ return new BaseResponse<>(true, atomicCount.getAndIncrement()); }
  1. 数据库层面

实际项目开发中,经常会遇到并发下单类似的场景,我们以商品购买下单为例,讲解一下如何通过数据库悲观锁乐观锁控制并发,本案例有一定的参考性,但其中代码比较简单,省去了很多逻辑校验,遇到类似场景请勿照搬。

所涉及表:

  1. 商品库存表:

create table T_STOCK

(

  id           NUMBER not null,

product_name VARCHAR2(255),

price NUMBER,

  num          NUMBER,

create_time TIMESTAMP(6),

  version      NUMBER

);

-- Add comments to the columns

comment on column T_STOCK.id

  is 'id';

comment on column T_STOCK.product_name

  is '商品名称';

comment on column T_STOCK.price

  is '商品价格';

comment on column T_STOCK.num

  is '商品数量';

comment on column T_STOCK.create_time

  is '创建时间';

comment on column T_STOCK.version

  is '版本号';

  1. 订单表:

-- Create table

create table T_ORDER

(

  id           NUMBER not null,

stock_id NUMBER,

product_name VARCHAR2(255),

  num          NUMBER,

user_name VARCHAR2(255)

);

-- Add comments to the columns

comment on column T_ORDER.id

  is 'id';

comment on column T_ORDER.stock_id

  is '库存id';

  • 最Low实现(非线程安全)

日常实现中很容易出现超发或者库存数量扣为负数线下,先看下我们初学者很容易实现的方式。

Service层代码:

@Override

@Transactional public String dealOrder(String userName, Long num, Long stockId) { //查询stock Stock stock = stockDao.getById(stockId);

    if(stock.getNum()>=num){

        Order order=new Order(); order.setProductName(stock.getProductName()); order.setUserName(userName); order.setStockId(stockId); order.setNum(num); //保存订单信息 orderDao.insert(order); stock.setNum(stock.getNum() - num); stock.setVersion(stock.getVersion()+1); //更新库存信息 stockDao.update(stock); } return "success"; }

  • 悲观锁
  • @Override @Transactional public void dealOrderWithPessLock(String userName, Long num, Long stockId) {     Stock stock = stockDao.getByIdForUpdate(stockId);     if(stock.getNum()>=num){           Order order=new Order(); order.setProductName(stock.getProductName()); order.setUserName(userName); order.setStockId(stockId); order.setNum(num); orderDao.insert(order); stock.setNum(stock.getNum() - num); stock.setVersion(stock.getVersion()+1); stockDao.update(stock); } }

Dao层

@Select("select * from t_stock where id=#{id} for update") @Results({ @Result(property = "id",column = "id"), @Result(property = "productName",column = "product_name"), @Result(property = "price",column = "price"), @Result(property = "num",column = "num"), @Result(property = "createTime",column = "create_time"), @Result(property = "version",column = "version")

})

Stock getByIdForUpdate(Long stockId);

适用场景:

比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

  • 乐观锁

Service层:

@Override

@Transactional public void dealOrderWithOptLock(String userName, Long num, Long stockId) {

    Stock stock = stockDao.getById(stockId);

    if(stock.getNum()>=num){

        Order order=new Order(); order.setProductName(stock.getProductName()); order.setUserName(userName); order.setStockId(stockId); order.setNum(num); stock.setNum(stock.getNum() - num);

        int i= stockDao.updateWithVersion(stock);

        if(i==1){ orderDao.insert(order); } logger.info("更新库存成功条数:"+i); }else{ logger.error("库存不足"); }

}

Dao层:

@Update("update t_stock  set  num = #{num}, version =version+1 where id = #{id} and version=#{version}") int updateWithVersion(Stock stock);

适用场景:

比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

  1. 分布式锁层面

我们这里适用redis 做分布式锁控制,这里仅仅做分布式锁,并不做缓存使用。

Service层:

@Override

@Transactional public void dealOrderWithDisLock(String userName, Long num, Long stockId) {

    RedissLockUtil.lock("lock", TimeUnit.SECONDS, 3); //查询stock @Override

@Transactional public void dealOrderWithDisLock(String userName, Long num, Long stockId) {

    RedissLockUtil.lock("lock", TimeUnit.SECONDS, 3); //查询stock Stock stock = stockDao.getById(stockId);

    if(stock.getNum()>=num){

        Order order=new Order(); order.setProductName(stock.getProductName()); order.setUserName(userName); order.setStockId(stockId); order.setNum(num); //保存订单信息 orderDao.insert(order); stock.setNum(stock.getNum() - num); stock.setVersion(stock.getVersion()+1); //更新库存信息 stockDao.update(stock); logger.info("下单成功:"+userName+"--num:"+num); }else{ logger.error("库存不足,下单失败"); }

    RedissLockUtil.unlock("lock"); }

  1. 性能比较

数据库悲观锁:

java 并发问题分析解决

数据库乐观锁:

java 并发问题分析解决

Redis分布式锁

java 并发问题分析解决

  1. 总结:

         在我们日常项目开发中,涉及并发控制的地方一般建议使用数据库乐观锁就可以解决,对于有些场景需要控制插入记录数量时,可以通过分布式锁去解决。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
3年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这