JavaEE初阶Day 14:多线程(12)

目录

  • Day 14 :多线程(12)
    • CAS的ABA问题
    • Callable接口
    • ReentrantLock
    • 信号量Semaphore
    • CountDownLatch
    • 集合类的多线程安全问题
      • 1. Collections.synchronizedList(new ArrayList)
      • 2. CopyOnWriteArrayList
      • 3. BlockingQueue
      • 4. ConcurrentHashMap

Day 14 :多线程(12)

回顾

锁策略

乐观锁VS悲观锁;轻量级锁VS重量级锁;自旋锁VS挂起等待锁;公平锁VS非公平锁;可重入锁VS不可重入锁;普通斥锁VS读写锁

synchronize基本特性与实现原理

  • 锁升级:偏向锁 --> 轻量级锁 --> 重量级锁
  • 锁消除:编译器优化
  • 锁粗化:编译器优化

CAS的ABA问题

使用CAS编写代码:比较然后再交换

在比较过程中:检查当前内存的值,是否被其他线程修改了,如果被修改了,就要稍后再重试,如果没被修改,接下来就可以直接修改,不会有线程安全问题,没有其他线程穿插执行。但是值没变 != 值没变过,有可能另一个线程把这个值从A变为B,再从B变为A了

ABA在大部分情况下没什么问题,但是在极端情况下,就可能产生bug

如何避免ABA问题,核心思路是引入版本号,约定版本号只能加不能减,每一次操作版本号都要+1,通过CAS判定版本号,如果版本号没有发生改变,数据就一定没有变过

Callable接口

Callable也是用来描述任务的,并且call方法带有返回值,表示这个线程执行结束会得到什么结果

package thread;public class Demo39 {private static int sum = 0;public static void main(String[] args) throws InterruptedException {//创建一个线程,让这个线程来实现 1 + 2 + 3 +......+ 1000Thread t = new Thread(new Runnable() {@Overridepublic void run() {int result = 0;for (int i = 0; i <= 1000; i++) {result += i;}//此处为了把result告知主线程,就需要通过静态成员变量倒腾一下sum = result;}});t.start();t.join();//主线程获取得到结果System.out.println(sum);}
}

上述代码主线程与t线程耦合太大了,线程内部定义的局部变量是不能被其他线程获取得到的,线程更多,就会更麻烦

Callable就是为了更优雅的解决上述问题

package thread;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo40 {public static void main(String[] args) throws InterruptedException, ExecutionException {Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int result = 0;for (int i = 0; i <= 1000; i++) {result += i;}return result;}};//创建线程,把callable搭载到线程内部执行FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();t.join();System.out.println(futureTask.get());}
}

创建线程的方式

  • 继承Thread
  • 使用Runnable
  • 使用lambda
  • 使用线程池/ThreadFactory
  • 使用Callable

ReentrantLock

ReentrantLock:可重入

package thread;import java.util.concurrent.locks.ReentrantLock;public class Demo41 {public static void main(String[] args) {ReentrantLock locker = new ReentrantLock();try {//加锁locker.lock();} finally {//解锁locker.unlock();}}
}
  • ReentrantLock提供了公平锁的实现,synchronized只是非公平锁,ReentrantLock locker = new ReentrantLock(true);表示公平锁,false/不填写表示非公平锁

  • ReentrantLock提供tryLock操作,给加锁提供了更多的可操作空间,尝试加锁,如果锁已经被获取到了,直接返回失败,而不会像synchronized遇到锁竞争会阻塞等待,tryLock也可以去指定等待超时时间

  • ReentrantLock搭配Condition类完成等待通知,synchronized搭配wait与notify等待通知机制,Condition可以指定线程唤醒,多个线程wait,notify是唤醒随机一个

信号量Semaphore

信号量就是一个计数器,描述了可用资源的个数,围绕信号量有两个基本操作

  • P操作:计数器+1,申请资源(acquire)
  • V操作:计数器-1,释放资源(release)
package thread;import java.util.concurrent.Semaphore;public class Demo42 {public static void main(String[] args) throws InterruptedException {//4个可用资源Semaphore semaphore = new Semaphore(4);semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");semaphore.acquire();System.out.println("P操作");}
}

上述代码进行了5次P操作,但是信号量只有4个可用资源,所以在第5次P操作的时候,会出现阻塞等待

锁其实就是特殊的信号量,如果信号量只有0、1两个取值,此时就称为”二元信号量“,本质就是一把锁

package thread;import java.util.concurrent.Semaphore;public class Demo43 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(1);Thread t1 = new Thread(()->{try {for (int i = 0; i < 50000; i++) {semaphore.acquire();count++;semaphore.release();}}catch (InterruptedException e){e.printStackTrace();}});Thread t2 = new Thread(()->{try {for (int i = 0; i < 50000; i++) {semaphore.acquire();count++;semaphore.release();}}catch (InterruptedException e){e.printStackTrace();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);}
}

CountDownLatch

当我们把一个任务拆分成很多个的时候,可以通过这个工具类来识别任务是否整体执行完毕

package thread;import java.util.concurrent.CountDownLatch;public class Demo44 {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(10);for (int i = 0; i < 10; i++) {int id = i;Thread t = new Thread(()->{System.out.println("线程启动" + id);try {//假设这里是进行一些“下载”这样的耗时操作Thread.sleep(3000);}catch (InterruptedException e){throw new RuntimeException(e);}System.out.println("线程结束" + id);latch.countDown();});t.start();}//通过await等待所有线程调用countDownlatch.await();System.out.println("所有线程结束");}
}

await会阻塞等待,一直到countDown调用的次数和构造方法指定的次数一致的时候,await才会返回,await不仅仅能代替join,比如有1000个任务,交给4个线程的线程池来执行,如何判定1000个任务执行完了,也可以使用CountDownLatch来判定(这个过程没有线程真正结束)

集合类的多线程安全问题

ArrayList、LinkedList、Stack、Queue、HashMap…大部分都是线程不安全的

  • Vector自带了synchronized,Stack继承了Vector,也自带了synchronized
  • Hashtable也自带了synchronized

加锁不能保证线程一定安全,不加锁也不能确定线程一定不安全

手动加锁比较麻烦,标准库提供了一些其他的解决方案

1. Collections.synchronizedList(new ArrayList)

给ArrayList这些集合类,套一层壳,壳上是给关键方法都加了synchronized,就可以使ArrayList达到类似于vector的效果

2. CopyOnWriteArrayList

写时拷贝:在读的时候读取旧的数组,在写的时候,使用新的数组来写,当写完之后,用新的数组的引用,代替旧的数组的引用(引用赋值操作,是原子的),旧的空间就可以释放了

上述过程,没有任何加锁和阻塞等待,也就能确保读线程不会读出错误的数据

上述操作其实实用性非常高,有些服务器程序需要更新配置文件/数据文件,就可以采取上述策略

  • 显卡渲染画面到显示器就是按照写时拷贝的方式,在显示上一个画面的时候,在背后用额外的空间生成下一个画面,生成完毕了,使用下一个画面代替上一个画面

3. BlockingQueue

多线程使用队列,直接使用BlockingQueue即可

4. ConcurrentHashMap

多线程使用哈希表,HashMap是线程不安全的,Hashtable是带锁的,但是标准库提供了更好的代替,即ConcurrentHashMap

Hashtable加锁是简单粗暴给每个方法加了synchronized,相当于是针对this加锁,只要针对Hashtable上的元素进行操作,就会涉及到锁冲突

ConcurrentHashMap做出了优化

  • 使用**“锁桶”的方式,来代替“一把全局锁”,有效降低锁冲突的概率,即对每个哈希桶进行加锁**

    • 如果两个线程针对两个不同的链表进行操作,是不会涉及到锁冲突的,本身操作两个不同链表上的元素,也没修改“公共变量”,本身就不涉及到线程安全问题,上述提升的收益是非常大的,一个hash表,上面的hash桶的个数是非常多的,大部分的操作都没有锁冲突了(synchronized如果不产生锁冲突,就是偏向锁)
    • 另一方面,看起来锁对象多了,实际上也不会产生更多的额外开销,Java中每个对象都可以作为锁对象,就只需要把synchronized加到链表头节点上,就可以达成上述效果
  • 哈希表中的size,即使插入的元素是不同的链表上的元素,也会涉及到多线程修改同一变量,ConcurrentHashMap引入CAS,通过CAS的方式来修改size,也就避免了加锁操作

  • ConcurrentHashMap针对扩容操作做了特殊优化——化整为零,普通的HashMap要在一次put的过程中完成整个扩容过程,就会使put操作非常卡,ConcurrentHashMap会在扩容的时候,搞两份空间

    • 一份是扩容之前的空间
    • 一份是扩容之后的空间

    每次进行hash表的基本操作,都会把一部分数据从就空间搬到新空间,不是一口气搬完,分多次搬

    搬的过程中

    1. 插入:插入到新的上面

    2. 删除:新的旧的都要删除

    3. 查找:新的旧的都要查找

Java 8之前,ConcurrentHashMap基于分段锁的方式实现,引入若干个锁对象,每个锁对象管理若干个哈希桶,Java 8之后就把这种实现方式废弃了

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

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

相关文章

CSS实现广告自动轮播

实现原理 该广告轮播功能的实现主要依靠HTML和CSS。HTML负责搭建轮播框架&#xff0c;而CSS则控制样式和动画效果。通过CSS中的关键帧动画&#xff08;Keyframes&#xff09;&#xff0c;我们可以定义图片在容器内的滚动效果&#xff0c;从而实现轮播功能。 HTML结构 首先&am…

如何搭建线下陪玩系统(本地伴游、多玩圈子)APP小程序H5多端前后端源码交付,支持二开!

一、卡顿的优化方法 1、对陪玩系统源码中流媒体传输的上行进行优化&#xff0c;通过提升推流端的设备性能配置、推流边缘CDN节点就近选择等方式解决音视频数据源流的卡顿。 2、对陪玩系统源码中音视频数据的下载链路进行优化&#xff0c;通过选择更近更优质的CDN边缘节点来减少…

Navicat导入sql文件图文教程

本文使用的MySQL工具为:Navicat.默认已经连接数据库!! 步骤: 1.右键自己的数据库,选择新建数据库. 2.输入数据库名称&#xff0c;字符集选择“utf8”&#xff0c;排序规则选择“ utf8_general_ci”,确定. 3.双击新建好的“数据库”。右键点击“运行SQL文件”。 4.选择本地的s…

linux信号相关概念

signal 信号引入什么是信号&#xff1f;如何产生信号&#xff1f;通过按键产生信号调用系统函数向进程发信号系统调用函数发送信号的流程: 由软件条件产生信号软件发送信号的流程&#xff1a; 硬件异常产生信号硬件异常的流程&#xff1a; Deliver、Pending、Block概念信号在内…

Vue 查看真实请求地址

当你在项目中配置了proxy代理&#xff0c;前端在浏览器开发调试的时候&#xff0c;是看不到真是的请求地址的。 这时候&#xff0c;后端要说话了&#xff1a;你这连的是我的地址吗&#xff1f;网络里这显示的也不对吧~ 前端: 额、不是在这里看的。既然你不相信我&#xff0c;…

wsl ubuntu18.04升级为cmake-3.15.3

wsl ubuntu18.04 默认的cmake为3.10&#xff0c;编译CMakeLists.txt经常需要高版本cmake。 升级过程如下&#xff1a; 下载cmake-3.15.3-Linux-x86_64.tar.gz wget https://cmake.org/files/v3.15/cmake-3.15.3-Linux-x86_64.tar.gz 解压文件 tar zxvf cmake-3.15.3-Linux-x86…

替代普通塑料吸头的PFA移液吸头

目前市场上的规格&#xff1a;0.01ml、0.05ml、0.1ml、0.2ml、0.5ml、1ml、2ml、5ml、10ml等均可定制加工PFA材质枪头&#xff0c;可以适配市场上大部分移液枪&#xff0c;普兰德&#xff0c;大龙&#xff0c;赛默飞&#xff0c;赛多利斯&#xff0c;力辰、吉尔森&#xff0c;瑞…

K8S哲学 - probe 探针

探针分类&#xff1a; liveness probe readiness probe startup probe Liveness Probe&#xff1a;用于检查容器是否还在运行。如果 Liveness Probe 失败&#xff0c;Kubernetes 会杀死容器&#xff0c;然后根据你的重启策略来决定是否重新启动容器。常见的做法是使用与 Readin…

error解决expression before ‘static‘

问题现象 报警如下 跳转到提示第125行&#xff0c;但是这行明显是没有问题的。 问题分析 经过排查可以看到&#xff0c;是120行的末尾\在S32DS编译器里面被认为是“接下一行”的意思&#xff0c;120行注释掉之后&#xff0c;后面的121行、122行、123行均被注释掉&#xff0c;…

2024年3月 青少年软件编程(图形化) 等级考试试卷(一级)

2024.3青少年软件编程&#xff08;图形化&#xff09; 等级考试试卷&#xff08;一级&#xff09; 一、 单选题(共 25 题&#xff0c; 共 50 分) 1.单击下列哪个按钮&#xff0c; 能够让舞台变为“全屏模式” &#xff1f; &#xff08; &#xff09; A. B. C. D. 标准答案&am…

Redis系列3:高可用之主从架构

1 主从复制介绍 上一篇《Redis系列2&#xff1a;数据持久化提高可用性》中&#xff0c;我们介绍了Redis中的数据持久化技术&#xff0c;包括 RDB快照 和 AOF日志 。有了这两个利器&#xff0c;我们再也不用担心机器宕机&#xff0c;数据丢失了。 但是持久化技术只是解决了Redi…

Redis进阶——相互关注Feed流推送

目录 关注和取消关注业务需求实现步骤效果如下 共同关注业务需求实现步骤效果如下 Feed流实现方案Feed流简介三种Timeline方式三种模式对比 推送到粉丝收件箱业务需求Feed流的滚动分页 实现分页查询收件箱业务需求具体步骤如下 关注和取消关注 业务需求 当我们进入到笔记详情…

如何用C++写一个日期计算器

目录 前言 代码的布局 设计数据 方法声明 方法的实现 获取某年某月的天数 *全缺省的构造函数 * 拷贝构造函数 *赋值运算符重载 *析构函数 日期天数 日期天数 日期-天数 日期-天数 前置 后置 后置-- 前置-- 实现比较大小运算符重载思路 >运算符重载 运算…

互联网通信原理

互联网通信原理 ISO/OSI(开放系统互连)的七层模型 注意事项 上三层是为用户提供服务的&#xff0c;下四层负责实际数据传输下四层的传输单位 传输层&#xff08;数据段&#xff09;、网络层&#xff08;数据包&#xff09;、数据链路层&#xff08;数据帧&#xff09;、物理层…

git基础教程(10) git push将本地修改推送到远端

git push 命令用于将本地分支的更新推送(上传)到远程仓库。命令的基本语法为: git push [<repository> [<refspec>...]]<refspec>的格式是<+><src>:<dst> 你可以理解成: git push <远程仓库名称> <本地分支名称>:<远…

MySQL数据库——15.连接的使用

常见的连接类型包括 INNER JOIN(内连接)、LEFT JOIN(左连接)和 RIGHT JOIN(右连接)。 INNER JOIN(内连接) INNER JOIN 也称为等值连接,它获取两个表中字段匹配关系的记录。具体用法如下: SELECT column1, column2, ... FROM table1 INNER JOIN table2 ON table1.col…

数据库系统概论(超详解!!!)第六节 触发器

数据的完整性是为了防止数据库中存在不符合语义的数据。 一种是在定义表时声明数据完整性&#xff0c;称为声明完整性。 另一种是在服务器端编写触发器来实现&#xff0c;称为过程完整性。完成比参照完整性约束和CHECK约束更复杂的数据约束。 1、触发器概述 触发器是一种特…

力扣经典150题第三十三题:最小覆盖子串

目录 解题思路与实现 - 最小覆盖子串问题描述示例解题思路算法实现复杂度分析测试与验证总结 感谢阅读&#xff01; 解题思路与实现 - 最小覆盖子串 问题描述 给定一个字符串 s 和一个字符串 t&#xff0c;返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字…

图文教程 | 2024年最新Typora激活使用教程合集

前言 汇总一下网上的三种方法。 &#x1f4e2;博客主页&#xff1a;程序源⠀-CSDN博客 &#x1f4e2;欢迎点赞&#x1f44d;收藏⭐留言&#x1f4dd;如有错误敬请指正&#xff01; 关于安装教程&#xff1a;http://t.csdnimg.cn/SCIQ8http://t.csdnimg.cn/SCIQ8自行跳转安装 一…

35. 【Android教程】视频页面:ViewPager

ViewPager 是一种可以让用户通过左右滑动来切换页面的控件&#xff0c;通过它我们可以展示超过屏幕尺寸大小的内容&#xff0c;在某种程度上它可以说是实现多页面的最佳方式&#xff0c;同时 ViewPager 还支持任意动态的添加/删除页面。比如我们可以将不同的类别的内容分别放在…