java 并发问题分析解决

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

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

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

private Integer count=1;

private
AtomicInteger atomicCount = new AtomicInteger(1);

/**
 *
非线程安全方式
 * @param
request
 
* @return
 
*/

@RequestMapping("/getCount")
public BaseResponse<Integer> 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<Integer> visitCountWithSynchro(HttpServletRequest request){

    synchronized (this){

        count++;

    }

    return new BaseResponse<>(true, count);

}

 

  • 使用Lock
  • /**  * 使用synchronized  * @param request  * @return  */ @RequestMapping("/getCountWithSyn") public BaseResponse<Integer> visitCountWithSynchro(HttpServletRequest request){     synchronized (this){         count++;     }     return new BaseResponse<>(true, count); }
  • 使用原子操作类
  • /**  * 使用原子类  * @param request  * @return  */ @RequestMapping("/getCountWithAct") public BaseResponse<Integer> 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. 性能比较

 

数据库悲观锁:

数据库乐观锁:

 

Redis分布式锁

 

 

  1. 总结:

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章