【JavaEE初阶】第二节.多线程( 进阶篇 ) 锁的优化、JUC的常用类、线程安全的集合类

news2024/9/28 19:16:05

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 

文章目录

  • 前言
  • 一、synchronized的优化操作
  •        1.1 锁膨胀/锁升级
  •        1.2 锁消除
  •        1.3 锁粗化
  • 二、JUC
  •         2.1 Callable接口
  •         2.2 ReentrantLock类(可重入锁)
  •         2.3 原子类
  •         2.4 Semaphore类(信号量)
  •         2.5 CountDownLatch类
  • 三、线程安全的集合类
  •         3.1 多线程使用顺序表
  •         3.2 多线程环境使用队列
  •         3.3 多线程环境使用哈希表


前言

这篇博客主要介绍 synchronized 的底层工作原理,包括:锁膨胀/锁升级、锁消除、锁粗化 ;并且介绍了 关于JUC的详细知识点 ;以及一些线程安全的集合类 ;


一、synchronized的优化操作 

由上一篇博客,我们可以知道,synchronized 使用的锁策略 有以下特点:

  1. 既是悲观锁,也是乐观锁(自适应)
  2. 既是轻量级锁,也是重量级锁(自适应)
  3. 轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁来实现
  4. 不是读写锁
  5. 是非公平锁
  6. 是可重入锁

1.1 锁膨胀/锁升级

现在,我们需要知 synchronized 是怎样 自适应的?这个过程,又叫做 锁膨胀/锁升级) 

synchronized 在加锁的时候要经历几个阶段:

  1. 当线程没有加锁的时候,这个阶段叫做 无锁;
  2. 当线程刚开始加锁,并没有产生竞争的时候,这个阶段叫做 偏向锁;
  3. 当线程进行加锁,并且已经产生锁竞争了,这个阶段叫做 轻量级锁;
  4. 如果此时锁竞争的更激烈了,这个阶段叫做 重量级锁;

图示解析:

当然,这几个阶段,并不是说 每一次加锁都会一直加到 最后,可能会到某一阶段之后,就会解锁了;不会每一个阶段都会经历,但会经历到某一部分 ;

像上面的,锁的状态不断的升级,锁的竞争也逐渐加剧 的情况,就叫做 锁膨胀/锁升级;

这里就先重点介绍一下 偏向锁;

所谓偏向锁,并不是 "真正加锁",而是只使用一个标记表示 "这个锁是我的了",在遇到其他线程来竞争锁之前,会一直保持着这个状态;

直到真的有线程来竞争锁了,此时才会真正的加锁;

这个过程类似于 单例模式中的 "懒汉模式" —— 在必要的时候进行加锁,从而会节省一定的开销;

因为如果没有额外的线程 来参与锁竞争的话,那么就会一直处于偏向锁的状态,也就省去了加锁和解锁的开销了;

1.2 锁消除

synchronized 除了锁升级,还有其他的优化操作,比如说:锁消除 

所谓锁消除即 编译器自动锁定,如果认为这个代码没必要加锁,就不加了!!!

代码举例:

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
//就比如说,StringBuffer 是线程安全的,它的 append 等方法,都是带有synchronized,
//如果上述代码 都只是在同一个线程里面执行的,那么此时就没有必要加锁了
//此时,JVM 就自动悄悄的把锁给去掉了(这也属于编译器优化的一个方面)

但是,锁消除的操作 不是所有的情况下都会触发,实际上,大部分情况下不能被触发;

向偏向锁 则是每一次加锁,都会进入到偏向锁的状态;

1.3 锁粗化

锁粗化,也是 synchronized 的一种优化方式 ;

在这之前,我们需要知道 锁的粒度 这个概念;

所谓 锁的粒度,指的是 synchronized 包含的代码范围是大还是小;

范围越大,粒度越粗 ;范围越小,粒度越细!!!

锁的粒度越细,能够更好的提高线程的并发,但是也会增加 "加锁解锁" 的次数 ;

 锁粗化,也是类似的情况:它把一段代码中,频繁的加锁解锁,"粗化" 成一次加锁解锁了 ;

图示分析:

 毕竟,加锁解锁 也是需要开销的嘛 

 二、JUC

所谓 JUC,实际上指的是:java.util.concurrent,它是一个包;

在这个包里面 存放了很多和多线程开发 的相关的类、接口;

2.1 Callable接口

Callable接口 和 前面的Runnable 非常相似,都是可以在创建线程的时候,来指定一个 "具体的任务" ;

区别是:Callable 指定的任务是带返回值的,Runnable 指定的任务是不带返回值的;

Callable 提供的 call方法,可以方便的获取到代码的执行结果  ;
 

如:创建线程计算 1+2+3+ ... +1000,使用Callable版本 ;

代码示例:

package thread;
 
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class Demo29 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };
 
        //套上一层,目的是为了获取到后续的结果
        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread t = new Thread(task);
        t.start();
 
        //在 线程t 执行结果之前,get会阻塞,
        //直到 线程t 执行完了,结果算好了,get 才能返回,返回值就是 call方法 return 的内容
        //获取执行结果
        System.out.println(task.get());
    }
}

运行结果:

需要重点理解的是 下面一部分代码: 

//给 计算结果 一个小票
FutureTask<Integer> task = new FutureTask<>(callable);

在线程需要计算结果的情况下,需要非常明确的知道 这个结果是谁来算的,不能像以前一样,直接把 callable 加到 Thread 的构造方法之中(之前使用 Runnable 的时候,不需要知道一个明确的结果):

就类似于,去餐馆吃饭的时候,人多的时候,老板会给一个小票,到时候就可以凭着小票来取餐,不至于搞乱了 ;

2.2 ReentrantLock类(可重入锁)

我们已经知道,synchronized 已经是一个可重入锁了,但是 为啥还要再搞一个 ReentrantLock 呢? 

实际上,ReentrantLock 还是和 synchronized 之间有很大的差别的:

1.synchronized 是一个单纯的关键字,以代码块为单位进行加锁解锁;ReentrantLock 则是一个类,提供 lock方法 加锁,unlock方法 解锁;

 2.ReentrantLock 在构造实例的时候,可以指定一个 fair参数 来决定锁对象是 公平锁还是非公平锁;synchronized 加的锁只能是非公平锁,不能指定为公平锁

3.ReentrantLock 还提供了一个特殊的加锁操作 —— tryLock() 方法;

4.ReentrantLock 提供了更加强大的 等待/唤醒 机制; 

说明:大部分情况下,使用锁还是以 synchronized 为主,特殊场景下,才使用 ReentrantLock 

2.3 原子类

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

原子类有以下几个:

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicInterArray
  4. AtomicLong
  5. AtomicReference
  6. AtomicStampedReference

举例说明:

以 AtomicInteger 为例,常见方法有:

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

2.4 Semaphore类(信号量) 

所谓信号量,可以理解成 计数器,描述了可用资源的个数;

举例说明:

比如说,停车场的入口处,会有牌子(上面显示:当前有车位 N 个);

当有车开进去以后,N 就会减去一个;当有车从出口开出去以后,N 就会加上一个 ;

这个 N 就叫做 信号量 ;

如果当前 N 已经是 0 了,如果还有车想进来,那就进不去了,新来的车只能阻塞等待,直到有车给开出去 ;

如上所示:

把车开进去,称之为 申请一个可用资源,信号量 -= 1,称为 P 操作 ;

把车开出来,称之为 释放一个可用资源,信号量 +=1,称为 V 操作 ;

其实,信号量 可以把它视为一个 更广义的锁,当信号量的取值是 0~1 的时候,就退化成了一个普通的锁 ;

相当于是一个可用资源是 1 的信号量,只要加锁成功,其他的线程就获取不了 ;

但是,信号量不一样,只要可用资源还有,就可以不断的进行 P 操作 ;

说明:我们需要知道,上面的信号量 +=1,-=1 都是原子的;


代码演示:

package thread;
 
import java.util.concurrent.Semaphore;
 
public class Demo31 {
    public static void main(String[] args) throws InterruptedException {
        //构造方法传入有效资源的个数是 3 个
        Semaphore semaphore = new Semaphore(3);
        //P操作 申请资源,使用 acquire方法
        //每一次使用一个资源,信号量 -1
        semaphore.acquire();
        System.out.println("申请资源");
        semaphore.acquire();
        System.out.println("申请资源");
        semaphore.acquire();
        System.out.println("申请资源");
        semaphore.acquire();
        System.out.println("申请资源");
    }
}

 运行结果:

有效资源是 3 份,即使 想要申请4份资源,但是在运行结果中显示,最终只是申请了 3 份资源 ;

只有前面的线程把资源释放了,才可以使用 ;

代码示例:

package thread;
 
import java.util.concurrent.Semaphore;
 
public class Demo31 {
    public static void main(String[] args) throws InterruptedException {
        //构造方法传入有效资源的个数是 3 个
        Semaphore semaphore = new Semaphore(3);
        //P操作 申请资源,使用 acquire方法
        //每一次使用一个资源,信号量 -1
        semaphore.acquire();
        System.out.println("申请资源");
        semaphore.acquire();
        System.out.println("申请资源");
        semaphore.acquire();
        System.out.println("申请资源");
        //V操作 释放资源,使用 release 方法
        //此时,信号量 = 0,线程进入阻塞,只有释放资源,线程才可以继续运行
        semaphore.release();
        System.out.println("释放资源成功");
        semaphore.acquire();
        System.out.println("申请资源成功");
        
    }
}

运行结果:

2.5 CountDownLatch类 

CountDownLatch类,相当于 在一个大的任务被拆分成若干个子任务的时候,用这个来衡量 什么时候这些子任务都执行结束 ;

举个例子:

此时在某地正在进行一场跑步比赛,只有当所以选手都到达终点的时候,裁判才可以吹哨结束比赛 ;

CountDownLatch 描述的就是啥时候所有选手都到达终点 ;

代码示例:

package thread;
 
import java.util.concurrent.CountDownLatch;
 
public class Demo32 {
    public static void main(String[] args) throws InterruptedException {
        //模拟跑步比赛
        //构造方法中设定有 10 个选手参赛
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(() -> {
                try {
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName()+" 到达终点");
                    //countDown 相当于 "撞线"
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
        //await 再等待所有的线程都 "撞线"
        //换句话说,调用 countDown 的次数达到初始化的时候 设定的值,
        //await 就返回,否则 await 就阻塞等待
        latch.await();
 
        System.out.println("比赛结束");
    }
}

运行结果:

 三、线程安全的集合类

原来的集合类,大部分都是 线程不安全的 ;

换句话来说,大部分集合类在多线程环境下使用 都是有问题的 ;

总结:

ArrayList、LinkedList、TreeSet、TreeMap、HashSet、HashMap、Queue 是线程不安全的;

Vector、Stack、HashTable 是线程安全的 ;

3.1 多线程使用顺序表 

(一)自己使用加锁操作

(二)使用 Collections.synchronizedList (new ArrayList);

synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List ;

synchronizedList 的关键操作上都带有 synchronized ;

(三)使用 CopyOnWriteArrayList;

如果出现修改操作,就把 Ar rayList 进行复制 ;

先拷贝一份数据(创建一份副本),再接着新线程修改副本,修改完成后,再用副本替换原有的数据 ;

这就叫做 写实拷贝 ;

不过这种行为的 拷贝成本可能会很高,所以 一般元素个数多的时候会用到 ;

3.2 多线程环境使用队列 

多线程环境下常常使用以下阻塞队列:

  1. ArrayBlockingQueue 基于数组实现的阻塞队列;
  2. LinkedBlockingQueue 基于链表实现的阻塞队列;
  3. PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列;
  4. TransferQueue 最多只包含一个元素的阻塞队列;

3.3 多线程环境使用哈希表 

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

  1. HashTable
  2. ConcurrentHashMap

HashTable


其中,HashTable 直接搞了一个大锁,这就导致了 在多个线程去修改上面的元素 的时候,都需要去进行加锁控制,就会有锁冲突,比较低效: 

对于两个不同的哈希桶上的元素,不牵扯修改同一个变量,就不会发生线程安全问题(此时加锁的话,意义不大);

但是,如果两个修改涉及到同一个哈希桶上,就会有线程安全问题 ;

于是,HashTable 就会显得低效 ;

ConcurrentHashMap

相比之下,ConcurrentHashMap类 做出了重大的改进 —— 把锁的粒度细化了!!!

接下来介绍的 ConcurrentHashMap类 基于 java 1.8 的 ;

该类是基于哈希表中的每一个链表对象进行加锁,线程需要对哪个链表对象进行操作,就在哪里加锁;

由于哈希表中链表数量很多,链表对象的元素个数较少,可以有效地降低锁竞争的概率 ;

 ConcurrentHashMap 优化特点:

[ 最重要 ] 把锁的粒度细化,降低锁冲突的概率;
有一个激进的操作:读没加锁,写才加锁;
更充分的使用了 CAS 特性,达到一个更高效的操作(如 维护 size 的时候);
针对扩容场景进行了优化(化整为零,不会一口气的完成扩容;而是 每次基本操作,都扩容一点点,逐渐完成整个扩容 —— 不会特别卡); 

总结

今天的关于多线程( 进阶篇 ) 锁的优化、JUC的常用类、线程安全的集合类就讲到这里,我们下一节内容再见!!!!

 

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

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

相关文章

Python获取中国大学MOOC某课程评论及其参与人数

文章目录前言一、需求二、分析三、运行结果前言 本系列文章来源于真实的需求本系列文章你来提我来做本系列文章仅供学习参考 一、需求 1、课程参加人数 2、课程学员名称及其评论 二、分析 首先查看网页源代码是否有需要的数据 课程参加人数 课程学员名称及其评论 F12 打开浏…

Linux中断处理

目录 一、什么是中断 二、中断处理原理 三、中断接口 3.1 中断申请 3.2 中断释放 3.3 中断处理函数原型 四、按键驱动 一、什么是中断 一种硬件上的通知机制&#xff0c;用来通知CPU发生了某种需要立即处理的事件 分为&#xff1a; 1. 内部中断 CPU执行程序的过程中&am…

力扣sql简单篇练习(二十)

力扣sql简单篇练习(二十) 1 广告效果 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 SELECT ad_id,IFNULL(ROUND(sum(IF(actionClicked,action,0))/sum(IF(actionIgnored,0,1))*100,2),0.00) ctr FROM Ads GROUP BY ad_id ORDER BY ctr desc,ad_id …

消息队列MQ介绍

消息队列技术是分布式应用间交换信息的一种技术。消息队列可驻留在内存或磁盘上,队列存储消息直到它们被应用程序读走。通过消息队列&#xff0c;应用程序可独立地执行--它们不需要知道彼此的位置、或在继续执行前不需要等待接收程序接收此消息。 消息中间件概述 消息队列技术是…

Wi-Fi 7技术揭秘

引言 2022年4月7日&#xff0c;紫光股份旗下新华三集团全球首发企业级智原生Wi-Fi 7 AP新品 WA7638和WA7338。仅在同年的6月15日&#xff0c;在东京举行的第29届日本网络通信展览会&#xff08;Interop Tokyo 2022&#xff0c;简称Interop展&#xff09;中&#xff0c;WA7638就…

Java - 数据结构,栈

一、栈 1.1、什么是栈 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈 顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO&#xff08;Last In First Out&#xff09;的原则。 压…

关于《How to Learn to Code Get a Developer Job in 2023》的经验学习

1. Who is This Book For ? for Anyone who is considering a career in software development. 2. Can Anyone Learn to Code? Any sufficiently motivated person can learn to code. 3.Executive Summary Learning to code is hard.Getting a job as a software devel…

01-基于SOA架构someip 开发-Linux开发环境搭建

前言&#xff1a;SOME/IP 是一个汽车的中间件解决方案&#xff0c;可用于控制消息。从一开始&#xff0c;它的设计就是为了完美地适应不同尺寸和不同操作系统的设备。这包括小型设备&#xff0c;如相机、AUTOSAR设备&#xff0c;以及头部单元或远程信息处理设备。同时还确保了S…

华为OD机试题,用 Java 解【VLAN 资源池】问题

最近更新的博客 华为OD机试 - 猴子爬山 | 机试题算法思路 【2023】华为OD机试 - 分糖果(Java) | 机试题算法思路 【2023】华为OD机试 - 非严格递增连续数字序列 | 机试题算法思路 【2023】华为OD机试 - 消消乐游戏(Java) | 机试题算法思路 【2023】华为OD机试 - 组成最大数…

脑洞|ChatGPT加持下,ChatOps将如何革新团队协作与运维管理?

要说近期科技圈 “顶流”&#xff0c;非 ChatGPT 莫属。 比起目前常见的语音助手与聊天 bot&#xff0c;这位机器人显得更有 “人味儿”&#xff0c;不仅能模拟人类的语气&#xff0c;跟你聊得有来有回&#xff0c;还能写剧本、编音乐、写代码。 说到聊天工具&#xff0c;就让…

低代码开发可以解决哪些问题?

低代码开发可以解决哪些问题&#xff1f;如果用4句话去归纳&#xff0c;低代码开发可以解决以下问题—— 为企业提供更高的灵活性&#xff0c;用户可以突破代码的限制自主开发业务应用&#xff1b;通过减少对专业软件开发人员的依赖&#xff0c;公司可以快速响应市场上的新业务…

完全背包—动态规划

一、背包问题概述 如图&#xff0c;完全背包与01背包的区别只有一点&#xff1a;01背包中每个物品只能取一个而完全背包中每个物品可以取无数个。解决完全背包问题必须首先弄明白01背包&#xff0c;不清楚的可以看我的这篇文章01背包—动态规划。 二、例题 重量价值物品0115物…

Jenkins+docker发布Springbot服务

1.开发Springbot应用 新建多个环境的配置文件 bootstrap.yaml 通过变量获取不同环境active bootstrap-dev.yml bootstrap-pre.yaml 预发布及生产环境配置文件走nacos 二.配置docker 新增Dockerfile文件 Dockerfile内容 # Docker image for springboot file run # VERSION…

代码名命规范浅析

日常开发编码中&#xff0c;代码的名命是个大学问&#xff0c;能快速的看懂开源代码的结构和意图&#xff0c;也是一项必备的能力。在java项目的代码结构中&#xff0c;采用长名命的方式来规范类的名命&#xff0c;能够自己表达其主要意图&#xff0c;配合高级IDE&#xff0c;可…

万字长文带你实战 Elasticsearch 搜索

ES 高级实战 前言 上篇我们讲到了 Elasticsearch 全文检索的原理《别只会搜日志了,求你懂点原理吧》,通过在本地搭建一套 ES 服务,以多个案例来分析了 ES 的原理以及基础使用。这次我们来讲下 Spring Boot 中如何整合 ES,以及如何在 Spring Cloud 微服务项目中使用 ES 来…

考研复试机试 | C++ | 王道机试课程笔记

目录Zero-complexity (上交复试题)题目&#xff1a;代码&#xff1a;括号匹配问题题目&#xff1a;代码&#xff1a;表达式解析问题 &#xff08;浙大机试题&#xff09;题目&#xff1a;代码&#xff1a;标准库里提供了栈 stack<typename> myStack .size() 栈的大小 .pu…

互斥信号+任务临界创建+任务锁

普通信号量 1、信号量概念 2、创建信号量函数 3、互斥信号量 创建互斥信号量函数 等待信号量函数 释放互斥信号量 4、创建任务临界区 5、任务锁 任务上锁函数 ​编辑 任务结束函数 效果 普通信号量 1、信号量概念 信号量像是一种上锁机制&#xff0c;代码必须获…

Java性能-回收算法-Throughout回收算法

垃圾回收算法 理解Throughput回收器 回收器三个基本操作——回收 找到不使用的对象 释放内存 压缩堆碎片 Minor GC和Full GC&#xff0c;每个操作都会标记&#xff0c;释放和压缩对应的目标分代 [63.205s][info][gc,start ] GC(13) Pause Full (Ergonomics) [63.205s][info][…

Odoo丨Odoo框架源码研读三:异常处理与定制化开发

Odoo丨Odoo框架源码研读三&#xff1a;异常处理与定制化开发 Odoo源码研读的第三期内容&#xff1a;异常处理与定制化开发。 *异常处理* Odoo中的Exception是对Python内置异常做了继承和封装&#xff0c;设定了自己核心的几个Exception。 而对异常的处理和Python内置异常的…

【趋势分析方法三】MATLAB代码实现TFPW-MK检验

目前水文时间序列趋势分析的方法很多&#xff0c;主要分为参数检验和非参数检验两大类&#xff1a; 参数检验中常用的有线性回归法、滑动平均法、累积距平法等非参数检验则主要包括Mann-Kendal&#xff08;MK&#xff09;法和 Spearman 秩次相关法等 虽然从理论上讲&#xff…