教你用三种方式模拟两个线程抢票

Java后端技术

共 9682字,需浏览 20分钟

 ·

2024-06-25 09:19

往期热门文章:


    

1、MySQL中varchar(50)和varchar(500)区别是什么?

2、顶级Javaer都在使用的类库,真香!

3、最适合程序员的画图工具?

4、Logback 与 log4j2 性能哪个更强?

5、只用Tomcat,不用Nginx行不行?


前言

在多线程编程中,资源竞争是一个常见的问题。资源竞争发生在多个线程试图同时访问或修改共享资源时,可能导致数据不一致或其他并发问题。在模拟两个线程抢票的场景中,我们需要考虑如何公平地分配票,并确保每个线程都有机会成功获取票。

本篇文章将通过三种方式来模拟两个线程抢票的过程,以展示不同的并发控制策略。

这三种方式包括:

  • 使用 Synchronized 来确保一次只有一个线程可以访问票资源。

  • 使用 ReentrantLock 来实现线程间的协调。

  • 使用 Semaphore 来限制同时访问票的线程数量。


通过比较这三种方式,我们可以深入了解并发控制的不同实现方式及其优缺点。在实际应用中,需要根据具体场景和需求选择合适的并发控制策略。

此外,为了更直观地展示抢票过程,我们将使用代码来描述每种方式的实现逻辑。

一、Synchronized

含义:Synchronized 是 Java 中的一个关键字,用于实现线程同步。当一个方法或代码块被 Synchronized 修饰时,同一时间只能有一个线程可以执行这个方法或代码块。


代码如下:
static class TicketSystemBySynchronized {  private int tickets = 100;
public void sellTicket() { while (tickets > 0) { //还有票时进行循环 synchronized (this) { try { if (tickets > 0) System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余票数:" + --tickets); Thread.sleep(200); //模拟售票 } catch (InterruptedException e) { e.printStackTrace(); } } } }}
这个类中有一个私有的整型变量 tickets,表示票的总数,初始值为 100。

类中有一个公共方法 sellTicket(),这个方法模拟售票过程。当还有票(tickets > 0)时,会进入一个 while 循环。在循环中,首先通过 synchronized (this) 对当前对象进行同步,保证同一时间只有一个线程可以执行以下代码块。

在同步代码块中,首先检查票的数量是否大于0。如果是,则输出当前线程的名称以及售出的票数和剩余票数。然后,通过 --tickets 操作将票的数量减1。

接下来,线程休眠 200 毫秒(模拟售票过程)。休眠结束后,循环继续执行,直到票的数量为 0。

二、ReentrantLock

含义:ReentrantLock,也称为可重入锁,是一种递归无阻塞的同步机制。它可以等同于 synchronized 的使用,但是 ReentrantLock 提供了比 synchronized 更强大、灵活的锁机制,可以减少死锁发生的概率。


代码如下:
static class TicketSystemByReentrantLock {  private int tickets = 100;
private final ReentrantLock lock = new ReentrantLock(); //定义锁
public void sellTicket() { while (tickets > 0) { lock.lock(); //上锁 try { Thread.sleep(200); //模拟售票 if (tickets > 0) System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余票数:" + --tickets); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); //解锁 } } }}
这个类中有一个私有的整型变量 tickets,表示票的总数,初始值为 100。另外定义了一个私有的 final 类型的 ReentrantLock 对象 lock,这个对象用于控制对共享资源的访问。

类中有一个公共方法 sellTicket(),这个方法模拟售票过程。当还有票(tickets > 0)时,会进入一个 while 循环。在循环中,首先通过 lock.lock() 获取锁,保证同一时间只有一个线程可以执行以下代码块。

在锁保护的代码块中,首先线程休眠 200 毫秒(模拟售票过程)。然后检查票的数量是否大于 0。如果是,则输出当前线程的名称以及售出的票数和剩余票数。然后,通过 --tickets 操作将票的数量减 1。

最后,都会通过 lock.unlock() 释放锁。防止死锁!

三、Semaphore

含义:Semaphore 是一种计数信号量,用于管理一组资源。它是一种在多线程环境下使用的设施,该设施负责协调各个线程,以保证它们能够正确、合理地使用公共资源。Semaphore 内部基于 AQS(Abstract Queued Synchronizer)的共享模式,相当于给线程规定一个量从而控制允许活动的线程数。


代码如下:


static class TicketSystemBySemaphore { private final Semaphore semaphore;
public TicketSystemBySemaphore() { this.semaphore = new Semaphore(100); //总共100张票 }
public void sellTicket() { int i = semaphore.availablePermits(); //返回此信号量中当前可用的许可证数
while (i > 0) { try { Thread.sleep(200); semaphore.acquire(); // 获取信号量,如果信号量为0,线程将阻塞等待 System.out.println( Thread.currentThread().getName() + "卖出一张票,剩余票数:" + --i); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { semaphore.release(); // 释放信号量,允许其他线程获取信号量 } } }}

Semaphore 是一个计数信号量,用于控制资源的并发访问。在构造函数中,初始化了这个 Semaphore,设置总的可用票数为 100。

sellTicket() 方法模拟售票过程。首先获取当前可用的票数,然后进入一个 while 循环,只要还有可用的票,就会尝试获取一个票。如果当前没有可用的票,线程将会阻塞等待。一旦获取了票,就输出售出的信息。最后释放信号量。

四、抽象工厂模式优化

含义:抽象工厂模式是一种创建型设计模式,它为创建一系列相关或互相依赖的对象提供了一种最佳解决方案。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。



因为要对三种实现类型的代码进行测试,不想多写 if...else... 的代码,不想每次指定创建的对象,也为了防止以后有更多实现方法的不方便。提高代码的可维护性和可扩展性。

所以这里采用抽象工厂模式来进行优化。

代码如下:

首先实现一个接口类:
public interface TicketSystem {    void sellTicket();}

因为三个模拟实现中都定义了 sellTicket 这个方法,所以在接口类里面定义一个方法,然后由实现类去重写该方法。

接下来实现静态工厂类:
static class CodeSandboxFactory {  static TicketSystem newInstance(String type) {    switch (type) {      case "Synchronized":        return new TicketSystemBySynchronized();      case "ReentrantLock":        return new TicketSystemByReentrantLock();      case "Semaphore":      default:        return new TicketSystemBySemaphore();    }  }}

这个 CodeSandboxFactory 类是一个静态工厂类,用于创建TicketSystem对象的不同实例。它接受一个字符串参数 type,根据该参数的值决定创建哪种类型的TicketSystem 对象。

  • 如果type参数的值为"Synchronized",则返回一个新的 TicketSystemBySynchronized对象;

  • 如果type参数的值为"ReentrantLock",则返回一个新的 TicketSystemByReentrantLock 对象;

  • 如果type参数的值为"Semaphore",则返回一个新的 TicketSystemBySemaphore对象;

  • 如果type参数的值不是以上三种之一,则默认返回一个新的TicketSystemBySemaphore 对象。


这种设计使得客户端代码可以方便地通过传递不同的类型字符串来获取不同类型的 TicketSystem 对象,而不需要关心这些对象的实际创建过程。

这有助于降低客户端代码与具体实现之间的耦合度,提高代码的可维护性和可扩展性。

五、整体代码 

代码如下:

public class ThreadsGrabTickets {  public static void main(String[] args) {    TicketSystem system = CodeSandboxFactory.newInstance("Synchronized");    //        TicketSystem system =    //        CodeSandboxFactory.newInstance("ReentrantLock"); TicketSystem    //        system = CodeSandboxFactory.newInstance("Semaphore");
new Thread(system::sellTicket, "线程1").start(); new Thread(system::sellTicket, "线程2").start(); }
static class CodeSandboxFactory { static TicketSystem newInstance(String type) { switch (type) { case "Synchronized": return new TicketSystemBySynchronized(); case "ReentrantLock": return new TicketSystemByReentrantLock(); case "Semaphore": default: return new TicketSystemBySemaphore(); } } }
static class TicketSystemBySynchronized implements TicketSystem { private int tickets = 100;
@Override public void sellTicket() { while (tickets > 0) { synchronized (this) { try { if (tickets > 0) System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余票数:" + --tickets); Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
static class TicketSystemByReentrantLock implements TicketSystem { private int tickets = 100;
private final ReentrantLock lock = new ReentrantLock(); //定义锁
@Override public void sellTicket() { while (tickets > 0) { lock.lock(); //上锁 try { Thread.sleep(200); //模拟售票 if (tickets > 0) System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余票数:" + --tickets); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); //解锁 } } } }
static class TicketSystemBySemaphore implements TicketSystem { private final Semaphore semaphore;
public TicketSystemBySemaphore() { this.semaphore = new Semaphore(100); //总共100张票 }
@Override public void sellTicket() { int i = semaphore.availablePermits(); //返回此信号量中当前可用的许可证数
while (i > 0) { try { Thread.sleep(200); semaphore.acquire(); // 获取信号量,如果信号量为0,线程将阻塞等待 System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余票数:" + --i); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { semaphore.release(); // 释放信号量,允许其他线程获取信号量 } } } }}

六、总结

本文通过模拟两个线程抢票的场景,展示了三种不同的并发控制策略:使用 Synchronized、ReentrantLock 和 Semaphore。

通过比较这三种方式,我们可以深入了解并发控制的不同实现方式。

在实际应用中,需要根据具体场景和需求选择合适的并发控制策略。

转自:绿皮龟,

链接:blog.csdn.net/kologin/article/details/135953580


   
往期热门文章:

1、听说你还在用Xshell?
2、惊艳到我的 10个 MySQL高级查询技巧!
3、我有点想用JDK17了
4、解放大脑:ChatGPT + PlantUML = 不用画图了
5、高逼格的SQL写法:行行比较
6、限流算法哪家强?时间窗口,令牌桶与漏桶算法对比
7、每天都提交代码,那你知道.git目录内部的秘密吗?
8、我患上了空指针后遗症
9、这10个小技巧让你减少80%的Bug!
10、升级 JDK17 一个不可拒绝的理由

浏览 133
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报