线程进阶

news2025/1/22 13:11:31

常见的锁策略

乐观锁 vs 悲观锁

乐观锁:预测锁竞争不是很激烈

悲观锁:预测锁竞争会很激烈

轻量级锁 vs 重量级锁

轻量级锁加锁解锁开销比较小,效率更高

重量级锁加锁解锁开销比较大,效率更低

多数情况下,乐观锁,也是一个轻量级锁(不能完全保证)

多数情况下,悲观锁,也是一个重量级锁(不能完全保证)

自旋锁 vs 挂起等待锁

自旋锁 是一种典型的轻量级锁,

挂起等待锁 是一种典型的重量级锁

给大家举个例子:我要向女神表白,但是女神告诉我她已经有男朋友了,对我而言我是不会放弃的(等待机会,锁被释放,女神分手)

自选锁:每天都给女神问候,一旦女神分手,就能第一时间感知到,从而有机会获取到锁,很明显,自选锁占用了大量的系统资源

挂起等待锁:一直默默地等待女神分手,如果女神分手了,有可能会告诉我,她分手了,但是也有可能女神把我忘了,指不定啥时候才能想起,这样把CPU省下来了。

自选锁优点:没有放弃CPU,不涉及线程阻塞和调度,一旦被释放,就能第一时间获取到锁 缺点:如果锁被其他线程占有的时间比较长,会消耗CPU资源,而挂起等待锁不消耗CPU。

互斥锁vs读写锁

互斥锁:像synchronized这样的锁提供加锁和解锁两个操作,如果一个线程加锁了,另一个线程也尝试加锁,就会阻塞等待。

而读写锁:提供三种操作1.针对读加锁 2.针对写加锁3.解锁 多个线程针对同一个变量读,这时候没有线程安全问题,也不需要加锁控制,如果当前一组操作有读也有写或者都是写,这时候会有线程安全问题,会产生锁竞争。在开发场景中,读操作非常高频,比写操作频率高很多

公平锁vs非公平锁

公平锁,当锁释放后,由等待队列中,最早的线程(等待时间最长)获取到锁

不可重入锁 vs 可重入锁

不可重入锁:一个线程针对同一把锁,连续加锁两次,出现死锁

可重入锁:一个线程针对同一把锁,多次加锁,不会死锁

synchronized

  1. synchronized 即是一个悲观锁,也是一个乐观锁,synchronized默认是一个乐观锁,但是如果发现当前锁竞争比较激烈,就会变成悲观锁

  1. synchronized即是轻量级锁,也是一个重量级锁,synchronized默认是轻量级锁,如果发现当前锁竞争比较激烈,会转换成重量级锁

  1. synchronized这里的轻量级锁,是基于自选锁的方式实现的,synchronized这里的重量级锁,基于挂起等待锁实现

  1. synchronizedf不是读写锁

  1. synchronized是非公平锁

  1. synchronized是可重入锁

CAS(Compare and swap)比较并交换

  1. 比较A和V是否相等

  1. 如果相等,将B和V的值交换

  1. 返回操作是否相等

上述交换的过程,大多数不关心B的情况,因此这里的交换也可以认为是赋值

上述CAS的过程,并非是通过一段代码实现的,而是通过一条CPU指令完成的,CAS操作是原子的,这样可以回避线程安全问题,这样为解决线程安全问题除了加锁又提供了一种方法。

CAS在处理++操作时会有这独特的作用,这时我们可以不用通过加锁的方式。

public static void main(String[] args)throws InterruptedException {
        AtomicInteger count=new AtomicInteger(0);
        Thread t1=new Thread(()->{
            for(int i=0;i<5000;i++)
            {
                count.getAndIncrement();//相当于count++
            }

        });
        t1.start();
        Thread t2=new Thread(()->{
            for(int j=0;j<5000;j++)
            {
                count.getAndIncrement();
            }

        });
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);



    }

CAS的应用场景:

  1. 实现原子类

  1. 实现自旋锁:

CAS的典型问题:

CAS在运行中的核心,检查value和oldvalue是否一致,如果一致,就交换,这里的一致可能是没改过,但也可能是改过了,又还原回来了。举个例子:把value的值设为A,cas判定value为A,此时确实value始终是A.也可能是value本来是A,被改成了B,又被还原成了A,ABA这种情况,大部分情况下是不会对代码产生影响的,但是不排除一些极端的情况,也是会产生影响的。给大家举个例子,假设我要去银行准备取钱600,当前账户的余额是1000,当按下取款机的那一刻,机器卡了下,我就多按了几下,这就可能产生bug,可能触发重复扣款的操作。

像这种问题出现的概率低,但也会出现,解决这种问题做好的办法就是加入一个版本号,初始版本号是1,每次修改,版本号都加1,然后进行CAS的时候,不是以金额为基准,而是以版本号为基准。

还是以取钱为例子,假设线程1余额为1000,打算取600,版本号为1,这时取款机卡了,多按了一下,线程2余额也为1000,取600,版本号为1,线程1CAS取款成功,余额为400,版本号加1变为2,这时朋友转账600过来,余额又变为1000,版本号加1变为3,此时线程2余额和之前读到的一样,但是版本号之前取到的是1,这时成了3,CAS返回false,取款失败。

Synchronized 原理

synchronized内部有一些优化机制,存在的目的是让这个锁更高效,实用。

  1. 锁升级/锁膨胀

(1)无锁

(2)偏向锁

(3)轻量级锁

(4)重量级锁

synchronized(locker)

{

}

当代码执行到这,首先会进入偏向锁状态,偏向锁并不是真正的加锁,而只是占个位置,有需要再真加锁,没需要就算了。synchronized的时候,并不是真正的加锁,先偏向锁状态,做个标记(这个过程是非常轻量的)如果在整个使用锁的过程中,都没有出现锁竞争,在synchronized执行完之后,取消偏向锁即可,但是如果在使用过程中,另一个线程也在尝试加锁,在它加锁之前,迅速的把偏向锁升级为真正的加锁状态,另一个线程也只能阻塞等待了。

当synchronized发生锁竞争的时候,就会从偏向锁,升级为轻量级锁,此时当synchronized相当于是通过自旋的方式,来进行加锁的,如果要是很快别人就释放锁了,自旋是划算的,但是如果迟迟拿不到锁,一直自旋,并不划算,synchronized自旋并不是一直的自旋,自旋到一定程度之后,就会升级到重量级锁(挂起等待锁),挂起等待锁则是基于操作系统原生的API来进行加锁,linux原生提供了mutex一组API,操作系统内核提供的加锁功能,这个锁会影响到线程的调度,此时如果线程试图进行重量级加锁,并且发生锁竞争,此时线程会被放到阻塞队列中,暂时不参与CPU调度,直到锁被释放了,这个线程才有机会被调度到,并且有机会获取到锁。

  1. 锁消除:

编译器智能的判定,看当前的代码是否需要真正的加锁,如果这个场景不需要加锁,但是程序员加了,就会自动的把锁干掉。

  1. 锁粗化:

锁的粒度:synchronized包含的代码越多,粒度就越粗,包含的代码越少,粒度就越细,通常情况下认为锁的粒度细一点好,但是有一些情况,锁的粒度粗一些更好

Callable接口类似于Runnable,Runnable用来描述一个任务,描述的任务没有返回值,Callable也是用来描述一个任务,此任务有返回值。如果需要一个线程计算出结果再返回,使用Callable更合适

 public static void main(String[] args) {
       // int count=0;
        Callable<Integer>a=new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int count=0;
                for(int i=0;i<100;i++)
                {
                    count++;
                }
                return count;

            }
        };
        FutureTask<Integer>futureTask=new FutureTask<>(a);
        Thread t1=new Thread(futureTask);
        t1.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            Integer result=futureTask.get();//获取结果
            System.out.println(result);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }

    }
}

ReentrantLock

ReentrantLock是标准库提供的另一种锁,是“可重入的”,synchronized是基于代码块的方式来加锁解锁的,ReentrantLock使用了lock方法和unlock方法加锁解锁

 public static void main(String[] args) {
        ReentrantLock reentrantLock=new ReentrantLock();
        reentrantLock.lock();
        
        
        
        reentrantLock.unlock();
        
    }
    

但是这样的写法是存在问题的:

public static void main(String[] args) {
        ReentrantLock reentrantLock=new ReentrantLock();
        reentrantLock.lock();
        if(true)
        {
            return;//直接返回了,但是锁还没有释放
        }

        

        reentrantLock.unlock();
    }

解决这样的问题:我们可以把unlock放在Finally语句中

 public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        try {
            reentrantLock.lock();
            if (true) {
                return;//直接返回了,但是锁还没有释放
            }
            if (3 > 1) {
                return;
            }
        } finally {
            reentrantLock.unlock();

        }
    }

上面是ReentrantLock的劣势,但是也是有优势的

  1. ReentrantLock提供了公平锁版本

 ReentrantLock reentrantLock = new ReentrantLock(true);

只需把括号里写上true,如果是false就是非公平锁版本

  1. 对于synchronized来说,加锁操作就是死等,只要获取不到锁,就一直死等,ReentrantLock则提供了更灵活的方法

  reentrantLock.tryLock();

无参数版本,能加上锁就加,加不上就放弃

有参数版本,能加上锁就加,加不上锁,就等待指定时间,到时间还没加上,就放弃

  1. ReentrantLock提供了一个更强大的等待机制

synchronized搭配wait、notify使用,notify随机唤醒一个等待的线程,而ReentrantLock能唤醒指定的线程

public static void main(String[] args)throws InterruptedException {
        AtomicInteger count=new AtomicInteger(0);
        Thread t1=new Thread(()->{
            for(int i=0;i<5000;i++)
            {
                //count.getAndIncrement();//相当于count++
                //count.incrementAndGet();
                count.addAndGet(2);
            }

        });
        t1.start();
        Thread t2=new Thread(()->{
            for(int j=0;j<5000;j++)
            {
                //count.getAndIncrement();
                //count.getAndDecrement();
                count.incrementAndGet();
            }

        });
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);



    }
}

信号量 Semaphore

信号量本质上就是一个计数器,描述了一个“可用资源的个数”P操作:申请一个可用资源,计数器就要-1,V操作:释放一个可用资源,计数器就要+1,P操作如果是计数器为0,继续P操作,就会出现阻塞等待的情况。

考虑一个计数初始值为1的信号量,针对这个信号量的值,就只有1和0两种取值(信号量不能是负的)执行一次P操作1->0,执行一次V操作0->1.如果已经执行了一次P操作了,再进行一次P操作,就会阻塞等待。

锁(锁也可以视为是计数器为1的信号量,二元信号量)锁是信号量的一种特殊表达,信号量是锁的一般表达。

实际中开发中,最经常用到的是锁,但是信号量也是偶尔会用到的

public static void main(String[] args) throws InterruptedException{
        Semaphore a=new Semaphore(4);4表示可用资源的个数
        a.acquire();
        System.out.println("申请一个资源空间");
        a.acquire();
        System.out.println("申请一个资源空间");
        a.acquire(2);
        System.out.println("申请两个资源空间");
        //此时信号量为0,会阻塞等待
        a.acquire();
        System.out.println("申请一个资源空间");

    }
public static void main(String[] args) throws InterruptedException{
        Semaphore a=new Semaphore(1);
        a.acquire();
        System.out.println("申请一个资源空间");
        a.release();
        System.out.println("释放一个资源空间");

    }

CountDownLatch(用于特定的场景)

给大家举个例子:有一场游泳比赛,这场游泳比赛,开始时间是明确的(裁判的指令)结束时间是不明确的(所有选手都冲过终点线)为了等待这个游泳比赛结束,就引入这个CountDownLatch

主要是两个方法:

  1. await(等待所有的线程)主线程调用这个方法

  1. countDown表示选手冲过了终点线

countDownLatch 在构造的时候,指定一个计数(选手的个数)

例如:指定三个选手进行比赛

初始情况下,调用await,就会阻塞,每个选手冲过终点,都会调用countDown方法,前两次调countDown,await没有任何影响。第三次调用countDown,await就会被唤醒,返回(解除阻塞),此时就可以认为整个比赛都结束了。

在实际开发中countDownLatch也是有很多使用场景的,比如下载一个大文件

Vector、Stack、HashTable 这几个是少数的线程安全类,其关键方法都带有synchronized。

多线程环境使用ArrayList

  1. 自己加锁,自己使用synchronized或者ReentrantLock

  1. Collections.synchronizedList 这里会提供一些ArrayList的相关方法,同时是带锁的,使用这个方法把集合类套一层

  1. CopyOnWriteArrayList

也叫做“写时拷贝”,当进行读操作时,ArrayList不进行任何操作,当进行写操作时,我们拷贝一份新的ArrayList,修改我们在新的ArrayList上修改,读操作我们在旧的ArrayList上读,显然这种方法的优点是不用加锁,但是也有局限性,只适合于数组比较小的情况。

多线程使用哈希表

HashMap是线程不安全的,HashTable是线程安全的,给关键方法加上了synchronized,更好的是ConcurrentHashMap,更优化的线程安全哈希表

那么ConcurrentHashMap相比于HashTable又有哪些优点呢?

  1. ConcurrentHashMap把大锁转化成了多把小锁,大大降低了锁冲突的概率,HashTable是直接给方法加上了synchronized,只要操作哈希表上的元素就都会加锁,也就都会产生锁冲突。

像这里的1,2线程1修改变量1,线程2修改变量2,此时会有线程安全问题,如果这两个元素相邻,就需要修改会产生线程安全问题,但是像1、3或者2、3这

不同的变量,我们在对其进行修改的时候,不会涉及到线程安全问题。但因为此时各个链表上的锁

对象一样,也会发生锁冲突。

此时,针对1 2锁的粒度变小了,针对同一把锁进行加锁,会有锁竞争,会保证线程安全

针对3 4这个情况,针对不同的锁进行加锁,不会有锁竞争,没有阻塞等待,程序就会更快(快是相对的,不会比不加锁快)。

  1. ConcurrentHashMap 做了一个激进的操作,针对读操作,不加锁,只针对写操作,加锁。读和读之间没有冲突,写和写之间有冲突,读和写之间也没有冲突,有同学可能会问读和写操作不加锁,不会产生“脏读“问题吗?是因为这里用了volatile+原子的写操作。

  1. ConcurrrntHashMap 内部充分利用了CAS,通过这个也来进一步的削减加锁操作的数目,比如维护元素个数

  1. 针对扩容,采取了“化整为零”的方式,HashMap HashTable扩容,创建一个更大的数组空间,在旧的数组链表上的每个元素 搬到新的数组上(删除+插入)这个扩容操作会在某次put的时候进行触发,如果元素个数特别多,就会导致这样的搬运操作,比较耗时,就会出现某次put比平时的put卡很多倍。

ConcurrentHashMap,扩容采取的是每次搬运一小部分元素的方式,创建新的数组,旧的数组也保留,每次put操作,都会往新数组上添加,同时进行一部分搬运(把一小部分旧的元素搬到新数组上)

每次get的时候,旧数组和新数组都查询

每次remove的时候,把元素删了就行。

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

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

相关文章

在CSDN年收入竟达五位数?----大学生技术自媒体成长之路

前言&#xff1a; Hello大家好&#xff0c;我是Dream。 还有不到两周就要过年了&#xff0c;自己也马上迈入了21岁&#xff0c;感慨时间飞快&#xff0c;从19岁开始入驻C站&#xff0c;到现在也已经整整两年了&#xff0c;把自己最好的两年青春时光留在了CSDN&#xff0c;超百万…

定义输出格式的使用-printf()函数

目录&#x1f4d6;printf()函数简介格式化规定符格式控制特殊规定字符示例printf()函数简介 printf()函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出信息。 printf()函数的调用格式为:printf("<格式化字符串>", <参量表>);这里看输出很简…

Lua入门学习

一、初识Lua脚本 Lua 是一种轻量小巧的脚本语言&#xff0c;用标准C语言编写并以源代码形式开放&#xff0c; 其设计目的是为了嵌入应用程序中&#xff0c;从而为应用程序提供灵活的扩展和定制功能。官网&#xff1a;https://www.lua.org/ 1、HelloWorld CentOS7默认已经安装…

什么是docker

文章目录简介Docker 架构特性局限Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中&#xff0c;然后发布到任何流行的 Linux或Windows操作系统的机器上&#xff0c;也可以实现虚拟化。容器是完全使用沙箱机制&#xff0c…

〖产品思维训练白宝书 - 产品思维认知篇⑨〗- 像 产品经理 一样去思考解决问题

大家好&#xff0c;我是 哈士奇 &#xff0c;一位工作了十年的"技术混子"&#xff0c; 致力于为开发者赋能的UP主, 目前正在运营着 TFS_CLUB社区。 &#x1f4ac; 人生格言&#xff1a;优于别人,并不高贵,真正的高贵应该是优于过去的自己。&#x1f4ac; &#x1f4e…

数据结构:算法的初步认识

算法是解决特定问题求解步骤的描述&#xff0c;在计算机中表现为指令的有限序列&#xff0c;并且每条指令表示一个或多个操作。 什么是算法 要求你写一个求 123…100 结果的程序&#xff0c;你应该怎么写呢? 大多数人会马上写出下面的C语言代码(或者其他语言的代码): int a,…

seata的AT模式

seata 分布式事务解决方案 官网 &#xff1a; seata.io 事务模式&#xff1a; 名词 TC &#xff1a; transaction coordinator 事务协调者 维护全局事务 和 分支事务的状态&#xff0c;驱动全局事务提交或者回滚TM &#xff1a; transaction manager 事务管理器 定义全局事…

(考研湖科大教书匠计算机网络)第一章概述-第三节:计算机网络的定义、功能分类

文章目录一&#xff1a;计算机网络的定义&#xff08;1&#xff09;最简单定义&#xff08;2&#xff09;较好定义二&#xff1a;计算机网络功能三&#xff1a;计算机网络的分类&#xff08;1&#xff09;按照“覆盖范围”分类&#xff08;2&#xff09;按照“使用者”分类&…

【Linux学习】进程控制

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《Linux学习》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 在前面&#xff0c;我们学习了进程的相关概念&#xff0c;在这里本喵会给大家介绍如何控制进程。…

私人影院微信小程序源码,采用腾讯小程序云开发,包括影院动态,最新影讯,房间预约,后台预约管理,导出预约表格Excel数据等功能

功能介绍 私人影院是很多年轻人比较喜欢的地方&#xff0c;虽然空间小&#xff0c;但是他们喜欢在这样的空间里享受休闲的时光&#xff0c;通过提前预约订位&#xff0c;可以使商家和消费者节省时间&#xff0c;减少人力成本&#xff0c;大大方便业务的拓展&#xff0c;同时也…

Word控件Spire.Doc 【Table】教程(4):如何在C#、VB.NET中设置Word表格样式

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

备战蓝桥杯数学基础:位运算理论

[TOC](目录)位运算概述位运算就是基于整数的二进制表示进行的运算&#xff0c;由于计算机内部就是以二进制来村塾数据&#xff0c;位运算是相当快的。基本的位运算共6种&#xff0c;分别为按位与、按位或、按位异或、按位取反、左移和右移与、或、异或这三者都是两数间的运算&a…

【算法刷题】栈与队列题型及方法归纳

栈与队列的特点 1、栈&#xff1a;FIFO 栈是仅能对其一端进行操作的结构&#xff0c;实现后进先出的效果。在C中采用容器适配器的方式实现栈。&#xff08;容器适配器实际上就是对某一类型的对象进行泛化&#xff0c;定义了这一类泛化对象的可进行操作的逻辑&#xff09; 什么…

上半年要写的博客文章22

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…

极端尺度物体的显著性分割方法(SOD 新 SOTA)

Paper Link&#xff1a;http://cvteam.buaa.edu.cn/papers.htmlBackground&#xff1a;显著性物体分割在常规图像场景取得突破进展&#xff0c;在极端尺度物体场景仍面临挑战。图像前景物体分割是深度学习、计算机视觉等领域的研究热点&#xff0c;在机器视觉、智能交通、智慧医…

Mac 电脑磁盘空间释放记录

起因 点开钉钉页面就卡住&#xff0c;看了一下&#xff0c;光一个钉钉占到4G左右的内存&#xff0c;给钉钉发了工单&#xff0c;没人理我。又随手看了一下系统磁盘空间使用情况&#xff0c;发现快满了&#xff01;&#x1f613; 应用清单 我是一个应用的超轻度用户&#xff…

NFT市场聚合器:他们有没有得到广泛使用?他们对于冷门的NFT交易市场有什么影响?

聚合器是什么&#xff1f;NFT市场聚合器是一种交易工具平台&#xff0c;允许用户在一个页面同时间从多个不同的市场浏览和发现NFT。 这些聚合器将市场上的NFT挂单整合在一起&#xff0c;使用户更容易找到并购买NFT&#xff0c;并节省了用户在多个不同NFT市场之间比较来回比较NF…

rabbitMQ简介

rabbitMQ简介 目前应用最广泛的一个基于AMQP规范的开源的消息中间件RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现 rabbitMQ的重要概念 Message&#xff1a;消息&#xff0c;消息是没有名称的&#xff0c;消息由消息头和消息体组成&#xf…

腾讯安全联合Gartner发布SOC+白皮书,助力政企构筑实战化安全运营体系

随着数字化浪潮的蓬勃兴起&#xff0c;各类政企机构上云步伐加快。与此同时&#xff0c;如高危漏洞、勒索病毒、挖矿木马、APT攻击等威胁层出不穷&#xff0c;安全形势日益严峻。许多政企机构虽部署了较为完备的基础安全产品&#xff0c;但防御体系仍以异构设备堆叠式为主&…

STM32+ESP8266+机智云+DHT11数据上传

机智云 文章目录机智云前言一、工程的修改二、数据的上传1.标识符2.数据处理3.数据上传三、app控制前言 今天搞了一下机智云&#xff0c;就想把温湿度发到app上去&#xff0c;然后能够控制灯的开关。之前从来没有用过这个玩意&#xff0c;用阿里云和点灯科技多一点&#xff0c…