前提

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

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

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

Java并发编程实战总结 (一)的更多相关文章

  1. 【Java并发编程实战】----- AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...

  2. 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport

    在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...

  3. 【Java并发编程实战】----- AQS(二):获取锁、释放锁

    上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...

  4. 【Java并发编程实战】-----“J.U.C”:CountDownlatch

    上篇博文([Java并发编程实战]-----"J.U.C":CyclicBarrier)LZ介绍了CyclicBarrier.CyclicBarrier所描述的是"允许一 ...

  5. 【Java并发编程实战】-----“J.U.C”:CyclicBarrier

    在上篇博客([Java并发编程实战]-----"J.U.C":Semaphore)中,LZ介绍了Semaphore,下面LZ介绍CyclicBarrier.在JDK API中是这么 ...

  6. 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock

    ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...

  7. 【Java并发编程实战】-----“J.U.C”:Semaphore

    信号量Semaphore是一个控制访问多个共享资源的计数器,它本质上是一个"共享锁". Java并发提供了两种加锁模式:共享锁和独占锁.前面LZ介绍的ReentrantLock就是 ...

  8. 【Java并发编程实战】-----“J.U.C”:ReentrantLock之三unlock方法分析

    前篇博客LZ已经分析了ReentrantLock的lock()实现过程,我们了解到lock实现机制有公平锁和非公平锁,两者的主要区别在于公平锁要按照CLH队列等待获取锁,而非公平锁无视CLH队列直接获 ...

  9. 【Java并发编程实战】-----“J.U.C”:ReentrantLock之一简介

    注:由于要介绍ReentrantLock的东西太多了,免得各位客官看累,所以分三篇博客来阐述.本篇博客介绍ReentrantLock基本内容,后两篇博客从源码级别分别阐述ReentrantLock的l ...

  10. 【java并发编程实战】-----线程基本概念

    学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习 ...

随机推荐

  1. Codeforces 909E(Coprocessor,双队列维护)

    题意:给出n个待处理的事件(0 ~n-1),再给出了n个标(0表示只能在主处理器中处理这个事件,1表示只能在副处理器中处理这个事件),处理器每次能处理多个任务.每个事件有关联,如果一个任务要在副处理器 ...

  2. java 多线程详细笔记(原理理解到全部使用)

    鸽了好久以后终于又更新了,看同学去实习都是先学源码然后修改之类,才发觉只是知道语法怎么用还远远不够,必须要深入理解以后不管是学习还是工作,才能举一反三,快速掌握. 目录 基础知识 进程与线程 线程原子 ...

  3. 「雕爷学编程」Arduino动手做(37)——MQ-3酒精传感器

    37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里 ...

  4. nodejs上使用sql

    1.首先本地要安装mysql, https://www.mysql.com/downloads/. 2.在node中连接mysql,要安装mysql驱动,也就是npm安装mysql模块:npm i m ...

  5. python3.x 基础二:内置函数

    自带的函数可以非常简单快捷的实现某些功能, 比如产生一个序列,可以用循环实现: count = 0 while count < 10: print(count) count+=1 但其实用ran ...

  6. mpvue从一无所有开始仿大众点评小程序

    最近尝试了下用mpvue框架开发小程序,它是基于vue开发的. 官方介绍: mpvue 是一个使用 Vue.js 开发小程序的前端框架.框架基于 Vue.js 核心,mpvue 修改了 Vue.js ...

  7. 简述 zookeeper 基于 Zab 协议实现选主及事务提交

    Zab 协议:zookeeper 基于 Paxos 协议的改进协议 zookeeper atomic broadcast 原子广播协议. zookeeper 基于 Zab 协议实现选主及事务提交. 一 ...

  8. Spring Boot 教程(1) - HelloWorld

    Spring Boot 教程 - HelloWorld 1. Spring Boot 的由来 大家都知道,Spring框架是Java生态中举足轻重的轻量型框架,帮助我们广大的大佬们进行Java开发.S ...

  9. 不可不知的 7 个 JDK 命令

    这篇文章主要来介绍下 JDK 内置的命令,话不多说,让我们开始吧! javap 使用 javap 可以查看 Java 字节码反编译的源文件,javap 的命令格式如下: 下面来演示下用 javap - ...

  10. Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.26/images/j..........

    如果你在执行 docker images 出现这样的问题 注意:把你的用户切到root下