JavaEE_CAS_Synchronized原理_线程安全集合类

文章目录

  • 一、CAS
    • 1.什么是CAS
    • 2.CAS有哪些应用
      • 1.实现原子类 - AtomicInteger
      • 2.基于CAS实现的自旋锁
      • 3.CAS的ABA问题
  • 二、Synchronized原理
    • 1.基本特点
    • 2.偏向锁
    • 3.锁消除
    • 4.锁粗化
  • 三、JUC(java.util.concurrent)的常见类
    • 1.Callable接口
    • 2.ReentrantLock
    • 3.信号量Semaphore
    • 4.CountDownLatch
  • 四、线程安全集合类
    • 1.多线程环境使用 ArrayList
    • 2.多线程环境使用队列
    • 3.多线程环境使用哈希表


一、CAS

1.什么是CAS

CAS:全称Compare and swap ,字面意思,“比较并交换”。

比较交换的是内存和寄存器。
如果一个内存:M
现在还有两个寄存器:A,B
CAS(M,A,B):
如果M和A的值相同,就把M和B里的值进行交换,同时整个操作返回true.
如果M和A的值不相同,无事发生,同时整个操作返回false

交换的本质,是为了把B赋值给M.(寄存器B里的值是啥,我们不太关心,更关心的是M里的情况)
M=B

CAS其实是一个cpu指令。一个cpu指令,就能完成上述比较交换的逻辑。单个cpu指令,是原子的!!!就可以使用CAS完成一些操作,进一步替代“加锁”。————给编写线程安全的代码引入新的思路,基于CAS实现线程安全的方式,也称为“无锁编程”

优点:保证线程安全,同时避免阻塞(效率)
缺点:
1.代码会更复杂,不好理解
2,只能够适合一些特定场景,不如加锁方式更普通

CAS本质上是cpu提供的指令=》又被操作系统封装,提供api=》又被JVM封装,也提供api=》程序员使用。

2.CAS有哪些应用

1.实现原子类 - AtomicInteger

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.

AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();

样例:

    public static AtomicInteger count= new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 =new Thread(()->{
            for(int  i =0;i<50000;i++)
            {
                //count++;
                count.getAndIncrement();
//                //++ count;
//                count.incrementAndGet();
//                //count --
//                count.getAndDecrement();
//                //-- count
//                count.decrementAndGet();
            }
        });
        Thread t2 =new Thread(()->{
            for(int  i =0;i<50000;i++)
            {
//                count++;
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

在这里插入图片描述
在Java中,有些操作的偏底层的操作,偏底层的操作在使用的时候有更多的注意事项。稍有不慎就容易写出问题,这些操作,就放到unsafe中进行归类。
在这里插入图片描述
原子类内部没有使用synchronized加锁的。
在这里插入图片描述
native修饰的方法称为“本地方法”
也就是在JVM源码中,使用C++实现的逻辑。=》涉及到一些底层操作。

结论:原子类里面是基于CAS来实现的。

前面说“线程不安全”本质上是进行自增的过程中,穿插执行了。
CAS也是让这里的自增,不要穿插执行,核心思路和加锁是类似的。
CAS则是会通过重试的方式,避免穿插。
在这里插入图片描述

2.基于CAS实现的自旋锁

public class SpinLock {
	//
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

3.CAS的ABA问题

CAS也是多线程中的一种重要技巧,虽然开发中直接使用CAS的概率不大,但是经常会用到一些内部封装了CAS的操作。

还会伴随ABA问题,什么是ABA问题?
CAS进行操作的关键,是通过值“没有发生变化”来作为“没有其他线程穿插执行”判断依据。
但是,这种判定方式不够严谨,更极端的的情况下:可能有另一个线程穿插进来,把值从A->B->B,针对第一个线程来说,看起来好像把这个值,没变,但实际上已经被穿插执行了。

针对ABA问题如果真的出现,其实大部分情况下也不会产生bug,虽然中间穿插一个线程执行了,由于值又改回去了,此时逻辑上不一定会产生bug.

ABA 问题引来的 BUG
假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 操作.

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期
    望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
  3. 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !!
  4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作
    这个时候, 扣款操作被执行了两次!!! 都是 ABA 问题搞的鬼

解决方案
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期

CAS 操作在读取旧值的同时, 也要读取版本号.
真正修改的时候,

  • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
  • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了)

小结:在实际开发中,一般不会直接使用CAS,都是用的封装好的,但是面试中比较容易考到CAS,一旦考察到CAS,一定会涉及到ABA问题。

二、Synchronized原理

1.基本特点

结合之前的锁策略,我们就可以总结出,Synchronized具有以下特性(只考虑JDK1.8):
1.开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁。
2.开始是轻量级锁实现,如果锁如果持有时间较长,就转换为重量级锁。
3.实现轻量级锁的时候大概率用到了自旋锁策略
4.是一种不公平锁
5.是一种可重入锁
6.不是读写锁

synchronized几个重要机制:
1.锁升级
2.锁消除
3.锁粗化

在这里插入图片描述

锁升级的过程是单向的,不能再降级了。

2.偏向锁

第一个尝试加锁的线程,优先进入偏向锁状态。
偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程. 如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.
偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销. 但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.
偏向锁既保证效率,也保证了线程安全。

3.锁消除

锁消除:也是一种编译器优化的手段。
编译器会自动针对你当前写的加锁的代码,做出判断,如果编译器觉得这个场景,不需要加锁,此时就会把你写的synchronized给优化掉。

StringBulider不带synchronized
StringBuffer带synchronized
如果是在单线程中使用StringBuffer,此时编译器就会自动把synchronized给优化掉。
编译器只会在自己非常有把握的时候,才会进行锁消除

偏向锁,这是运行的事情,运行过程中多线程的调度情况不同。
这个线程的锁肯有人竞争,可能没人竞争。

4.锁粗化

锁的粒度。
synchronized里头,代码越多,就认为锁的粒度越粗。代码越少,锁的粒度越细。

粒度细的时候,能够并发执行的逻辑更多,更有利于充分利用多核CPU资源。
但是,如果粒度细的锁,被反复进行加锁解锁,可能实际效果还不如粒度粗的锁(涉及到反复的锁竞争)

三、JUC(java.util.concurrent)的常见类

并发(这个包里的内容,主要就是一些多线程相关的组件)

1.Callable接口

也是创建线程的方式,适合于,想让某个线程执行一个逻辑,并且返回结果的时候,相比之下,Runable不关注结果。

    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();
        //此时的get就能获取到callable里面的返回结果
        //由于线程是并发执行的,执行到主线程的get的时候,t线程可能还没执行完。
        //没执行完的话,get就会阻塞。
        System.out.println(futureTask.get());
    }

futureTask是给我们凭借谁来取结果。

小结:线程的创建方式
1.继承Thread,重写run(创建单独的类,也可以匿名内部类)
2.实现Runable重写run(创建单独的类,也可以匿名内部类)
3.实现Callable,重写call(创建单独的类,也可以匿名内部类)
4.使用lambda表达式
5.ThreadFactory线程工厂
6.线程池

2.ReentrantLock

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
ReentrantLock 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
  • unlock(): 解锁
    public static void main(String[] args) {
        ReentrantLock lock =new ReentrantLock();
        lock.lock();
        try{
            //woking
        }finally
        {
            lock.unlock();
        }
    }

优势:
1.ReentrantLock,在加锁的时候,有两种方式,lock,tryLock.——tryLock给了更多操作空间。
2.ReentrantLock,提供了公平锁的实现(默认情况下是非公平锁)
3.ReentranLock提供了更强大的等待通知机制。搭配了Condition类实现等待通知的。
虽然ReentrantLock有上述优势,但是咱们在加锁的时候,还是首选synchronized,但是很明显,ReentrantLock使用更复杂,尤其是容易忘记解锁。

3.信号量Semaphore

信号量,用来表示“可用资源的个数”,本质上就是一个计数器。描述了“可用资源”的个数
每次申请一个可用资源,就需要让计数器-1 P操作acquire
每次释放一个可用资源,就需要让计数器+1 V操作 release
(这里的+1和-1都是原子的)

信号量,假设初始情况下数值是10
每次进行P操作,数值就-1
当我已经进行了10次P操作之后,数值就是0了。
如果我继续进行P操作,会咋样?=>阻塞等待!!
锁,本质上就属于是一种特殊的信号量。锁就是可用资源为1的信号量。锁就是可用资源为1的信号量。加锁操作,P操作1-》0,解锁操作,v操作0->1——二元信号量
操作系统,提供了信号量实现,提供了api.JVM封装了这样的api,就可以在java代码中使用了。

    public static void main(String[] args) throws InterruptedException {
        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操作");
        semaphore.release();
    }

在这里插入图片描述

4.CountDownLatch

这个东西,主要是适用于,多个线程来完成一些列任务的时候,来衡量任务的进度是否完成。
比如需要把一个大的任务,拆分成多个小的任务,让这些任务并发的去执行。
就可以使用countDownLatch来判定说当前这些任务是否全都完成了。

下载一个文件,就可以使用多线程下载。
很多的下载工具,下载速度,很一般。
相比之下,有一些专业下载工具,就可以成倍的提升下载速度(IDM),多个线程下载,每个线程都建立一个连接,此时就需要把任务进行分割。

CountDownLatch主要有两个方法:

  1. await,调用的时候就会阻塞,就会等待其他的线程完成任务,所有的线程都完成了任务之后,此时这个await才会返回,才会继续往下走。
  2. countDown,会告诉countDownLatch,我当前这一个子任务已经完成了。
    public static void main(String[] args) throws InterruptedException {
        //10个选手参赛,await就会在10次调用完,countDown之后才能继续执行。
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for(int i=0;i<10;i++)
        {
            int id =i;
            Thread t= new Thread(()->{
                System.out.println("thread"+id);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //通知说当前任务执行完毕了。
                countDownLatch.countDown();
            });
            t.start();
        }
        countDownLatch.await();
        System.out.println("所有的任务都完成了");
    }

在这里插入图片描述

四、线程安全集合类

数据结构中大部分的集合类,都是线程不安全的。
Vector,Stack,Hashtable线程安全 用了synchronized
上古时期,Java引入的集合类,现在都不建议使用了,未来会被删除的内容。

1.多线程环境使用 ArrayList

1.针对这些线程不安全的集合类,要想在多线程环境下使用,就需要考虑好线程安全的问题了。

2.同时,标准库,也给我们提供了一些搭配的组件,保证线程安全。

Collections.synchronizedList(new ArrayList);

这个东西会返回一个新的对象,这个新对象,就相当于给ArrayList套上一层壳。这层壳就是在方法上直接使用synchronized的。

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

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

2.多线程环境使用队列

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

3.多线程环境使用哈希表

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

  • Hashtable
  • ConcurrentHashMap
    1)Hashable
    只是简单的把关键方法加上了 synchronized 关键字
    只要两个线程,在操作同一个Hashtable就会出现锁冲突,但实际上,对于哈希表来说,锁不一定非得这么加,有些情况,其实是不涉及到线程安全问题的。
  1. ConcurrentHashMap
  • ConcurrentHashMap最核心的改进,就是把一个全局的大锁,改进成了每个链表独立的一把小锁。这样做,大幅降低了锁冲突的概率。
  • 充分利用到了CAS特性,把一些不必要加锁的环节给省略加锁了。比如需要使用变量记录hash表中的元素个数,此时,就可以使用原子操作(CAS)修改元素个数。
  • ConcurrentHashMap还有一个激进的操作,针对读操作没有加锁,读和读之间,读和写之间,都不会有锁竞争。
  • ConcurrentHashMap针对扩容操作,做出了单独的优化。本身hashtable或者HashMap在扩容的时候,都是需要把所有元素都拷贝一遍的(如果元素很多,拷贝就比较耗时)化整为零,一旦需要扩容,确实需要搬运,不是在一次操作中搬运完成,而是多分多次,来搬运,每次搬运一部分数据,避免这单次操作过于卡顿。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

11.7 堆排序

目录 11.7 堆排序 11.7.1 算法流程 11.7.2 算法特性 11.7 堆排序 Tip 阅读本节前&#xff0c;请确保已学完“堆“章节。 堆排序&#xff08;heap sort&#xff09;是一种基于堆数据结构实现的高效排序算法。我们可以利用已经学过的“建堆操作”和“元素出堆操作”…

(uniapp)简单带动画的tab切换效果

效果图 代码 <template><view class"tabBox"><view :style"{transform: translateX(${translateX})}" class"whiteBox"></view><view click"changeTab(k)" class"itemBox" v-for"(v,k) in…

安防视频融合汇聚平台EasyCVR如何实现视频画面自定义标签?

安防视频融合汇聚平台EasyCVR兼容性强&#xff0c;可支持Windows系统、Linux系统以及国产化操作系统等&#xff0c;平台既具备传统安防视频监控的能力&#xff0c;也具备接入AI智能分析的能力&#xff0c;可拓展性强、视频能力灵活&#xff0c;能对外分发RTMP、RTSP、HTTP-FLV、…

【二叉树】Leetcode 222. 完全二叉树的节点个数【简单】

完全二叉树的节点个数 你一棵 完全二叉树 的根节点 root &#xff0c;求出该树的节点个数。 完全二叉树 的定义如下&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&#xff0c;其余每层节点数都达到最大值&#xff0c;并且最下面一层的节点都集中在该层最…

273 基于matlab的改进型节点重构小波包频带能量谱与 PNN(概率神经网络)的联合故障诊断新方法

基于matlab的改进型节点重构小波包频带能量谱与 PNN&#xff08;概率神经网络&#xff09;的联合故障诊断新方法。针对风电机组故障信号的非平稳性以及故障与征兆的非线性映射导致的故障识别困难问题&#xff0c;提出了改进型的节点重构小波包频带能量谱与PNN&#xff08;概率神…

C++设计模式-中介者模式,游戏对象之间的碰撞检测

运行在VS2022&#xff0c;x86&#xff0c;Debug下。 31. 中介者模式 中介者模式允许对象之间通过一个中介者对象进行交互&#xff0c;而不是直接相互引用。可以减少对象之间的直接耦合&#xff0c;同时集中化管理复杂的交互。应用&#xff1a;如在游戏开发中&#xff0c;可以使…

【pytorch】大模型训练张量并行

Large Scale Transformer model training with Tensor Parallel (TP) 张量并行如何工作 原始 Tensor Parallel (TP) 模型并行技术于Megatron-LM论文中被提出&#xff0c;是一种用于培育大规模Transformer模型的高效模型并行技术。我们在本练习指南中介绍的序列并行 (SP) 实际…

postgresql常用命令#postgresql认证

PostgreSQL 是一个功能强大的开源关系数据库管理系统&#xff0c;提供了一系列命令行工具来管理和操作数据库。以下是一些常用的 PostgreSQL 命令&#xff0c;涵盖数据库和用户管理、数据操作以及查询和维护等方面。 #PostgreSQL培训 #postgresql认证 #postgreSQL考试 #PG考试…

从零开始:腾讯云轻量应用服务器上部署MaxKB项目(基于LLM大语言模型的知识库问答系统)

使用腾讯云轻量应用服务器部署和使用MaxKB项目 前言 一&#xff0c; MaxKB介绍 MaxKB是基于LLM大语言模型的知识库问答系统&#xff0c;旨在成为企业的最强大脑。它支持开箱即用&#xff0c;无缝嵌入到第三方业务系统&#xff0c;并提供多模型支持&#xff0c;包括主流大模型…

R语言绘图 --- 气泡图(Biorplot 开发日志 --- 4)

「写在前面」 在科研数据分析中我们会重复地绘制一些图形&#xff0c;如果代码管理不当经常就会忘记之前绘图的代码。于是我计划开发一个 R 包&#xff08;Biorplot&#xff09;&#xff0c;用来管理自己 R 语言绘图的代码。本系列文章用于记录 Biorplot 包开发日志。 相关链接…

大数据数据治理工具

大数据数据治理-CSDN博客 大数据数据治理工具&#xff1a; 开源工具&#xff1a; Apache Atlas&#xff1a; 一个开源的数据治理和元数据框架&#xff0c;为Hadoop生态系统提供数据分类、管理和安全功能。 Apache Ranger&#xff1a; 一个集中式安全管理框架&#xff0c;用于…

RPG Maker MV 踩坑十一 精灵及背景绘制问题

精灵绘制问题 RPG Maker MV战斗问题入场飞身战斗背景绘制精灵集及精灵 RPG Maker MV战斗问题 在RMMV中战斗是在场景中调用战斗管理器&#xff0c;通过管理器去操作角色对象行动及精灵的绘制的。 入场飞身 在其中就发现一个问题加载图片进场时&#xff0c;会偏高&#xff0c;…

SSRF及相关例题

SSRF及相关例题 服务端请求伪造&#xff08;Server Side Request Forgery, SSRF&#xff09;指的是攻击者在未能取得服务器所有权限时&#xff0c;利用服务器漏洞以服务器的身份发送一条构造好的请求给服务器所在内网。SSRF攻击通常针对外部网络无法直接访问的内部系统。 SSR…

NAVICAT从MYSQL链接到ORCAL数据库

1、工具-选线 2、环境&#xff0c;将原有的mysql的oci.dll文件改为oracle的 3、新建连接 填写对应数据

【全开源】Java代驾小程序APP代驾跑腿源码微信小程序代驾源码

&#x1f697;代驾小程序&#xff1a;便捷、安全的出行新选择 一、引言&#xff1a;出行新风尚 在如今繁忙的都市生活中&#xff0c;出行问题一直是人们关注的焦点。代驾小程序的出现&#xff0c;为我们提供了一种便捷、安全的出行新选择。无论是酒后不能驾车&#xff0c;还是…

Visual C++2010学习版详细安装教程(超详细图文)

Visual C 介绍 Visual C&#xff08;简称VC&#xff09;是微软公司推出的一种集成开发环境&#xff08;IDE&#xff09;&#xff0c;主要用于开发C和C语言的应用程序。它提供了强大的编辑器、编译器、调试器、库和框架支持&#xff0c;以及丰富的工具和选项&#xff0c;使得开…

opencv dnn模块 示例(26) 目标检测 object_detection 之 yolov10

文章目录 1、yolov10简要介绍1.1、双标签分配策略1.2、架构改进1.3、性能1.4、预训练模型 1.4、网络有关层说明2、测试2.1、官方测试2.2、opencv dnn仅运行到内部"NMS"步骤之前的层完整代码完整实现所有层 2.3、onnxruntime测试3.4、tensorrt 1、yolov10简要介绍 从…

攻防世界---misc---a_good_idea

1、下载附件得到一张图片&#xff0c;winhex分析&#xff0c;发现有压缩包 2、在kali中用普通用户对jpg进行binwalk 3、得到两张图片和一个文本&#xff0c;查看文本信息&#xff1a;提示试着找到像素的秘密 4、提到像素就想到了Stegsolve这个工具&#xff0c;将这两张图片用该…

Windows主机信息收集

一、内网环境分析 1、什么是内网渗透 内网渗透: ①在拿到webshell的时候&#xff0c;想办法获得系统信息拿到系统权限&#xff0c;进入到网络系统内部之后收集内部 网络的各种信息&#xff0c;获取内部网络有价值的人员、资产信息。 ②内网渗透的第一步&#xff0c;内网信…

Java | Leetcode Java题解之第129题求根节点到叶节点数字之和

题目&#xff1a; 题解&#xff1a; class Solution {public int sumNumbers(TreeNode root) {if (root null) {return 0;}int sum 0;Queue<TreeNode> nodeQueue new LinkedList<TreeNode>();Queue<Integer> numQueue new LinkedList<Integer>();…