【JavaEE初阶】多线程7(面试要点)

news2024/9/22 22:09:42

欢迎关注个人主页:逸狼


创造不易,可以点点赞吗~

如有错误,欢迎指出~



目录

常见的锁策略

乐观锁vs悲观锁

重量级锁vs轻量级锁

挂起等待锁vs自旋锁

公平锁vs非公平锁

可重入锁vs不可重入锁

读写锁

synchronized的加锁过程

锁升级的过程

偏向锁

锁消除(编译器的优化策略)

锁粗化(编译器的优化策略)

CAS

 CAS具体的使用场景

通过CAS实现的原子类

CAS能保证线程安全

ABA问题

ABA问题 举例

引入版本号解决上述问题

Callable 接口

使用举例

 ReentrantLock可重入锁

ReentrantLock与synchronized区别

信号量Semaphore

CountDownLatch

 解决线程不安全问题

CopyOnWriteArrayList集合类

写时拷贝

写时拷贝的缺点

Hashtabel

ConcurrentHashMap


常见的锁策略

锁 是一个非常广义的话题,synchronized只是市面上其中最典型一种锁的实现(Java内置的,推荐使用的锁)

乐观锁vs悲观锁

  • 乐观锁:加锁时,假设出现锁冲突的概率不大 =>接下来围绕加锁要做的工作,就会更少
  • 悲观锁:加锁时,假设出现锁冲突的概率很大 =>接下来围绕加锁要做的工作,就会更

synchronized这把锁 属于'自适应'锁

使用synchronized,初始情况下 是乐观的(预估接下来锁冲突概率不大),同时会在背后统计锁冲突了多少次,如果发现冲突的次数达到一定程度了,就会转变为 悲观的

重量级锁vs轻量级锁

效果和 乐观悲观是重叠的(乐观悲观 是站在"预估锁冲突"角度,重量轻量 站在 加锁的开销 角度)

  • 重量级锁:加锁的开销比较大,要做更多的工作.
  • 轻量级锁:加锁的开销比较,要做的工作相对更少..

synchronized也是自适应的

挂起等待锁vs自旋锁

挂起等待锁:悲观锁/重量级锁的一种典型实现

自旋锁: 乐观锁/轻量级锁的一种典型实现

自旋锁使用 '忙等' 的策略:等待的过程中,不会释放cpu资源(不停的检测锁 是否被释放,一旦锁被释放了,就立即有机会能够获取到锁了)

挂起等待锁则"让出了cpu资源"(cpu就可以用来做别的事情了)

synchronized是 自适应的

公平锁vs非公平锁

在计算机中,约定了"先来后到"为公平

synchronized属于非公平锁(概率相等)

  • 当N个线程竞争同一个锁.其中一个线程先拿到锁了.后续该线程释放锁之后,剩下的N-1个线程,就是要重新竞争,谁拿到锁,就都不一定了(当然,也不能保证这些线程竞争中获取锁的概率一定是数学上的严格均等)
  • 本身操作系统内核里针对锁的处理就是如此,synchronized在系统内核的基础上,没有做啥额外的工作

如果需要 使用公平锁,就需要做额外的操作(比如引入队列,记录每个线程加锁的顺序)
 

可重入锁vs不可重入锁

针对死锁问题: 如果一个线程,针对一把锁,连续加锁两次,就可能出现死锁.如果把锁设定成"可重入"就可以避免死锁了.

可重入锁的原理:

  • 记录当前是哪个线程持有了这把锁
  • 在加锁的时候判定,当前申请锁的线程,是否就是锁的持有者线程
  • 计数器,记录加锁的次数.从而确定何时真正释放锁.
     

读写锁

读写锁,本身也是系统内置的锁

读写锁把加锁操作 分为两种情况:读加锁 和 写加锁,读写锁提供了两种加锁的api :加读锁,加写锁,他们解锁的api是相同的

  • 如果两个线程,都是按照读方式加锁,此时不会产生锁冲突
  • 如果两个线程,都是加写锁,此时产生锁冲突
  • 如果一个线程读锁,一个是写锁,也会产生锁冲突

如果多个线程同时读这个变量,没有线程安全问题,但是一个线程 且一个线程或者两个线程都写 就会产生问题(大部分场景,读操作的频次操作 要高)

synchronized不是读写锁

synchronized的加锁过程

代码执行到synchronized的代码块时,jvm大概要做哪些事情?

锁升级的过程

synchronized加锁时会经历:无锁=> 偏向锁=>轻量级锁=> 重量级锁

偏向锁

偏向锁不是真的加锁(真的加锁,开销比较大),只是做了个标记(标记的过程,非常轻量高效)

偏向锁 本质上是推迟了 加锁的时机

对于当前JVM的实现来说,上述锁升级的过程,属于"不可逆"

锁消除(编译器的优化策略)

编译器会对你写的synchronized代码做出判定,判定是否需要真的加锁,如果这里没必要加锁,就能够自动把synchronized给干掉.

锁粗化(编译器的优化策略)

锁的粒度:在synchronized的{}里,代码越多,"粒度越粗";代码越少,"粒度越细"

锁粗化就是把多个"细粒度"的锁,合并为"粗粒度"的锁

CAS

CAS全称Compare and swap

一个内存的数据和两个cpu寄存器中的数据进行操作(寄存器1和寄存器2),比较内存 和 寄存器1中的内容,如果发现相同,就交换内存和cpu寄存器2的内容.(一般只关心 内存交换后的内容(这里的交换希望达到的目的是"赋值")),如果不同,无事发生

 CAS具体的使用场景

基于CAS实现"原子类"

int/long 在进行++,--的时候,都不是原子的

基于CAS实现的原子类,对int/long等这些类型进行了封装,从而可以原子的完成++,--等操作

原子类,在Java标准库中也有现成的实现

通过CAS实现的原子类

实际开放中,一般很少直接使用CAS,都是使用现成的操作

CAS能保证线程安全

CAS之所以能保证线程安全,是因为在通过CAS比较的过程中,确认了当前是否有其他线程插入进来执行

ABA问题

value=A,oldvalue=A,value可能被其他线程修改成了B,又被另一个线程修改回了A,是value值从A到B再到A过程,value依然等于oldvalue,所以在CAS判断下,会进行交换操作.

CAS中确实存在ABA问题,但是大多情况下ABA问题不会带来bug

ABA问题 举例

使用CAS逻辑进行转账操作(极端的例子)

引入版本号解决上述问题

版本号是一个"整数"(不一定是"次数",也可以是"时间"),只能增加,不能减

Callable 接口

  • callable接口  -> call方法=>带有返回值
  • Runnable    -> run方法=> void

使用举例

public class Demo33 {

    private static int result;

    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
           int sum=0;
            for (int i = 0; i < 1000; i++) {
                sum+=i;
            }
            result=sum;
        });
        t.start();
        t.join();

        System.out.println("result= "+result);
    }
}

使用举例2 


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo36 {
    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 = 0; i < 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();

        // 后续需要通过 FutureTask 拿到最终的结果.
        System.out.println(futureTask.get());
    }
}

 ReentrantLock可重入锁

ReentrantLock属于经典风格的锁,是通过lock和unlock方法完成加锁解锁的

实际开发中,大多情况下使用synchronized即可(synchronized只是Java提供的其中一种加锁的方式)

ReentrantLock与synchronized区别

  • synchronized属于关键字,底层是通过JVM的c++代码实现的
  • ReentrantLock则是标准库提供的类,通过Java代码实现的
  • synchronized通过代码块控制加锁解锁
  • ReentrantLock通过调用lock ,unlock方法来完成,(unlock可能会忘记=>将unlock放到finally中)
  • ReentrantLock提供了tryLock这样的加锁风格,tryLock在加锁失败时,不会阻塞,会直接返回,通过返回值来反馈是加锁成功还是失败(前面介绍的加锁,都是发现锁被别人占用了,就阻塞等待)
  • ReentrantLock还提供了公平锁的实现(默认是非公平的,可以在构造方法中 传入参数,设定成公平的)
  • ReentrantLock还提供了功能更强的"等待通知机制",基于Condition类,能力要比wait ,notify更强一些

信号量Semaphore

信号量 是一个"计算器",通过计数器衡量"可用资源"个数,操作系统本身提供了 信号量的实现,JVM把操作系统的 信号量封装了一下

例如

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;

public class Demo37 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock locker = new ReentrantLock(true);
        Semaphore semaphore = new Semaphore(1);
//值为1的信号量 就相当于"锁"

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
//                locker.lock();
                try {
                    semaphore.acquire();
                    count++;
                    semaphore.release();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
//                locker.unlock();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
//                locker.lock();
                try {
                    semaphore.acquire();
                    count++;
                    semaphore.release();
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
//                locker.unlock();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

CountDownLatch

很多时候需要把一个大任务拆成多个小任务,通过多线程/线程池执行 ,借助CountDownLatch就能衡量出当前任务是否整体执行结束

比如多线程下载,通过多线程下载提高下载速度,多个线程每个线程下载一部分,所有线程下载完毕在进行拼装

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo39 {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(4);

        // 构造方法的数字, 就是拆分出来的任务个数.
        CountDownLatch countDownLatch = new CountDownLatch(20);

        for (int i = 0; i < 20; i++) {
            int id = i;
            executorService.submit(() -> {
                System.out.println("下载任务 " + id + " 开始执行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("下载任务 " + id + " 结束执行");
                // 完毕 over!!!
                countDownLatch.countDown();
            });
        } // end for

        // 当 countDownLatch 收到了 20 个 "完成" , 所有的任务就都完成了.
        // await => all wait
        // await 这个词也是计算机术语. 在 python / js 意思是 async wait (异步等待)
        countDownLatch.await();

        System.out.println("所有任务都完成");
    }
}

 解决线程不安全问题

ArrayList Queue HashMap...都是线程不安全的,对于Vector Stack Hashtable(内置了synchronized)等线程安全的来说实际上又不建议使用

解决方案

1.自己加锁

 2.如果需要使用ArrayList/LinkedList 这样的结构,标准库中提供了一个带锁的List

CopyOnWriteArrayList集合类

这个集合类没有加锁,通过"写时拷贝"来实现线程安全

第三个解决方案就是通过写时拷贝,来避免两个线程同时修改一个变量

写时拷贝

  • 如果只是读取,ArrayList不需要进行任何改变
  • 如果有其他线程 修改ArrayList上面的元素,此时不会进行修改,而是拷贝一份新的ArrayList
    • 拷贝过程中,操作 都仍然读取旧版本的内容
    • 操作,则是在版本的内容上修改

如果修改操作直接基于旧版本来修改,同时还有其他线程去读,就容易读到"修改一半的数据"

ArrayList有的修改是原子的,也有一些修改不是原子的,比如"插入/删除操作

写时拷贝的缺点

  • 无法应对多个线程同时修改的情况
  • 如果涉及到的数据量很大,拷贝起来就非常慢 

3..想多线程环境下使用队列,用BlockingQueue

4.多线程环境下使用哈希表,Hashtable虽然是可选项,但是推荐使用ConcurrentHashMap,这个数据结构相对于HashMap和Hashtable来说,改进力度比较大

Hashtabel

  • Hashtable的加锁,就是直接给put ,get等方法上加上synchronized(就是给this加锁), 整个哈希表 对象就是一把锁,任何一个针对这个哈希表的操作,都会发生锁竞争

ConcurrentHashMap

  • ConcurrentHashMap是给hash表中每个"链表"进行加锁(不是一把锁,而是多把锁),这个方式大大降低了锁冲突的概率,只有进行的两次修改,恰好在修改同一个链表上的元素时,才会触发锁竞争
  • ConcurrentHashMap引入了CAS原子操作,针对像 修改size这样的操作,直接借助CAS完成,并不会加锁
  • 针对读操作,做了特殊的处理,通过volatile以及一些精巧的代码实现,确保读操作不会读到"修改一半的数据"
  • 针对hash表的扩容,做了特殊的优化. 普通hash表扩容,需要创建新的hash表,把元素都搬运过去,这一系列操作,很可能就在一次put就完成了,就会使这次put开销非常大,耗时非常长.ConcurrentMap进行了"化整为零",不会在一次操作中 进行所有数据搬运,而是只搬一部分. 此时后续的每次操作,都会触发一部分key的搬运,最终把所有的key 都搬运完成

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

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

相关文章

博途TIA v18下载时,需要重启才能安装下载路径是灰色改不了

一、需要重启才能安装 删除下面注册表P开头的文件&#xff1a; 二、下载路径是灰色改不了 注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion里找到C:\Program Files或者C:\Program Files&#xff08;x86&#xff09;&#xff0c;具体哪个看安装的时候对应…

【面向对象】设计原则

单一职责&#xff1a;低耦合&#xff0c;高内聚。一个类不要负责太多的功能&#xff0c;否则会导致类内部耦合度高&#xff0c;不利于扩展&#xff1b;开闭原则&#xff1a;对扩展开放&#xff0c;对修改关闭。扩展新功能时&#xff0c;不建议修改原有的代码&#xff0c;建议通…

【CSS】样式

文本color 颜色font-size 大小font-family 字体font-style 样式font-weight 加粗text-decoration 下划线text-shadow 阴影text-transform 大小写变换text-indent 缩进text-align 水平对齐 、vertical-align垂直对齐text-overflow 溢出word-wrap 换行 、word-break 截断 、white…

GAMES101(15节)

Irradiance辐射度量学 辐射度量学在渲染领域&#xff0c;可以帮助理解基于物理的光照模型 radiant energy辐射能量Q&#xff0c;累计总能量&#xff08;单位J joule焦耳&#xff09;&#xff0c;就像太阳能板&#xff0c;光照时间越长接收能量越多&#xff0c;收到的能量总和…

Vue点击按钮生成pdf文件/Vue点击按钮生成png图片

本次案例是vue的点击生成pdf文件和png格式的图片 一、生成pdf文件案例 看代码之前&#xff0c;我们肯定得需要看看&#xff0c;效果图是什么的啦&#xff0c;这样子才能先看看自己想要实现的效果是不是这样子的&#xff01;上效果图嘿嘿嘿~ A、实现的效果图 这是页面&#…

【M-LOAM学习】

M-LOAM(INITIALIZATION) Article Analysis Scan-Based Motion Estimation 通过在consecutive frame (each LiDAR)&#xff08;因为omp parallel&#xff09;中寻找correspondences然后通过最小化所有考虑feature之间residual error的transformation between frame to frame 针…

java(3)数组的定义与使用

目录 1.前言 2.正文 2.1数组的概念 2.2数组的创建与初始化 2.2.1数组的创建 2.2.1数组的静态初始化 2.2.2数组的动态初始化 2.3数组是引用类型 2.3.1引用类型与基本类型区别 2.3.2认识NULL 2.4二维数组 2.5数组的基本运用 2.5.1数组的遍历 2.5.2数组转字符串 2.…

ETCD学习使用

一、介绍 etcd&#xff08;分布式键值存储&#xff09;是一个开源的分布式系统工具&#xff0c;用于可靠地存储和提供键值对数据。etcd 通常通过 HTTP 或 gRPC 提供 API&#xff0c;允许应用程序通过简单的接口与其交互。由于其可靠性和稳定性&#xff0c;etcd 在构建可扩展、分…

基于springboot的在线视频点播系统

文未可获取一份本项目的java源码和数据库参考。 国外研究现状&#xff1a; 与传统媒体不同的是&#xff0c;新媒体在理念和应用上都采用了新颖的媒介或媒体。新媒体是指应用在数字技术、在传统媒体基础上改造、或者更新换代而来的媒介或媒体。新兴媒体与传统媒体在理念和应用…

UML——统一建模语言

序言&#xff1a; 是统一建模语言的简称&#xff0c;它是一种由一整套图表组成的标准化建模语言。UML用于帮助系统开发人员阐明&#xff0c;展示&#xff0c;构建和记录软件系统的产出。UML代表了一系列在大型而复杂系统建模中被证明是成功的做法&#xff0c;是开发面向对象软件…

ModbusTCP通讯错误的排查

Modbus是一种由MODICON公司开发的工业现场总线协议标准&#xff0c;是一项应用层报文传输协议。该协议用于传输数字和模拟变量[1]。有关该协议的报文具体格式&#xff0c;以及一些基本概念&#xff0c;见[1]。 本文以一个例子&#xff0c;阐述当ModbusTCP通讯出现错误的时候&a…

文件上传、重定向、Gin路由

文件上传 单个文件上传 index.html 文件上传前端页面代码&#xff1a; <!DOCTYPE html> <html lang"zh-CN"> <head><title>index</title> </head> <body> <form action"/upload" method"post"…

MySQL学习(索引)

文章目录 基本概念单列索引普通索引&#xff08;index&#xff09;唯一索引&#xff08;unique&#xff09;主键索引 组合索引全文索引&#xff08;fulltext&#xff09;空间索引&#xff08;spatial&#xff09;MySQL存储引擎 基本概念 通过某种算法&#xff0c;构建数据模型&…

云手机的海外原生IP有什么用?

在全球数字化进程不断加快的背景下&#xff0c;企业对网络的依赖程度日益加深。云手机作为一项创新的工具&#xff0c;正逐步成为企业优化网络结构和全球业务拓展的必备。尤其是云手机所具备的海外原生IP功能&#xff0c;为企业进入国际市场提供了独特的竞争优势。 什么是海外原…

高等数学——微分学

1. 一元函数微分学 1.1. 导数概念 1.2. 导数运算 1.3. 导数与几何 2. 多元函数微分学 2.1. 多元函数的极限 2.1.1. 计算 直接代入法 无穷小乘有界 有理化型 等价无穷小型 ……总结 2.1.2. 是否存在 考试中,判断极限是否存在的问题,答案一般都是不存在。因为,证明一个…

视频怎么剪切掉一部分?6款视频剪切软件,零基础也能快速学会!

您是否也曾遇到了这样的一个问题&#xff1a;在录制完视频之后&#xff0c;发现视频中存在一些多余或者不想要的片段&#xff0c;想要将它剪切掉却不知道具体要怎么操作&#xff1f;别担心&#xff0c;几乎所有视频都会需要这样的调整才能更加出色。如果您是刚入门的视频剪辑初…

MATLAB中多张fig图合并为一个图

将下列两个图和为一个图 打开查看-----绘图浏览器 点击第一幅图中曲线右键复制&#xff0c;到第二幅图中粘贴即可完成

设计模式之组合模式例题

答案&#xff1a;C A 知识点&#xff1a;组合模式的意图&#xff1a;将对象组合成树型结构以表示“整体-部分”的层次结构&#xff0c;使得用户对单个对象和组合对象的使用具有一致性

TMS320F28335的RS232 通信实验

TMS320F28335 内部含有非常多的通信接口,其中串口是通信接口中应用 非常广泛之一,开发板上集成了一个 RS232 模块,其中串口就是接在 F28335 芯 片的 SCIA 接口。 F28335 通过 SCIA 实现与 PC 机对话,F28335 的 SCIA 收到 PC 机发来的数据后 原封不动的返回给 PC 机显示,定…

分布式项目-开盒头条

开盒头条 前言 只懂得技术理论是远远不够的&#xff0c;还需要熟练掌握很多业务功能逻辑的实现&#xff0c;这样才能真正的提高自己的开发水平。因此&#xff0c;我新开了这个专栏&#xff0c;专门做项目&#xff0c;教给大家很多业务功能实现的逻辑以及在实现这些业务功能时…