概述

  《手写高并发下线程安全的单例模式》主要介绍使用枚举类实现JAVA单例模式,以及在高并发环境下验证此单例模式是线程安全的。本文借助ReentrantLock、CountDownLatch和Semaphore等,基于线程池验证如何创建线程安全和不安全的方法。

  实际项目中,我们有很多高并发的场景需要考虑、设计,在高并发领域有个耳熟能详的名词叫惊群效应。以喂鸽子为例进行说明,当你往一群鸽子中间扔一块食物时,虽然最终只有一只鸽子抢到食物,但所有鸽子都会被惊动从而飞过来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。也就是说,虽然只扔了一块食物,但是惊动了整个鸽群,最后却只有一只鸽子抢到食物,浪费了其它鸽子的能量。

  对于操作系统来说,多个进程/线程在等待同一资源时,也会产生类似的效果,其结果就是每当有可用资源时,所有的进程/线程都来竞争资源,造成了资源的浪费[2]

  • 系统对用户进程/线程频繁做无效的调度和上下文切换,导致系统性能大打折扣。
  • 为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。

  下面基于ReentrantLock、CountDownLatch和Semaphore创建一个并发模拟工具,当被调用函数不出现惊群效应时,说明是线程安全的。

  CountDownLatch是一个能阻塞主线程,让其它线程满足特定条件下再继续执行的工具。比如倒计时3000,每当一个线程完成一次操作就让它执行countDown一次,直到count为0之后输出结果,这样就保证了其它线程一定是满足了特定条件(执行某操作5000次),模拟了并发执行次数。

  Semaphore信号量是一个能阻塞线程且能控制统一时间请求的并发量的工具。比如能保证同时执行的线程最多200个,模拟出稳定的并发量。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。

模拟工具

  创建模拟工具,首先创建一个线程安全地售票的任务类:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock; /**
* 线程安全地售票
*/
public class SafeSale implements Runnable { //定义车票的数量
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock(); @Override
public void run() {
boolean a = true;
while (a) {
try {
//对操作共享数据的代码进行加锁
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":" + "出售第" + ticket + "张车票");
//车票数量减一
ticket--;
//线程休眠,增加其他线程调用的机会
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
a = false;
}
} finally {
lock.unlock(); //进行解锁
}
}
}
}

  紧接着,创建测试用例所需要的非线程安全函数和main函数:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore; /**
* 模拟高并发
*/
public class ConcurrencyCheck {
// 执行次数
private static final int THREAD_COUNT = 3000;
// 并发数
private static final int CONCURRENT_COUNT = 200;
// 全局变量,容易出幺蛾子
private static int count = 10000; public static void main(String[] args) throws InterruptedException {
lockSale();
checkUtil();
} /**
* 基于lock方法售票
*/
private static void lockSale() {
ExecutorService executorService = Executors.newCachedThreadPool();
SafeSale sale = new SafeSale();
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.execute(sale);
}
executorService.shutdown();
} /**
* 模拟线程非安全,模拟工具主题
* @throws InterruptedException
*/
private static void checkUtil() throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
Semaphore semaphore = new Semaphore(CONCURRENT_COUNT);
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
subtraction();
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("计数结果:" + count);
} /**
* 售票,非线程安全,被验证对象
*/
private static void subtraction() {
count--;
}
}

  如果多运行几次如上main函数就会发现,方法subtraction()并不是线程安全的,即其执行结果几乎都是大于7000,很少等于7000。

Reference

[1] https://www.jianshu.com/p/2da329ba5349

[2] https://blog.csdn.net/shipfei_csdn/article/details/103110621

Java基于线程池和AQS模拟高并发的更多相关文章

  1. springboot2.0+线程池+Jmeter以模拟高并发

    声明:原创在这里https://blog.csdn.net/u011677147/article/details/80271174,在此也谢谢哥们. 1.目录结构 2.BusinessThread.j ...

  2. 【Java TCP/IP Socket】基于线程池的TCP服务器(含代码)

    了解线程池 在http://blog.csdn.net/ns_code/article/details/14105457(读书笔记一:TCP Socket)这篇博文中,服务器端采用的实现方式是:一个客 ...

  3. Java中线程池的学习

    线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理.当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程 ...

  4. 基于线程池的多线程售票demo2.0(原创)

    继上回基于线程池的多线程售票demo,具体链接: http://www.cnblogs.com/xifenglou/p/8807323.html以上算是单机版的实现,特别使用了redis 实现分布式锁 ...

  5. 基于线程池的多线程售票demo(原创)

    废话不多说,直接就开撸import org.springframework.util.StopWatch;import java.util.concurrent.*;/** * 基于线程池实现的多线程 ...

  6. 深入理解Java之线程池

    原作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本文归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则 ...

  7. 设计模式:基于线程池的并发Visitor模式

    1.前言 第二篇设计模式的文章我们谈谈Visitor模式. 当然,不是简单的列个的demo,我们以电商网站中的购物车功能为背景,使用线程池实现并发的Visitor模式,并聊聊其中的几个关键点. 一,基 ...

  8. java利用线程池处理集合

    java利用线程池处理集合 2018年07月23日 17:21:19 衍夏成歌 阅读数:866   版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/s ...

  9. 深入理解Java之线程池(爱奇艺面试)

    爱奇艺的面试官问 (1) 线程池是如何关闭的 (2) 如何确定线程池的数量 一.线程池销毁,停止线程池 ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown() ...

  10. Java中线程池,你真的会用吗?

    在<深入源码分析Java线程池的实现原理>这篇文章中,我们介绍过了Java中线程池的常见用法以及基本原理. 在文中有这样一段描述: 可以通过Executors静态工厂构建线程池,但一般不建 ...

随机推荐

  1. 面试题54. 二叉搜索树的第k大节点

    地址:https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/ <?php /** 面试题54. ...

  2. C# Quartz 调度任务辅助类

    1 public class QuartzHelper 2 { 3 /// <summary> 4 /// 时间间隔执行任务 5 /// </summary> 6 /// &l ...

  3. 基于C语言实现UDP服务器

    UDP(User Datagram Protocol,用户数据报协议)是一种无连接的传输层协议,适用于对实时性有较高要求的应用场景,如视频流传输.语音通信.在线游戏等.与TCP不同,UDP不保证数据的 ...

  4. C语言(数据结构)时间标记

    数据结构 时间标记 循环输出1~N的数: #include<stdio.h>#include<time.h>clock_t start,stop;//clock_t是clock ...

  5. Jupyter Notebook的所有文件ipynb保存下来

    前言 如果你想要保存整个 Jupyter Notebook 工作目录,包括所有笔记本和其他相关文件,最直接的方法是将整个文件夹压缩为一个 ZIP 或 TAR 文件. 下载单个文件 压缩文件夹下载 在 ...

  6. mac输入法 cpu占用,解决mac使用输入法出现卡顿延迟

    1.介绍 网上有各种方法,例如有touchbar的macbook关闭输入建议:定时重启"简体中文输入法"进程:关闭"显示器具有单独的空间" 这些方法网上都能看到 ...

  7. 基础指令:三剑客之sed、三剑客之awk详解

    目录 4.9 三剑客之sed(查找.替换.删除.插入) 作用: 语法格式: 4.9.1 sed指定行输出 [行数]p 4.9.2 sed模糊搜索 /[字符串]/p 4.9.3 按照区间进行过滤查找 [ ...

  8. Python 生成器说明

    生成器 python 生成器 常规形态 # list def square_numbers(nums: list): squared_nums = [] for i in nums: squared_ ...

  9. 10年+ .NET Coder 心语 ── 继承的思维:从思维模式到架构设计的深度解析

    引言 ❝ 小编是一名10年+的.NET Coder,期间也写过Java.Python,从中深刻的认识到了软件开发与语言的无关性.现在小编已经脱离了一线开发岗位,在带领团队的过程中,发现了很多的问题,究 ...

  10. 比较LLM的function calling,Agent 和MCP

    比较 对比维度 ​MCP(Model Context Protocol)​ ​Function Calling ​Agent(智能体)​ ​定义 由 Anthropic 推出的开放协议,标准化 LLM ...