javaEE -5(8000字详解多线程)

一:JUC(java.util.concurrent) 的常见类

1.1 ReentrantLock

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全,ReentrantLock 也是可重入锁. “Reentrant” 这个单词的原意就是 “可重入”

ReentrantLock 的用法:

  1. lock() 方法:

    • void lock():如果锁可用,获取锁。如果锁不可用,当前线程会被阻塞,直到获取到锁为止。这是一个典型的阻塞式调用。
  2. tryLock() 方法:

    • boolean tryLock():尝试获取锁。如果锁可用,立即获取锁,并返回 true。如果锁不可用,立即返回 false,而不会等待。
    • boolean tryLock(long timeout, TimeUnit unit):加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁,如果在指定的超时时间内获取到锁,则返回 true,否则返回 false。
  3. unlock() 方法:

    • void unlock():释放锁,允许其他线程尝试获取锁。在使用完锁之后,必须显式地调用此方法进行解锁,否则会导致死锁。

下面是一个使用 ReentrantLock 的简单示例:

import java.util.concurrent.locks.ReentrantLock;class MyThread implements Runnable {private static ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {try {lock.lock(); // 加锁// 临界区代码} finally {lock.unlock(); // 解锁}}
}public class Main {public static void main(String[] args) {MyThread myThread = new MyThread();Thread thread1 = new Thread(myThread);Thread thread2 = new Thread(myThread);thread1.start();thread2.start();}
}

ReentrantLock 和 synchronized 的区别:

  • synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,但是也容易遗漏
    unlock.
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
// ReentrantLock 的构造方法
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}
  • 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒.每次唤醒的是一个随机等待的线程.ReentrantLock 搭配 Condition 类实现等待-唤醒,可以更精确控制唤醒某个指定的线程.

如何选择使用哪个锁?

  • 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
  • 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
  • 如果需要使用公平锁, 使用 ReentrantLock.

1.2 原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个

  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference

以 AtomicInteger 举例,常见方法有

addAndGet(int delta);  i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;

二:线程池

虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.

线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 "池子"中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.

2.1 ExecutorService 和 Executors

代码示例:

  • ExecutorService 表示一个线程池实例.
  • Executors 是一个工厂类, 能够创建出几种不同风格的线程池.
  • ExecutorService 的 submit 方法能够向线程池中提交若干个任务.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}
});

Executors 创建线程池的几种方式

  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

我们之前提到过:Executors 本质上是 ThreadPoolExecutor 类的封装,那么什么是ThreadPoolExecutor呢?

2.2 ThreadPoolExecutor

ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定.

ThreadPoolExecutor是Java中用于创建和管理线程池的类。它提供了多个不同的构造方法来满足不同的需求。下面是ThreadPoolExecutor常用的四个构造方法的讲解:

  1. ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue):
    该构造方法指定了核心线程池大小(corePoolSize)、最大线程池大小(maximumPoolSize)、线程的空闲时间(keepAliveTime)以及这些参数的单位(unit),还有一个用于保存等待执行任务的阻塞队列(workQueue)。当一个任务提交到线程池时,如果线程池中的线程数量小于corePoolSize,则会创建一个新的线程来执行任务。如果线程池中的线程数量已经达到corePoolSize,但是阻塞队列(workQueue)还未满,则将任务添加到阻塞队列中等待执行。如果阻塞队列已满,且线程池中的线程数小于maximumPoolSize,则创建新的线程来执行任务。如果线程池中的线程数已达到maximumPoolSize且阻塞队列也已满,则根据ThreadPoolExecutor的拒绝策略来处理该任务。

  2. ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory):
    该构造方法与前一个构造方法相同,不过多了一个参数threadFactory,用于自定义创建线程的工厂类。可以通过自定义线程工厂来为线程池中的线程指定名称、设置线程的优先级等。

  3. ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler):
    该构造方法与第一个构造方法相同,不过多了一个参数handler,用于处理任务被拒绝后的策略。RejectedExecutionHandler是一个接口,有四种预定义的实现:AbortPolicy(默认策略,直接抛出RejectedExecutionException异常)、CallerRunsPolicy(使用调用线程来执行被拒绝的任务)、DiscardOldestPolicy(丢弃阻塞队列中最旧的任务)、DiscardPolicy(直接丢弃被拒绝的任务)。

  4. ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler):
    该构造方法将前面三个构造方法的参数都包含进来,并多了一个threadFactory和handler参数。

理解 ThreadPoolExecutor 构造方法的参数
把创建一个线程池想象成开个公司. 每个员工相当于一个线程.

  • corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)
  • maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).
  • keepAliveTime: 临时工允许的空闲时间.
  • unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
  • workQueue: 传递任务的阻塞队列
  • threadFactory: 创建线程的工厂, 参与具体的创建线程工作.
  • RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.
  • AbortPolicy(): 超过负荷, 直接抛出异常.
  • CallerRunsPolicy(): 调用者负责处理
  • DiscardOldestPolicy(): 丢弃队列中最老的任务.
  • DiscardPolicy(): 丢弃新来的任务.

代码示例:

ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,new SynchronousQueue<Runnable>(),Executors.defaultThreadFactory(),newThreadPoolExecutor.AbortPolicy());
for(int i=0;i<3;i++) {pool.submit(new Runnable() {@Overridevoid run() {System.out.println("hello");}});
}

2.3线程池的工作流程

在这里插入图片描述

2.4 信号量 Semaphore

信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.

理解信号量:
可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.

当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)

当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)

如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

注意:Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.

代码示例:

  • 创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源.
  • acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
  • 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果.
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {@Overridepublic void run() {try {System.out.println("申请资源");semaphore.acquire();System.out.println("我获取到资源了");Thread.sleep(1000);System.out.println("我释放资源了");semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}}
};
for (int i = 0; i < 20; i++) {Thread t = new Thread(runnable);t.start();
}

2.5 CountDownLatch

就好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。

  • 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
  • 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
  • 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
public class Demo {public static void main(String[] args) throws Exception {CountDownLatch latch = new CountDownLatch(10);Runnable r = new Runable() {@Overridepublic void run() {try {Thread.sleep(Math.random() * 10000);latch.countDown();} catch (Exception e) {e.printStackTrace();}}};for (int i = 0; i < 10; i++) {new Thread(r).start();}// 必须等到 10 人全部回来latch.await();System.out.println("比赛结束");}
}

三:线程安全的集合类

原来的集合类, 大部分都不是线程安全的(Vector, Stack, HashTable, 是线程安全的其他的集合类不是线程安全的)

3.1多线程环境使用 ArrayList

  1. 自己使用同步机制 (synchronized 或者 ReentrantLock)
    前面做过很多相关的讨论了. 此处不再展开.

  2. Collections.synchronizedList(new ArrayList);
    synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
    synchronizedList 的关键操作上都带有 synchronized

  3. 使用 CopyOnWriteArrayList
    CopyOnWrite容器即写时复制的容器。

  • 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,
  • 添加完元素之后,再将原容器的引用指向新的容器。

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

优点:

  • 在读多写少的场景下, 性能很高, 不需要加锁竞争.

缺点:

  1. 占用内存较多.
  2. 新写的数据不能被第一时间读取到.

3.2 多线程环境使用队列

  1. ArrayBlockingQueue
    基于数组实现的阻塞队列

  2. LinkedBlockingQueue
    基于链表实现的阻塞队列

  3. PriorityBlockingQueue
    基于堆实现的带优先级的阻塞队列

  4. TransferQueue
    最多只包含一个元素的阻塞队列

3.3 多线程环境使用哈希表

HashMap 本身不是线程安全的,在多线程环境下使用哈希表可以使用:

  • Hashtable
  • ConcurrentHashMap
  1. Hashtable
    只是简单的把关键方法加上了 synchronized 关键字.
    在这里插入图片描述
    在这里插入图片描述

这相当于直接针对 Hashtable 对象本身加锁.

  • 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
  • size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
  • 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.

在这里插入图片描述
2) ConcurrentHashMap
相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例

  • 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是是用
    synchronized, 但是不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率.
  • 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
  • 优化了扩容方式: 化整为零

扩容方式为:

  1. 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
  2. 扩容期间, 新老数组同时存在.
  3. 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素.
  4. 搬完最后一个元素再把老数组删掉.
  5. 这个期间, 插入只往新数组加.
  6. 这个期间, 查找需要同时查新数组和老数组

在这里插入图片描述

四:死锁

死锁是多个进程或线程由于竞争共享资源而陷入的一种无限等待的状态,导致它们都无法继续执行。在死锁中,每个进程或线程都在等待其他进程或线程释放资源,但这些资源都被其他进程或线程占用,从而导致所有参与者都无法继续执行,形成了相互等待的循环。

死锁通常发生在以下四个必要条件同时满足时:

  1. 互斥条件:至少有一个资源在任意时刻只能被一个进程或线程占用。
  2. 请求和保持条件:进程或线程至少要占用一个资源,并请求其他资源。
  3. 不剥夺条件:资源只能由持有者显式地释放,其他进程无法强制性地剥夺资源。
  4. 循环等待条件:形成一个等待资源的环路,每个进程或线程都在等待下一个资源的释放。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/118292.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TypeScript学习 | 泛型

简介 泛型是指在定义函数、接口或类的时候&#xff0c;不预先指定具体的类型&#xff0c;而在使用的时候再指定类型的一种特性 作用 可以保证类型安全的前提下&#xff0c;让函数、接口或类与多种类型一起工作&#xff0c;从而实现复用 基本使用 举个例子&#xff1a; 创…

T113-S3-buildroot文件系统tar解压缩gz文件

目录 前言 一、现象描述 二、解决方案 三、tar解压缩.gz文件 总结 前言 本文主要介绍全志T113-S3平台官方SDK&#xff0c;buildroot文件系统tar不支持.gz文件解压缩的问题以及如何配置buildroot文件系统解决该问题的方法介绍。 一、现象描述 在buildroot文件系统中&#xff…

二、可行性分析与需求分析

文章目录 概念考点练习题一、可行性分析与需求分析1.可行性分析的任务2.可行性研究3.甘特图4.数据流图5.数据字典数据字典的内容 6.需求分析7. 实体联系ER图8. 状态转换图 二、练习题 概念考点练习题 一、可行性分析与需求分析 1.可行性分析的任务 用最小的代价在尽可能短的时…

【德哥说库系列】-PostgreSQL跨版本升级

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

区块链外包开发需要注意的问题

在进行区块链外包开发时&#xff0c;有一些关键问题需要特别注意&#xff0c;以确保项目的成功和质量。以下是一些需要考虑的问题&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.明确的需求和目标&…

【网络协议】聊聊TCP的三挥四握

上一篇我们说了网络其实是不稳定的&#xff0c;TCP和UDP其实是两个不同的对立者&#xff0c;所以TCP为了保证数据在网络中传输的可靠性&#xff0c;从丢包、乱序、重传、拥塞等场景有自己的一套打法。 TCP格式 源端口和目标端口是不可缺少的&#xff0c;用以区分到达发送给拿…

windows安装数据库MySQL

windows安装数据库MySQL 文章目录 windows安装数据库MySQL一、MySQL官网下载压缩包二、在D盘新建文件夹D:\MySQL&#xff0c;将下载的压缩包解压到该文件夹下三、配置环境变量四、通过命令行模式安装、启用、配置SQL服务 一、MySQL官网下载压缩包 下载地址&#xff1a;https:/…

APP逆向基础(APK流程)

APK的基本结构 Android体系结构和APK基本结构-CSDN博客 APK 打包流程 【Android 安装包优化】APK 打包流程 ( 文件结构 | 打包流程 | 安装流程 | 安卓虚拟机 )_adnroid 安装包优化,打指定资源_韩曙亮的博客-CSDN博客 APK安装流程

Go 实现插入排序算法及优化

插入排序 插入排序是一种简单的排序算法&#xff0c;以数组为例&#xff0c;我们可以把数组看成是多个数组组成。插入排序的基本思想是往前面已排好序的数组中插入一个元素&#xff0c;组成一个新的数组&#xff0c;此数组依然有序。光看文字可能不理解&#xff0c;让我们看看…

Power BI 傻瓜入门 9. 设计和部署数据模型

本章内容包含&#xff1a; 详细说明设计数据模型的技术要求Power BI Desktop中基本数据模型的设计将数据模型从Power BI Desktop发布到Power BI Services 在数据进入Power BI后对其进行操作既是一门艺术&#xff0c;也是一门科学。导入到任何应用程序中的数据不仅需要注意数据…

【论文阅读】(2023TPAMI)PCRLv2

目录 AbstractMethodMethodnsU-Net中的特征金字塔多尺度像素恢复多尺度特征比较从多剪切到下剪切训练目标 总结 Abstract 现有方法及其缺点&#xff1a;最近的SSL方法大多是对比学习方法&#xff0c;它的目标是通过比较不同图像视图来保留潜在表示中的不变合判别语义&#xff…

[AutoSar NVM] 存储架构

依AutoSAR及公开知识辛苦整理&#xff0c;禁止转载。 专栏 《深入浅出AutoSAR》&#xff0c; 全文 2900 字. 图片来源&#xff1a; 知乎 汽车的ECU内存中有很多不同类型的变量&#xff0c;这些变量包括了车辆各个系统和功能所需的数据。大部分变量在ECU掉电后就会丢失&#x…

深入探索Sharding JDBC:分库分表的利器

随着互联网应用的不断发展和用户量的不断增加&#xff0c;传统的数据库在应对高并发和大数据量的场景下面临着巨大的挑战。为了解决这一问题&#xff0c;分库分表成为了一个非常流行的方案。分库分表主流的技术包括MyCat和Sharding JDBC。我们来通过一张图来了解这两者有什么区…

性能测试jmeter命令行运行+html测试报告解读

windows下打开jmeter的运行窗口&#xff0c;可以看到提示不要用GUI模式进行负载测试&#xff0c;如果要用负载测试&#xff0c;用cli模式&#xff0c;因为GUI模式运行jmeter比较消耗性能。 命令行模式 windows下找到jemeter所在文件夹&#xff0c;打开cmd输入命令。 jmeter -n…

Centos安装gitlabce

服务器配置要求&#xff08;2c4g&#xff09; 1、 安装其他组件 yum install -y curl policycoreutils-python openssh perl2、 安装Postfix服务以发送电子邮件通知&#xff0c;启动服务并自启 yum -y install postfix systemctl enable postfix --now3、 安装gitlab&#xf…

Linux系统之file命令的基本使用

Linux系统之file命令的基本使用 一、file命令介绍1.1 Linux简介1.2 file命令简介 二、file命令的使用帮助2.1 file命令的help帮助信息2.2 file命令的语法解释2.3 file命令的man手册 三、文件类型介绍四、file命令的基本使用4.1 查询file版本4.2 显示文件类型4.3 输出时不显示文…

Ubuntu 安装 docker-compose

在Ubuntu上安装Docker Compose&#xff0c;可以按照以下步骤进行操作&#xff1a; 下载 Docker Compose 二进制文件 sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker…

39.B树,B+树(王道第7章查找补充知识)

目录 一. B树 &#xff08;1&#xff09;B树的定义 &#xff08;2&#xff09;B树的高度 &#xff08;3&#xff09;B树的插入 &#xff08;4&#xff09;B树的删除 二. B树 &#xff08;1&#xff09;B树的定义 &#xff08;2&#xff09;B树与B树的区别 一. B树 &am…

软件测试进阶篇----自动化测试脚本开发

自动化测试脚本开发 一、自动化测试用例开发 1、用例设计需要注意的点 2、设计一条测试用例 二、脚本开发过程中的技术 1、线性脚本开发 2、模块化脚本开发&#xff08;封装线性代码到方法或者类中。在需要的地方进行调用&#xff09; 3、关键字驱动开发&#xff1a;selen…

rust学习——智能指针

智能指针 在各个编程语言中&#xff0c;指针的概念几乎都是相同的&#xff1a;指针是一个包含了内存地址的变量&#xff0c;该内存地址引用或者指向了另外的数据。 在 Rust 中&#xff0c;最常见的指针类型是引用&#xff0c;引用通过 & 符号表示。不同于其它语言&#xf…