14

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

 4 years ago
source link: http://www.cnblogs.com/Johnson-lin/p/13053552.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前提

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

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

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

m6Fzqeq.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK