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

news/2024/10/16 2:22:27/

目录

  • 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方法带有返回值,表示这个线程执行结束会得到什么结果

java">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就是为了更优雅的解决上述问题

java">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:可重入

java">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)
java">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两个取值,此时就称为”二元信号量“,本质就是一把锁

java">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

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

java">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.ppmy.cn/news/1426400.html

相关文章

Jenkins机器已经安装了ansible, 运行的时候却报错ansible: command not found

操作系统&#xff1a;MacOS Jenkins log提示 ansible: command not found 直接在Jenkins 机器中&#xff0c;进入一样的目录执行ansible --version OK 原因&#xff1a; Jenkins 默认使用的环境是 /usr/bin, 而我的ansible 安装配置在conda3 下面&#xff0c;所以需要在Jenkin…

【QT进阶】Qt Web混合编程之html、 js的简单交互

往期回顾 【QT进阶】Qt Web混合编程之VS2019 CEF的编译与使用&#xff08;图文并茂超详细介绍&#xff09;-CSDN博客【QT进阶】Qt Web混合编程之QWebEngineView基本用法-CSDN博客【QT进阶】Qt Web混合编程之CMake VS2019编译并使用QCefView&#xff08;图文并茂超详细版本&…

网页SAP导出Excel报表提示“执行xls文件不存在”的解决方法

问题截图 解决办法一&#xff1a;降级谷歌浏览器版本在69-87之间&#xff0c;再次打开访问即可顺利导出网页sap报表。 解决方法二&#xff1a; 安装office或者wps软件&#xff0c;安装并配置相应sap客户端系统&#xff0c;如果在外网办公&#xff0c;还需要安装公司VPN客户端…

【快捷部署】020_Jenkins(2.60.3)

&#x1f4e3;【快捷部署系列】020期信息 编号选型版本操作系统部署形式部署模式复检时间020Jenkins2.60.3Ubuntu 20.04Docker单机2024-04-17 一、快捷部署 #!/bin/bash ################################################################################# # 作者&#xff1…

缓存之SpringCache整合redis

Spring Cache 是 Spring 框架提供的一种缓存抽象&#xff0c;在应用中可以方便地使用它来进行方法级别的缓存。结合 Redis 作为缓存存储后端&#xff0c;可以实现高性能的缓存服务。下面是一个简单的示例&#xff0c;演示了如何在 Spring Boot 项目中整合 Spring Cache 和 Redi…

人工智能大模型培训老师叶梓:探索AI的边界:本地化、定制化与隐私保护

随着人工智能技术的不断进步&#xff0c;AI商业智能工具正变得越来越重要。它们不仅提高了企业的决策效率&#xff0c;还为企业提供了前所未有的数据洞察力。在众多新兴工具中&#xff0c;AnythingLLM、LocalAI、VectorAdmin等平台以其独特的功能和优势&#xff0c;引领着AI商业…

NLP基础—jieba分词

jieba分词 支持四种分词模式 精确模式 试图将句子最精确地切开,适合文本分析;全模式 把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义;搜索引擎模式 在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。paddle模式 利用Paddle…

java+idea+mysql采用医疗AI自然语言处理技术的3D智能导诊导系统源码

javaideamysql采用医疗AI自然语言处理技术的3D智能导诊导系统源码 随着人工智能技术的快速发展&#xff0c;语音识别与自然语言理解技术的成熟应用&#xff0c;基于人工智能的智能导诊导医逐渐出现在患者的生活视角中&#xff0c;智能导诊系统应用到医院就医场景中&#xff0c…