多线程并发在电商系统下的追本溯源-电商实战

常见问题

线程协作

1) wait/notify
wait : Object中,让出锁,阻塞等待。
notify/notiryAll:Object中,唤醒wait的进程,具体唤醒哪一个,要看优先级。

public class NotifyTest {
    public static void main(String[] args) {
        byte[] LOCK = new byte[0];

        Thread t1 = new Thread(() -> {
            synchronized (LOCK) {
                try {
                    LOCK.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1");
            }

        });

        Thread t2 = new Thread(() -> {
            synchronized (LOCK) {
                try {
                    LOCK.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2");
            }

        });

        Thread t3 = new Thread(() -> {
            synchronized (LOCK) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t3");
                LOCK.notifyAll();
            }
        });
        t1.setPriority(1);
        t2.setPriority(3);
        t3.setPriority(2);
        t1.start();
        t2.start();
        t3.start();
    }
}

结果:
在这里插入图片描述
或者在这里插入图片描述
结论:wait不释放锁,notifyAll具体唤醒哪一个,参考优先级,但是不是绝对按照优先级来执行。

2)sleep
sleep:Thread中。使线程沉睡一段时间,在沉睡期间,不释放锁。

public class SleepTest{
  public static void main(String[] args) {
    final byte[] lock = new byte[0];
    new Thread(()->{
      synchronized (lock){
        System.out.println("start");
        try {
          Thread.currentThread().sleep(3000);
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
        System.out.println("end");
     }
   }).start();
    new Thread(()->{
      synchronized (lock){
        System.out.println("need lock");
     }
   }).start();
 }
}

结果:
在这里插入图片描述
等待1S后
在这里插入图片描述

3)yield
yield:Thread中。不释放锁,运行中转为就绪,让出cpu给大家去竞争。当然有可能自己又抢了回来。

public class YieldTest{
  public static void main(String[] args) throws InterruptedException {
    final byte[] lock = new byte[0];
   
    //让出执行权,但是锁不释放
    Thread t1 = new Thread(()->{
      synchronized (lock){
        System.out.println("start");
        Thread.yield();
        System.out.println("end");
     }
   });
    //可以抢t1,但是拿不到锁,白费
    Thread t2 = new Thread(()->{
      synchronized (lock){
        System.out.println("need lock");
     }
   });
    //不需要锁,可以抢t1的执行权,但是能不能抢得到,不一定
    //所以多执行几次,会看到不同的结果……
    Thread t3 = new Thread(()->{
   System.out.println("t3 started");
   });
    t1.start();
    t2.start();
    t3.start();
 }
}

在这里插入图片描述
或者

在这里插入图片描述
分析:
t3会插队抢到执行权,但是t2不会,因为t2和t1共用一把锁而yield不会释放
t3不见得每次都能抢到。可能t1让出又抢了回去

4)join

join:父线程等待子线程执行完成后再执行,将异步转为同步。注意调的是子线程,阻断的是父线
程。

public class JoinTest implements Runnable{
  @Override
  public void run() {
    try {
      Thread.sleep(1000);
   } catch (InterruptedException e) {
      e.printStackTrace();
   }
    System.out.println("I am sub");
 }
  public static void main(String[] args) throws InterruptedException {
    Thread sub = new Thread(new JoinTest());
    sub.start();
   sub.join();
    System.out.println("I am main");
 }
}

结果:
在这里插入图片描述

死锁

public class DeadLock {
  byte[] lock1 = new byte[0];
  byte[] lock2 = new byte[0];
  void f1() throws InterruptedException {
    synchronized (lock1){
      Thread.sleep(1000);
      synchronized (lock2){
        System.out.println("f1");
     }
   }
 }
  void f2() throws InterruptedException {
    synchronized (lock2){
      Thread.sleep(1000);
      synchronized (lock1){
        System.out.println("f2");
     }
   }
 }
  public static void main(String[] args) {
    DeadLock deadLock = new DeadLock();
    new Thread(()->{
      try {
        deadLock.f1();
     } catch (InterruptedException e) {
        e.printStackTrace();
     }
   }).start();
    new Thread(()->{
      try {
        deadLock.f2();
     } catch (InterruptedException e) {
        e.printStackTrace();
     }
   }).start();
 }
}

死锁产生的必要条件:

  • 互斥使用,即资源只能独享,一个占了其他都必须等。
  • 不可抢占,资源一旦被占,就只能等待占有者主动释放,其他线程抢不走。
  • 贪婪占有,占着一把锁不释放,同时又需要申请另一把。
  • 循环等待,即存在等待环路,A → B → C → A。

总结来说:两个线程已经持有了锁,还想拿对方手里的锁,就会导致死锁的产生。

那我们怎么来排查死锁呢?可以用JDK自带的工具:

  • jps + jstack pid
    通过jps找到线程号,再执行jstack pid,找到 Found one Java-level deadlock:xxx
  • jconsole
    执行jconsole,打开窗口,找到 线程 → 检测死锁
  • jvisualvm
    执行jvisualvm,打开窗口,双击线程pid,打开线程,会提示死锁,dump查看线程信息

电商实际应用

超时订单

在电商应用中,经常能够遇到订单超时的场景。当订单超时后,我们需要将订单状态修改,恢复库存等等这些操作。实际应用中,超时订单一般有以下几种设计方案。

设计方案

1)定死扫表:定时任务轮询扫描订单表,比对时间,超时的订单挑出来。
优点:实现简单
缺点:数据量大的时候,不适用。

2)延迟消费:定义一个队列用于延迟操作,在用户下单的时候,将订单放到这个队列中。

  • DelayQueue:实现简单,不需要借助外部中间件。可借助db事务,down机丢失,同时注意内存占用
  • 消息队列,设置延迟并监听死信队列,注意消息堆积产生的监控报警
  • redis过期回调,redis对内存空间的占用

这里的案例我们采用延迟队列来实现。

订单超时类

public class OrderDto implements Delayed {
    private int id; // 订单ID
    private long invalid; // 超时时间

    public OrderDto(int id, long deta){
        this.id = id;
        this.invalid = deta * 1000 + System.currentTimeMillis();
    }

    //倒计时,降到0时队列会吐出该任务
    @Override
    public long getDelay(TimeUnit unit) {
        return invalid - System.currentTimeMillis();
    }

    @Override
    public int compareTo(Delayed o) {
        OrderDto o1 = (OrderDto) o;
        return this.invalid - o1.invalid <= 0 ? -1 : 1;
    }

    public int getId() {
        return id;
    }

    public long getInvalid() {
        return invalid;
    }
}

定义监控类,启动守护线程,如果有超时任务,提交进线程池

@Component
public class OrderMonitor {
    @Autowired
    OrdersMapper mapper ;
    //延时队列
    final DelayQueue<OrderDto> queue = new DelayQueue<OrderDto>();
    //任务池
    ExecutorService service = Executors.newFixedThreadPool(3);

    //投放延迟订单
    public void put(OrderDto dto){
        this.queue.put(dto);
        System.out.println("put task:"+dto.getId());
    }

    //在构造函数中启动守护线程
    public OrderMonitor(){
        this.execute();
        System.out.println("order monitor started");
    }

    //守护线程
    public void execute(){
        new Thread(()->{
            while (true){
                try {
                    OrderDto dto = queue.take();
                    System.out.println("take task:"+dto.getId());
                    service.execute(new Task(dto));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //任务类
    class Task implements Runnable{
        OrderDto dto;
        Task(OrderDto dto){
            this.dto = dto;
        }
        @Override
        public void run() {
            Orders orders = new Orders();
            orders.setId(dto.getId());
            orders.setUpdatetime(new Date());
            orders.setStatus(-1);
            System.out.println("cancel order:"+orders.getId());
            mapper.updateByPrimaryKeySelective(orders);
        }
    }


}

下单:

@RestController
@RequestMapping("/order")
@Api(value = "多线程取消超时订单demo")
public class OrderController {
    @Autowired
    OrdersMapper mapper;
    @Autowired
    OrderMonitor monitor;

    @PostMapping("/add")
    @Transactional
    @ApiOperation("下订单")
    public int add(@RequestParam String name){
        Orders order = new Orders();
        order.setName(name);
        order.setCreatetime(new Date());
        order.setUpdatetime(new Date());
        //超时时间,取10-20之间的随机数(秒)
        order.setInvalid(new Random().nextInt(10)+10);
        order.setStatus(0);
        mapper.insert(order);
        //事务性验证
//        int i = 1/0;
        monitor.put(new OrderDto(order));
        return order.getId();
    }

}

加减库存

设计方案

1)rabbitmq异步排队:使用rabbitmq先排队,请求到来时之间入队,界面显示排队中,消费端逐个消费,同时扣减库存,界面轮询查询结果。可能会出现排队半天抢完的情况。

2)库存预热:使用缓存或内存变量,活动开始前从db中提取库存值初始化,请求到来时直接扣减,及时提醒。可能出现一种感觉,活动刚开始就抢没了……

实现

初始化库存:

 //热加载数据
    @GetMapping("/load")
    @ApiOperation(value = "热加载库存")
    //bean初始化后,立刻热加载
    @PostConstruct
    public Map load(){
        products.clear();
        List<Product> list = productMapper.selectByExample(null);
        list.forEach(p -> {
            products.put(p.getId(),new AtomicInteger(p.getNum()));
        });
        return products;
    }

开始抢购:

//抢购
    @GetMapping("/go")
    @ApiOperation(value = "抢购")
    public void go(int productId){

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                int count = 0;
                long userId = Thread.currentThread().getId();
                while (products.get(productId).getAndDecrement() > 0){
                    count++;
                    //扔消息队列,异步处理
                    template.convertAndSend("promotion.order",productId+","+userId);
                }
                System.out.println(Thread.currentThread().getName()+"抢到:"+count);
            }).start();
        }
    }

价格排序

设计方案

1)直接数据库sort,这种最典型
2)redis缓存zset获取,在商品列表缓存,web网站排序场景中常见
3)内存排序,有时候,需要复杂的运算和比较逻辑,sql sort操作表达不出来时,必须进入内存运算

我们这里使用方案3测试下。

实现

ForkJoinTask:

public class SortTask extends RecursiveTask<List<Product>> {
    private List<Product> list;
    public SortTask(List<Product> list){
        this.list = list;
    }

    @Override
    //分拆与合并
    protected List<Product> compute() {
        if (list.size() > 2){
            //如果拆分的长度大于2,继续拆
            int middle = list.size() / 2 ;
            //拆成两个
            List<Product> left = list.subList(0,middle);
            List<Product> right = list.subList(middle+1,list.size());
            //子任务fork出来
            SortTask task1 = new SortTask(left);
            task1.fork();
            SortTask task2 = new SortTask(right);
            task2.fork();
            //join并返回
            return mergeList(task1.join(),task2.join());
        }else if (list.size() == 2 && list.get(0).getPrice() > list.get(1).getPrice()){
            //如果长度达到2个了,但是顺序不对,交换一下
            //其他如果2个且有序,或者1个元素的情况,不需要管他
            Product p = list.get(0);
            list.set(0,list.get(1));
            list.set(1,p);
        }
        //交换后的返回,这个list已经是每个拆分任务里的有序值了
        return list;
    }

    //归并排序的合并操作,目的是将两个有序的子list合并成一个整体有序的集合
    //遍历两个子list,依次取值,两边比较,从小到大放入新list
    //注意,left和right是两个有序的list,已经从小到大排好序了
    private List<Product> mergeList(List<Product> left,List<Product> right){
        if (left == null || right == null) return null;

        //合并后的list
        List<Product> total = new ArrayList<>(left.size()+right.size());
        //list1的下标
        int index1 = 0;
        //list2的下标
        int index2 = 0;
        //逐个放入total,所以需要遍历两个size的次数之和
        for (int i = 0; i < left.size()+right.size(); i++) {
            //如果list1的下标达到最大,说明list1已经都全部放入total
            if (index1 == left.size()){
                //那就从list2挨个取值,不需要比较直接放入total
                total.add(i,right.get(index2++));
                continue;
            }else if (index2 == right.size()){
                //如果list2已经全部放入,那规律一样,取list1
                total.add(i,left.get(index1++));
                continue;
            }

            //到这里说明,1和2中还都有元素,那就需要比较,把小的放入total
            //list1当前商品的价格
            Float p1 = left.get(index1).getPrice();
            //list2当前商品的价格
            Float p2 = right.get(index2).getPrice();

            Product min = null;
            //取里面价格小的,取完后,将它的下标增加
            if (p1 <= p2){
                min = left.get(index1++);
            }else{
                min = right.get(index2++);
            }
            //放入total
            total.add(min);
        }
        //这样处理后,total就变为两个子list的所有元素,并且从小到大排好序
        System.out.println(total);
        System.out.println("------------------");
        return total;
    }
}

开始排序:

public class SortController {
    @Autowired
    ProductMapper mapper;

    @GetMapping("/list")
    List<Product> sort() throws ExecutionException, InterruptedException {
        //查商品列表
        List<Product> list = mapper.selectByExample(null);
        //线程池
        ForkJoinPool pool = new ForkJoinPool(2);
        //开始运算,拆分与合并
        Future<List<Product>> future = pool.submit(new SortTask(list));
        return future.get();
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值