JavaEE初阶-多线程进阶2

文章目录

  • 前言
  • 一、CAS
    • 1.1 CAS的概念
    • 1.2 原子类
    • 1.3 CAS的ABA问题
  • 二、JUC中常用类
    • 2.1 Callable接口
    • 2.2 ReentrantLock(可重入)
    • 2.3 Semaphore信号量
    • 2.4 CountDownLatch类
    • 2.5 CopyOnWriteArrayList类
    • 2.6 ConcurrentHashMap


前言

对于多线程进阶的部分,更多总结的就是面试常考,但是工作中开发中不常用到的知识。


一、CAS

1.1 CAS的概念

CAS就是compare and swap的首字母缩写,意味着比较和交换,这样的一条指令即可完成比较和交换这一套操作,也就是说这套操作是原子的。
我们可以将CAS的流程想象成一个方法。
在这里插入图片描述
这里的交换其实思想上更偏向于赋值,因为一般更关注于内存地址address中的内容而不关心寄存器reg2中的内容,所以就可以近似说这里的操作就是将reg2的值赋给了address地址。
CAS一般就是cpu中的一条指令,所以操作系统为了使用它完成这样的操作就需要去提供这样的CAS的api。然后JVM又对这样的api进行了封装,使得我们在java中也能够使用CAS操作了。但是实际上这样的CAS操作被封装到了“unsafe”包当中,就是提醒大家容易出错,不鼓励直接使用CAS。

1.2 原子类

Java当中也有一些类对CAS进行了进一步的封装,就比如说原子类。
在这里插入图片描述
如上图的AtomicInteger就相当于对int进行了封装,对于它的++或者–操作都是原子的,实例代码如下:

package thread;

import java.util.concurrent.atomic.AtomicInteger;

public class Demo41 {
    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                //count++ 这里的对count的修改都是原子的
                count.getAndIncrement();
                //++count
                //count.incrementAndGet();
                //--count
                //count.decrementAndGet();
                //count--
                //count.getAndDecrement();
                //count+=10;
                //count.getAndAdd(10);
            }
        });

        Thread t1 = new Thread(() -> {

            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();
            }
        });

        t.start();
        t1.start();

        t.join();
        t1.join();
        System.out.println(count);


    }
}

这里的多线程代码就是经典的两个线程两个循环来计算count值,因为这里的count使用到了原子类的方法,所以加一操作是原子性的,自然不存在线程安全的问题,也能够得到正确结果。
那么使用这种原子性操作的意义是什么呢?意义就在于效率,因为锁是一个很重量级的操作,如果操作没有原子性在多线程的情况下就要加锁,但是我们可以使用CAS从而不去使用锁,从而提高代码效率。这一套基于CAS不加锁实现线程安全代码的方式,也被称为“无锁编程”。但是CAS这种方法也就仅仅适用于少数场景。

1.3 CAS的ABA问题

属于CAS的一个重要注意事项,CAS的核心就是“比较-发现相等-交换”->发现相等即数据没发生任何改变,但是相等不等于没改变过。可能值经历了一个从A到B再到A的过程,这种情况在极端环境下会产生问题。
在这里插入图片描述

如上图取款操作,假如我们要取500,情急之下,我们多按了两次取款按钮,此时产生了两个线程来进行扣款操作,但是如果在此时别人给你转了500,那么就会出现问题了。
在这里插入图片描述
如图左边是t1线程,右边是t2线程,t2线程完成扣款五百之后,此时t3线程给账户又转了500,此时应该不成立的t1线程的判断又成立了,导致又完成一次扣款。上述的过程就是典型的ABA问题所造成的bug,是非常极端的情况。
如何去避免这样的问题呢?可以约定一个版本号,每次进行扣款或存款都更新版本号,如果版本号没有改变数据就一定没变过。
在这里插入图片描述
通过版本号约束就可以避免这里的ABA问题,避免多次扣款。即使t3线程仍然给账户汇了500,但是此时版本号已经是2了,所以t1线程的版本号对不上,方法内部的扣款操作无法完成,所以即使有两个线程去扣款,扣的款也只有500。

二、JUC中常用类

JUC是java.util.concurrent这个包的首字母,在这里介绍一下这个包当中的常用类。

2.1 Callable接口

我们都知道Runnable接口用来表示一个待执行的任务,Callable接口和Runnable也是相似的,他也是用来表示一个待执行的任务,但是Callable有返回值,表示这个线程执行结束要得到的结果是啥。

public class Demo42 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //使用Runnable来求出1~100的和
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int result = 0;
                for (int i = 1; i <= 100; i++) {
                    result += i;
                }
                //需要用成员变量来接收值 主线程和t线程的耦合程度高 如果有多个这样的线程就不方便了
                count = result;
            }
        });

以上给出了一段代码,就是使用类变量count来得到线程结果,这样的代码等线程多了之后很不方便,代码不够优雅。Callable就是用来解决上述代码的问题的。接下来给出全部代码用于对比:

package thread;

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

public class Demo42 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //使用Runnable来求出1~100的和
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int result = 0;
                for (int i = 1; i <= 100; i++) {
                    result += i;
                }
                //需要用成员变量来接收值 主线程和t线程的耦合程度高 如果有多个这样的线程就不方便了
                count = result;
            }
        });

        // t.start();
        // t.join();
        // System.out.println(count);

        // Callable和Runnable很相似 但是Runnable可以返回计算的值
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for (int i = 1; i <= 100; i++) {
                    result += i;
                }
                return result;
            }
        };

        // futuretask这个类用来包装callable这个类 这样callable就可以直接放入线程
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t2 = new Thread(futureTask);
        t2.start();
        //从future获取线程启动通过callable计算得到的值
        t2.join();
        System.out.println(futureTask.get());
    }

}

如以上代码,Callable接口需要使用FutureTask来包装,包装之后就将FutureTask对象放入线程,线程执行完成之后就可以通过FutureTask对象来得到线程执行的结果。

2.2 ReentrantLock(可重入)

在以前的JDK中,synchronized还没现在那么好用,那时ReentrantLock还是非常有市场的。但是随着版本的迭代,synchronized越来越强,基本上需要加锁的时候无脑使用synchronized大概率不会出问题。那么ReentrantLock现在还有什么价值?
(1)ReentrantLock实现了公平锁
在这里插入图片描述
这里代码中的参数写true就是公平锁,false就是非公平锁。
(2)ReentrantLock提供了tryLock操作,给加锁提供了更多的操作空间。
尝试加锁,如果该锁已经被获取了,那么就直接失败返回,不会继续等待。tryLock还有一个类似版本就是可以指定等待的时间,超时后返回。
(3)synchronized搭配wait以及notify的等待通知机制,ReentrantLock搭配Condition类完成等待通知。
Condition类比wait以及notify强一点。(多个线程wait,notify唤醒随机一个。Condition指定线程唤醒)

2.3 Semaphore信号量

信号量是一个非常简单的概念,就是一个计数器,描述了可用资源的数目。围绕信号量有两个操作,P操作,计数器减一,申请资源,V操作,计数器加一,释放资源。提出信号量的是荷兰人,PV是荷兰语的首字母,在英语中是acquire就是获取,以及release表示释放。代码示例如下:

package thread;

import java.util.concurrent.Semaphore;

public class Demo44 {

    public static void main(String[] args) throws InterruptedException {
        // 四个可用资源 P申请资源 V释放资源
        Semaphore semaphore = new Semaphore(4);

        semaphore.acquire(1);
        System.out.println("P操作");
        semaphore.acquire(1);
        System.out.println("P操作");
        semaphore.acquire(1);
        System.out.println("P操作");
        semaphore.acquire(1);
        System.out.println("P操作");

        // 此时信号量的四个资源已经被申请完了
        // 如果继续申请的话就会堵塞 因为要等别的线程释放信号量的资源
        semaphore.acquire(1);

    }


}

以上代码信号量拥有四个单位的资源,然后通过acquire方法来申请资源,当资源被申请完并且没有资源释放时,再次申请资源就会阻塞。当设置信号量资源为一个单位,则信号量取值只能为1或者0,此时的信号量可以当成锁来使用。代码示例如下:

package thread;

import java.util.concurrent.Semaphore;

public class Demo45 {
    public static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        //设置 1 0 信号量
        Semaphore semaphore = new Semaphore(1);

        Thread t1 = new Thread(() -> {
            try {
                for (int i = 0; i < 50000; i++) {

                    semaphore.acquire(1);
                    count++;
                    semaphore.release();


                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        });

        Thread t2 = new Thread(() -> {

            try {
                for (int i = 0; i < 50000; i++) {
                    semaphore.acquire(1);
                    count++;
                    semaphore.release();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(count);
    }
}

以上代码其实还是多线程代码的经典例子,使用两个线程来计算累加值。当t1进行count加一的操作时,它已经申请了唯一的信号量资源,此时如果t2线程也想进行count加一就必须先执行申请信号量资源的操作,此时就会阻塞,只有当t1线程的count++执行结束之后释放资源,t2线程才能继续执行,这就实现了count++操作的原子性,从而避免线程安全问题。

2.4 CountDownLatch类

相对来说比较实用的工具类,当我们把一个任务分为多个时,就可以通过这个工具类来识别任务是否整体执行完毕了。代码示例如下:

package thread;

import java.util.concurrent.CountDownLatch;

public class Demo46 {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);

        for (int i = 0; i < 10; i++) {
            int temp = i;
            Thread t = new Thread(() -> {
                System.out.println("线程启动:" + temp);
                //当作任务
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程结束:" + temp);
                latch.countDown();
            });

            t.start();

        }

        //等待所有线程中的任务结束
        latch.await();
        System.out.println("所有线程结束。");

    }

}

这段代码中我们给CountDownLatch类对象的参数为10,并且创建10个线程去执行任务,并且在每个线程中使用countDown方法,countDown方法相当于计数,当一个线程结束就会加一,latch.await()方法就会等待所有线程执行结束,当countDown方法累加的数等于初始化CountDownLatch对象的参数时await方法就会停止等待,整个代码就运行结束了。这里代码中CountdownLatch对象的参数和线程数相等,并且每个线程都放了countDown方法,所以所有线程运行结束await方法也就不等了。

2.5 CopyOnWriteArrayList类

ArrayList,LinkedList,Stack,Queue,HashMap…在多线程下使用集合类需要注意线程安全问题。Vector自带了synchronized,Stack继承自Vector所以也有synchronized,HashTable也是自带synchronized。但是需要注意一点,加锁不代表就是线程安全的,不加锁也不能确定线程就是一定不安全的,需要具体代码具体分析。
在我们使用未加锁的类时需要手动进行加锁,这样是比较麻烦的,标准库提供了一些其它的解决方案,如下图。
在这里插入图片描述
通过这样的操作,给ArrayList这些集合类套一层壳,就是给一些关键方法加上了synchronized,使得ArrayList达到Vector那样的效果。
CopyOnWriteArrayList类也是一种解决线程安全问题的方法。
在这里插入图片描述
如果当前有多个线程读列表上的数据,那么不需要做任何处理。如果某个线程对上面的数据进行修改,此时另一个线程进行读取,那么很可能会读到200 3这样的中间情况。CopyOnWriteArrayList这样的类就是一种写时拷贝,在你对列表进行修改时会开辟新空间在新空间上进行修改,你要读取数据那么就在旧空间进行读取,当修改完成后将新的列表的引用代替旧的引用,旧的空间就可以释放了。这样的过程没有任何的加锁和阻塞,也能保证线程读不到错误的数据。
这种方法的思想应用的很广,例如显卡渲染画面到显示器,显示的动态效果其实就是很多张图片,由于显卡渲染足够快这些图片就能融合在一起,看到动画效果。实际上就是写时拷贝,在显示上一个画面的时候,在背后的额外空间生成下一个画面,生成完毕了用下一个画面代替上一个画面。

2.6 ConcurrentHashMap

我们知道HashMap是线程不安全的,HashTable是带锁的,是否是线程安全的?事实上并不推荐使用这个,标准库提供了更好的替代也就是ConcurrentHashMap。
HashTable加锁就是简单粗暴的给每个方法加了synchronized,就相当于针对this加锁,只要针对HashTable上的元素进行操作,就都会涉及到锁冲突。
ConcurrentHashMap做出了以下优化:
(1)使用锁桶的方式来代替一把全局锁,有效降低冲突概率。
在这里插入图片描述
这一点很好理解,如果有两个线程针对两个不同的链表进行操作,那么它们之间是不会产生锁冲突的。本身两个线程修改的是不同的链表,也没涉及到“公共变量”,所以不涉及线程安全问题。这个提升是非常大的,因为一个哈希表上的桶非常多,桶之间发生冲突的概率非常小,并且synchronized我们前面的博客也讲过了,只要不发生冲突synchronized只是加了一个偏向锁,就类似一个标记,消耗非常小。
(2)对于哈希表的size即使你修改的不同链表/桶,但是你在多线程的情况下也会涉及到多个线程修改一个公共变量的问题,在ConcurrentHashMap中对于size的修改就是使用CAS这种具有原子性的语句来完成,这样不仅避免了加锁这种重量级的操作,也解决了线程安全的问题。
(3)针对扩容进行了特殊优化。
如果发现负载因子太大了,那么就需要扩容,然而扩容又是比较低效的操作,普通的HashMap要在一次put的过程中完成整个扩容过程,就会使得put操作非常卡。ConcurrentHashMap就会在扩容的时候整出另外的一份空间,每次进行哈希表的基本操作都会将一部分扩容之前空间的数据搬到新空间,不是一口气搬完而是分多次,在搬的过程中如果是插入操作就将新数据插入到新空间,删除操作,新旧空间都进行删除,查找操作,新旧空间都要查找。
另外值得一提的是,在java8之前ConcurrentHashMap是基于分段锁的形式进行实现的,就是引入多个锁对象,每个锁对象去管理若干个哈希桶。相比于HashTable这个方法是进化,但是还是不如直接锁桶,后面就把这个方法给废弃了。

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

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

相关文章

大型语言模型自我进化综述

24年4月来自北大的论文“A Survey on Self-Evolution of Large Language Models”。 大语言模型&#xff08;LLM&#xff09;在各个领域和智体应用中取得了显着的进步。 然而&#xff0c;目前从人类或外部模型监督中学习的LLM成本高昂&#xff0c;并且随着任务复杂性和多样性的…

嵌入式学习第三十三天!(二叉树)

1. 树的概念&#xff1a; 1. 树&#xff1a;由n个结点组成的有限集&#xff0c;有且只有一个根结点&#xff08;由根结点可以访问后继结点&#xff09;&#xff0c;其他结点只有一个前驱结点&#xff0c;但可以有多个后继结点&#xff08;一对多&#xff09;。当n 0时&#xf…

unordered_map 和 unordered_set

unordered —— 无序的&#xff0c;从表面上来看&#xff0c;与 map 和 set 不同之处就在于&#xff0c;unordered_map 和 unordered_set 无法保证插入数据是有序的&#xff1b; 尽管如此&#xff0c;由于这两种容器内部封装了“哈希桶”&#xff0c;可以实现快速查找数据 ——…

EE trade:投资贵金属的技巧

投资贵金属&#xff0c;特别是流行的黄金和白银&#xff0c;需要一个明智的策略和一些重要的技巧。以下是一些有用的投资技巧&#xff1a; 进行市场研究&#xff1a;在投资前了解市场运行机制、价格波动因素以及可能影响市场的宏观经济指标。 理解供需关系&#xff1a;贵金属…

历史影像的下载办法总结

最近想要下黄河口的历史影像&#xff0c;试验了几个办法&#xff1a; 1&#xff09;参考文献1中的办法&#xff0c;用Global Mapper下载World Imagery Wayback网站的历史数据&#xff0c;能下载从2014年至现在的&#xff1b; 2&#xff09;参考文献1中的办法&#xff0c;用SA…

2024淘宝天猫618凑单跨店满300减多少及618红包领取口令是什么?

2024年天猫618购物狂欢节全攻略 随着夏日的脚步临近&#xff0c;一年一度的天猫618年中大促也即将拉开帷幕。作为年中最大的购物狂欢节&#xff0c;天猫618不仅汇聚了众多优质商品&#xff0c;还准备了丰富的优惠活动和红包福利&#xff0c;让消费者在享受购物乐趣的同时&…

LeetCode1657确定两个字符串是否接近

题目描述 如果可以使用以下操作从一个字符串得到另一个字符串&#xff0c;则认为两个字符串 接近 &#xff1a; 操作 1&#xff1a;交换任意两个 现有 字符。例如&#xff0c;abcde -> aecdb操作 2&#xff1a;将一个 现有 字符的每次出现转换为另一个 现有 字符&#xff0…

如何做好图纸加密

在工业设计领域&#xff0c;图纸无疑是企业最宝贵的资产之一&#xff0c;其中包含了大量的创新思想、独特设计和商业秘密。然而&#xff0c;随着信息技术的发展和应用的普及&#xff0c;图纸防泄密工作面临着前所未有的挑战。因此&#xff0c;如何有效地防止图纸泄密&#xff0…

QCC---Aptx Lossless验证

因为aptx Lossless属于高通骁龙声音的一部分&#xff0c;一般支持高通骁龙声音的设备会支持到&#xff0c;比如说手机&#xff0c;而且还要支持最新的aptx adaptive协议R2.2版本。但是如果手上没有这样的手机的话&#xff0c;有source芯片也可以去做测试验证。在最新的784.1版本…

【class2】人工智能初步(自然语言处理)

要实现从评价中提取高频关键词&#xff0c;并判别其正负面性&#xff0c;其实是通过人工智能领域中的一个分支&#xff1a;自然语言处理。 在了解自然语言处理之前&#xff0c;我们先来说说&#xff0c;什么是自然语言&#xff08;Natural Language&#xff09;&#xff1f;自…

品牌出海新趋势:掌握“微创新”策略,快速适应海外市场

在全球化的今天&#xff0c;品牌出海已成为众多企业拓展业务、实现国际化发展的重要途径。然而&#xff0c;海外市场与本土市场在文化、消费习惯、法律法规等方面均存在显著差异&#xff0c;这要求品牌在海外市场中必须灵活应变&#xff0c;通过微小的、有针对性的创新来快速适…

idea配置MySQL提示

点击sql语句&#xff0c;然后再选择show context actions 然后再选择Inject language or reference 然后再选择MySQL 然后我们会发现sql语句变颜色了 如果表是红色 那么需要我们连接mysql的对于的数据库

478.8-480W 宽电压输入 AC/DC 导轨式开关电源——TPR/SDR-480-XS 系列

TPR/SDR-480-XS 系列导轨式开关电源&#xff0c;额定输出功率为478.8-480W&#xff0c;产品输入范围&#xff1a;85-264VAC。提供24V、36V、48V输出&#xff0c;具有短路保护&#xff0c;过载保护等功能&#xff0c;并具备高效率&#xff0c;高可靠性、高寿命、更安全、更稳定等…

BGP—边界网关协议

BGP 动态路由协议可以按照工作范围分为IGP以及EGP。IGP工作在同一个AS内&#xff0c;主要用来发现和计算路由&#xff0c;为AS内提供路由信息的交换&#xff1b;而EGP工作在AS与AS之间&#xff0c;在AS间提供无环路的路由信息交换&#xff0c;BGP则是EGP的一种。 BGP是一…

前端铺子-NodeJS后端:基于Node.js构建高效后端服务的探索与实践

一、引言 随着前端技术的快速发展&#xff0c;越来越多的开发者开始关注前后端分离的开发模式。前端铺子作为一个旨在服务前端开发者的开源项目&#xff0c;近期推出了基于Node.js的后端系统。该系统通过整合Node.js、Nodemon和MySQL等技术&#xff0c;为前端开发者提供了一个…

Python-VBA函数之旅-tuple函数

目录 一、tuple函数的常见应用场景 二、tuple函数使用注意事项 三、如何用好tuple函数&#xff1f; 1、tuple函数&#xff1a; 1-1、Python&#xff1a; 1-2、VBA&#xff1a; 2、推荐阅读&#xff1a; 个人主页&#xff1a; https://myelsa1024.blog.csdn.net/ 一、tu…

钡铼技术BL205模块分布式IO集成应用风电场状态监测

在风力发电这一绿色能源领域&#xff0c;高效、精确的状态监测对于提升风电场运维效率、保障设备安全运行至关重要。随着工业4.0和数字化转型浪潮的推进&#xff0c;传统的监测方式已难以满足日益增长的数据处理与分析需求。钡铼技术BL205模块的出现&#xff0c;为风电场状态监…

[图解]实现领域驱动设计译文暴露的问题04

0 00:00:00,960 --> 00:00:03,020 今天我们继续说一下 1 00:00:03,460 --> 00:00:05,350 实现领域驱动设计 2 00:00:05,630 --> 00:00:08,120 译文里面暴露的问题 3 00:00:10,630 --> 00:00:14,740 前面三个视频&#xff0c;我们提到了第①句 4 00:00:15,550 -…

【算法提升之赛事推荐】蓝桥杯没拿奖?你还有这个比赛的羊毛可以薅

目录 蓝桥算法双周赛-赛事介绍&#xff08;[官方连接](https://www.lanqiao.cn/oj-contest/)&#xff09;赛事奖励赛事概览快速上手划重点&#xff01;1. 赛题解析及答疑2. 排行榜3. 基础体验 注意事项 蓝桥算法双周赛-赛事介绍&#xff08;官方连接&#xff09; 为了激励同学们…

Django自定义封装Response

Django自定义封装Response 目录 Django自定义封装Response定义Response类视图层示例 定义Response类 # utils/common_response.py from rest_framework.response import Response# 继承DRF的Response并重写__init__ class APIResponse(Response):def __init__(self, code100, …