zoukankan      html  css  js  c++  java
  • Java并发编程实战总结 (一)

    前提

    首先该场景是一个酒店开房的业务。为了朋友们阅读简单,我把业务都简化了。
    业务:开房后会添加一条账单,添加一条房间排期记录,房间排期主要是为了房间使用的时间不冲突。如:账单A,使用房间1,使用时间段为2020-06-01 12:00 - 2020-06-02 12:00 ,那么还需要使用房间1开房的时间段则不能与账单A的时间段冲突。

    业务类

    为了简单起见,我把几个实体类都简化了。

    账单类

    public class Bill {
        // 账单号
        private String serial;
    
        // 房间排期id
        private Integer room_schedule_id;
        // ...get set
    }
    

    房间类

    // 房间类
    public class Room {
        private Integer id;
    
        // 房间名
        private String name;
        // get set...
    }
    

    房间排期类

    import java.sql.Timestamp;
    
    public class RoomSchedule {
        private Integer id;
        
        // 房间id
        private Integer roomId;
    
        // 开始时间
        private Timestamp startTime;
    
        // 结束时间
        private Timestamp endTime;
        // ...get set
    }
    

    实战

    并发实战当然少不了Jmeter压测工具,传送门: https://jmeter.apache.org/download_jmeter.cgi
    为了避免有些小伙伴访问不到官网,我上传到了百度云:链接:https://pan.baidu.com/s/1c9l3Ri0KzkdIkef8qtKZeA
    提取码:kjh6

    初次实战(sychronized)

    第一次进行并发实战,我是首先想到sychronized关键字的。没办法,基础差。代码如下:

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.TransactionDefinition;
    import org.springframework.transaction.TransactionStatus;
    
    import java.sql.Timestamp;
    
    /**
     * 开房业务类
     */
    @Service
    public class OpenRoomService {
        @Autowired
        DataSourceTransactionManager dataSourceTransactionManager;
        @Autowired
        TransactionDefinition transactionDefinition;
    
        public void openRoom(Integer roomId, Timestamp startTime, Timestamp endTime) {
            // 开启事务
            TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
            try {
                synchronized (RoomSchedule.class) {
                    if (isConflict(roomId, startTime, endTime)) {
                        // throw exception
                    }
                    // 添加房间排期...
                    // 添加账单
    
                    // 提交事务
                    dataSourceTransactionManager.commit(transaction);
                }
            } catch (Exception e) {
                // 回滚事务
                dataSourceTransactionManager.rollback(transaction);
                throw e;
            }
        }
    
        public boolean isConflict(Integer roomId, Timestamp startTime, Timestamp endTime) {
            // 判断房间排期是否有冲突...
        }
    }
    
    1. sychronized(RoomSchedule.class),相当于的开房业务都是串行的。不管开房间1还是房间2。都需要等待上一个线程执行完开房业务,后续才能执行。这并不好哦。
    2. 事务必须在同步代码块sychronized中提交,这是必须的。否则当线程A使用房间1开房,同步代码块执行完,事务还未提交,线程B发现房间1的房间排期没有冲突,那么此时是有问题的。

    错误点: 有些朋友可能会想到都是串行执行了,为什么不把synchronized关键字写到方法上?
    首先openRoom方法是非静态方法,那么synchronized锁定的就是this对象。而Spring中的@Service注解类是多例的,所以并不能把synchronized关键字添加到方法上。

    二次改进(等待-通知机制)

    因为上面的例子当中,开房操作都是串行的。而实际情况使用房间1开房和房间2开房应该是可以并行才对。如果我们使用synchronized(Room实例)可以吗?答案是不行的。
    第三章 解决原子性问题当中,我讲到了使用锁必须是不可变对象,若把可变对象作为锁,当可变对象被修改时相当于换锁,这里的锁讲的就是synchronized锁定的对象,也就是Room实例。因为Room实例是可变对象(set方法修改实例的属性值,说明为可变对象),所以不能使用synchronized(Room实例)
    在这次改进当中,我使用了第五章 等待-通知机制,我添加了RoomAllocator房间资源分配器,当开房的时候需要在RoomAllocator当中获取锁资源,获取失败则线程进入wait()等待状态。当线程释放锁资源则notiryAll()唤醒所有等待中的线程。
    RoomAllocator房间资源分配器代码如下:

    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 房间资源分配器(单例类)
     */
    public class RoomAllocator {
        private final static RoomAllocator instance = new RoomAllocator();
    
        private final List<Integer> lock = new ArrayList<>();
    
        private RoomAllocator() {}
    
        /**
         * 获取锁资源
         */
        public synchronized void lock(Integer roomId) throws InterruptedException {
            // 是否有线程已占用该房间资源
            while (lock.contains(roomId)) {
                // 线程等待
                wait();
            }
    
            lock.add(roomId);
        }
    
        /**
         * 释放锁资源
         */
        public synchronized void unlock(Integer roomId) {
            lock.remove(roomId);
            // 唤醒所有线程
            notifyAll();
        }
    
        public static RoomAllocator getInstance() {
            return instance;
        }
    }
    

    开房业务只需要修改openRoom的方法,修改如下:

        public void openRoom(Integer roomId, Timestamp startTime, Timestamp endTime) throws InterruptedException {
            RoomAllocator roomAllocator = RoomAllocator.getInstance();
            // 开启事务
            TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
            try {
                roomAllocator.lock(roomId);
                if (isConflict(roomId, startTime, endTime)) {
                    // throw exception
                }
                // 添加房间排期...
                // 添加账单
    
                // 提交事务
                dataSourceTransactionManager.commit(transaction);
            } catch (Exception e) {
                // 回滚事务
                dataSourceTransactionManager.rollback(transaction);
                throw e;
            } finally {
                roomAllocator.unlock(roomId);
            }
        }
    

    那么此次修改后,使用房间1开房和房间2开房就可以并行执行了。

    总结

    上面的例子可能会有其他更好的方法去解决,但是我的实力不允许我这么做....。这个例子也是我自己在项目中搞事情搞出来的。毕竟没有实战经验,只有理论,不足以学好并发。希望大家也可以在项目中搞事情[坏笑],当然不能瞎搞。
    后续如果在其他场景用到了并发,也会继续写并发实战的文章哦~

    个人博客网址: https://colablog.cn/

    如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您
    微信公众号

  • 相关阅读:
    Kubernetes 命令行工具之kubctl
    新一代数据库之Etcd 简介
    算法题 打家劫舍(动态规划)
    算法题 位1的个数
    Class强制类型转换
    算法题 阶乘后的零
    算法题 Excel表列序号
    多数元素
    有序数组两数之和
    一杯果汁和一杯水的故事
  • 原文地址:https://www.cnblogs.com/Johnson-lin/p/13053552.html
Copyright © 2011-2022 走看看