Java并发编程实战总结 (一)

  • 时间: 2020-06-06 11:07:30

前提

首先该场景是一个酒店开房的业务。为了朋友们阅读简单,我把业务都简化了。

业务:开房后会添加一条账单,添加一条房间排期记录,房间排期主要是为了房间使用的时间不冲突。如:账单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;/** * 开房业务类 */@Servicepublic 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) {        // 判断房间排期是否有冲突...    }}
sychronized(RoomSchedule.class)sychronized

错误点:有些朋友可能会想到都是串行执行了,为什么不把 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/

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