【Java多线程进阶】JUC常见类以及CAS机制

1. Callable的用法

之前已经接触过了Runnable接口,即我们可以使用实现Runnable接口的方式创建一个线程,而Callable也是一个interface,我们也可以用Callable来创建一个线程。

  • Callable是一个带有泛型的interface
  • 实现Callable接口必须重写call方法
  • call方法带有返回值,且返回值类型就是泛型类型
  • 可以借助FutureTask类接收返回值,更方便的帮助程序员完成计算任务并获取结果

下面我们来举一个案例,要求分别使用实现Runnable接口和Callable接口完成计算1+2+3+…1000并在主线程中打印结果的任务,体会区别。

1.1 使用Runnable接口

public class ThreadDemo01 {
    private static int result = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 1000; i++) {
                    result += i;
                }
            }
        });
        // 启动线程
        t.start();
        t.join();
        // 打印结果
        System.out.println("result: " + result); // 500500
    }
}

上述代码实现Runnable接口来启动一个线程,这段代码可以实现累加1-1000的任务,但是不够优雅!因为需要额外借助一个成员变量result来保存结果,当业务量繁多时,如果有多个线程完成各自的计算任务,那么就需要更多的成员变量保存结果,因此现在想想带有返回值的线程也许是有必要的!下面我们来看看如何使用Callable接口完成

1.2 使用Callable接口

public class ThreadDemo02 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for (int i = 1; i <= 1000; i++) {
                    result += i;
                }
                return result;
            }
        };
        // 创建FutureTask实例
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        // 创建线程并启动
        Thread t = new Thread(futureTask);
        t.start();
        // 阻塞并获取返回结果
        int result = futureTask.get();
        System.out.println("result: " + result); // 500500
    }
}

上述代码中我们使用了实现Callable的方式完成任务,值得一提的是,Callable实现类对象不能直接作为Thread构造方法的参数,我们需要借助一个中间人即 FutureTask 类,该类实现了Runnable接口,因此可以直接作为Thread构造方法的参数。

注:futureTask.get()方法带有阻塞功能,直到子线程完成返回结果才会继续运行

1.3 如何理解FutureTask和Callable

理解Callable:1、Callable与Runnable是相对的,都是描述一个任务,Callable描述带有返回值的任务,Runnable描述不带返回值的任务。2、Callable往往需要搭配FutureTask一起使用,FutureTask用于保存Callable的返回结果,因为Callable中的任务往往在另一个线程执行,具体什么时候执行并不确定
理解FutureTask:FutureTask顾名思义就是未来任务,即Callable中的任务是不确定何时执行完毕的,我们可以形象描述为去杨国福吃麻辣烫时等待叫号,但是什么时候叫号是不确定的,通常点餐完毕后服务员会给一个取号凭证,我们可以凭借这个取号凭证查看自己的麻辣烫有没有做好。

1.4 相关面试题

  1. 介绍下Callable是什么?

2. 信号量Semaphore

相信大学期间修过操作系统课的小伙伴对semaphore这个单词并不陌生,这不就是OS的信号量机制可以实现PV操作的么?而JVM对OS提供的semaphore又进行了封装形成了Java标准库中的Semaphore
semaphore:信号量,用来表示可用资源的数目,本质上是一个计数器。

我们可以拿停车场的展示牌进行举例,当前留有空闲车位100个时,展示牌上就会显示100,表示可用资源的数目,当有汽车开进停车位时,相当于申请了一个可用资源,此时展示牌数目就会-1(称为信号量的P操作),当有汽车驶出停车位,相当于释放了一个可用资源,此时展示牌数目+1(称为信号量V操作)

Semaphore的信号量PV操作都是原子性的,可以直接在多线程环境中使用。

锁其实本质上就是特殊的信号量,加锁可以看作时信号量为0,解锁可以看作信号量为1,所以也有说法称"锁"其实是一个二元信号量,那么既然"锁机制"能够实现线程安全,信号量也可以用来保证线程安全!

2.1 Semaphore代码案例

Semaphore相关API:

  1. acquire:相当于P操作,申请一个可用资源
  2. release:相当于V操作,释放一个可用资源

代码举例:

  1. 创建Semaphore实例并初始化为4,表示有4个可用资源
  2. 创建20个线程,每个线程都尝试申请资源,sleep1秒后释放资源,观察程序运行状况
public class SemaphoreExample {
    public static void main(String[] args) {
        // 初始化信号量为4
        Semaphore semaphore = new Semaphore(4);
        // 创建20个线程
        for (int i = 0; i < 20; i++) {
            int id = i;
            Thread t = new Thread(() -> {
                try {
                    semaphore.acquire(); // 申请资源
                    System.out.println("线程" + id + "申请到了资源");
                    Thread.sleep(1000); // 睡眠1s
                    semaphore.release(); // 释放资源
                    System.out.println("线程" + id + "释放资源");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            // 启动线程
            t.start();
        }
    }
}

执行结果
image.png
其中可以发现当前四个线程申请资源后,此时第五个线程尝试申请资源后,信号量变为-1就会阻塞等待,直到其他线程释放资源后才可以继续申请!

3. CountDownLatch的使用

CountDownLatch:可以等待N个任务全部完成。类似于N个人进行跑步比赛,直到最后一个人跃过终点才会宣布比赛结束,公布最后成绩。

3.1 CountDownLatch相关API

CountDownLatch提供API:

  1. new CountDownLatch(int n):构造方法,初始化n表示有n个任务需要完成
  2. countDown():任务执行完毕后调用,内部计数器进行自减
  3. await():阻塞等待所有任务全部完成后继续执行,相当于等待内部计数器为0

3.2 CountDownLatch代码案例

CountDownLatch代码案例:

  1. 创建10个线程,并初始化CountDownLatch为10
  2. 每个线程随机休眠1-5秒,模拟比赛结束
  3. 主线程中使用await阻塞等待全部执行完毕
public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        Random random = new Random();
        // 初始化CountDownLatch为10
        CountDownLatch latch = new CountDownLatch(10);
        // 创建10个线程
        for (int i = 0; i < 10; i++) {
            int curId = i;
            Thread t = new Thread(() -> {
                System.out.println("线程" + curId + "开始执行...");
                try {
                    Thread.sleep(random.nextInt(6) * 1000);
                    System.out.println("线程" + curId + "结束执行...");
                    latch.countDown(); // 计数器自减
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            // 启动线程
            t.start();
        }
        // 阻塞等待全部执行完毕
        latch.await();
        System.out.println("全部任务执行完毕!");
    }
}

执行结果
image.png
此时我们可以看到调用countDownLatch.await()方法时只有当所有的线程都执行完毕,即调用10次countDown方法后(即内部计数器为0)时才会停止阻塞!

4. ReentrantLock类

ReentrantLock:位于java.util.concurrent包下,被称为可重入互斥锁,和synchronized定位类似,都是用来实现互斥效果的可重入锁,保证线程安全。

4.1 ReentrantLock的用法

ReentrantLock相关API:

  1. lock():尝试加锁,如果加锁失败就阻塞等待
  2. tryLock(超时时间):尝试加锁,获取不到就阻塞等待一定时间,超时则放弃加锁
  3. unlock():解锁

注意:由于ReentrantLock需要手动释放锁,因此常常把unlock方法放在finally块中

ReentrantLock lock = new ReentrantLock();
--------------  相关代码  --------------
lock.lock();
try {
    // do something...
} finally {
    lock.unlock();
}

4.2 ReentrantLock与synchronized的区别(经典面试题)

  1. synchronized是一个关键字,是JVM内部实现的(大概率使用C++语言实现),而ReentrantLock是一个标准库中提供的类,是在JVM外部实现的(基于Java实现)
  2. ReentrantLock使用lock和unlock一对方法进行手动加锁、解锁,相较于synchronized更加灵活,但是也容易遗漏释放锁的步骤。
  3. synchronized是非公平锁,而ReentrantLock默认是非公平锁,但是可以变成公平锁,只需要在构造方法中传入参数为true即可。
  4. ReentrantLock比synchronized具有更加精准的唤醒机制,synchronized使用Object类的wait/notify进行唤醒,随机唤醒一个等待线程,而ReentrantLock借助类Condition实现等待唤醒,可以精准控制唤醒某一个指定线程。

5. CAS机制

5.1 CAS的基本概念

CAS:全程为compare and swap字面意思就是比较并且交换,一个CAS涉及以下操作:

我们假设内存中的原数据为V,旧的预期值为A,需要修改的新值为B

  1. 比较A与V是否相等(比较)
  2. 如果比较相等则将B写入内存替换V(交换)
  3. 返回操作是否成功。

CAS伪代码:

public boolean CAS(address, expectValue, swapValue) {
    if (&address == expectValue) {
        &address = swapValue;
        return true;
    }
    return false;
}

注意:CAS是一个硬件指令,其操作是原子性的,这个伪代码只是辅助理解CAS的工作流程

对CAS原子性的理解:CAS可以看做是乐观锁的一种实现方式,当多个线程同时对某个资源进行CAS操作时,只要一个线程操作成功返回true,其余线程全部返回false,但是不会阻塞。

5.2 CAS的常见应用

5.2.1 实现原子类

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

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

下面是AtomicInteger类基于CAS方式的伪代码实现:

class AtomicInteger {
    private int value;

    public int getAndIncrement() {
        int oldValue = value;
        while (CAS(value, oldValue, oldvalue + 1) != true) {
            oldValue = value;
        }
        return oldValue;
    }
}

5.2.2 实现自旋锁

基于CAS也可以实现更加灵活的锁,获取到更多的控制权
下面是基于CAS的自旋锁伪代码实现:

class SpinLock {
    private Thread owner;

    public void lock() {
        while (!CAS(owner, null, Thread.currentThread())) {}
    }

    public void unlock() {
        this.owner = null;
    }
}

5.3 CAS的ABA问题

5.3.1 ABA问题概述

什么是ABA问题:假设现在有两个线程共用一个共享变量num,初始值为A,此时线程t1,期望使用CAS机制将num值修改为Z,线程t1的CAS判定流程如下:

  • 如果此时num == A,那么就将内存中的num值修改为Z
  • 如果此时num != A,那么就不进行修改,重新判定

但是此时中间穿插了线程t2执行将num值修改为了B,但是又重新修改为了A,所以尽管t1线程比较的预期结果是一致的,但是很可能已经是别人的形状了!
image.png

ABA导致的问题

ABA问题有可能会导致严重的后果,比如说我去银行ATM机取钱,考虑采用的时CAS机制,目前我的账户余额为1500,我按下确定按钮想要取出500元钱,但是ATM机卡住了,因此我又按下一次确定按钮,此时正常情况如下:
正常情况
image.png
此时结果是符合预期的!扣款线程t1进行扣款操作,此时余额变为1000元,扣款线程t2判断当前余额已经不为1500了,说明已经扣款成功!于是不进行后续扣款操作!但是如果中间出现其他线程汇款操作,就会出现ABA问题,导致严重后果!
极端情况
image.png
此时扣款线程t1完成扣款操作后,余额变为1000,但是中间穿插了汇款线程t3,刚好往账户中存入金额500,此时扣款线程t3判断余额仍然为1500,因此又进行了多余的一次扣款操作!!!这是相当不合理的

5.3.2 ABA问题解决思路

为了解决ABA带来的问题,我们可以考虑使用 版本号 的思想解决:

  • CAS读取数据的时候,同时也要读取版本号
  • 如果当前版本号与读取版本号一致,修改数据同时将版本号+1
  • 如果当前版本号高于读取版本号,则说明数据已经被修改,当前操作非法

6. 线程安全的集合类

Java提供的集合类大部分都是线程不安全的

Vector,Stack,HashTable是线程安全的(但是官方不推荐使用)

6.1 多线程环境使用ArrayList

  1. 自己加锁(使用synchronized或者ReentrantLock实现)
  2. 使用Collections.synchronizedList,这是标准库提供的基于synchronized实现的线程安全的类,本质上是在关键方法上加了synchronized修饰
  3. 使用CopyOnWriteArrayList,这是一个借助写时拷贝机制实现的容器,常用于配置文件等不经常修改,占用内存较小等场景

写时拷贝机制的核心就是可以对原容器进行并发的读,涉及写操作则先对原容器进行拷贝,然后向新容器中添加元素,最后修改引用,实现了读写分离!

6.2 多线程环境使用队列

  1. 自己加锁实现(synchronized或者ReentrantLock实现)
  2. 使用标准库提供的BlockingQueue接口及其实现类

6.3 多线程环境使用哈希表

  1. 使用HashTable

    只是在关键方法上加上synchronized进行修饰

  2. ConcurrentHashMap(常考面试题)

    相比于Hashtable做出了一系列优化(以JDK1.8为例)

    • 优化方式一:读操作没有加锁,只有写操作才会加锁,加锁的方式仍然使用synchronized,但是并不是锁整个对象,而是锁"桶"(使用链表的头结点作为锁对象),大大降低了锁冲突的概率
    • 优化方式二:对于一些变量例如哈希表元素个数size,使用CAS机制避免重量级锁出现
    • 优化方式三:优化了扩容方式,采用"化整为零","蚂蚁搬家"的方式,扩容期间新老数组同时存在,一次搬运只搬运少量元素,因此新增操作只需要在新数组中插入即可,查询操作需要在新老数组同时查询,删除操作也需要新老数组同时删除

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

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

相关文章

GEE使用 Sentinel-1 SAR影像 和 Otsu 方法绘制洪水地图

洪水是世界上最常见、破坏性最大的自然灾害之一,造成了巨大的生命和财产损失。此外,随着气候变化的影响,近年来,洪灾变得更加频繁和不可预测。为了最大限度地减少生命和财产损失,必须迅速发现洪水蔓延的情况,并及时采取必要的干预措施。洪水蔓延探测大多使用光学传感器或…

【力扣】169.多数元素

这道题的解法是运用哈希表打擂台的思想 首先题目的意思是存在数字&#xff0c;意思就是最后返回的结果不可能为空就是了&#xff0c;所以便不用考虑{1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5}这种例子。那么就可以用哈希表存所出现数字出现的次数&#xff0c;然…

文生图提示词:天气条件

天气和气候 --天气条件 Weather Conditions 涵盖了从基本的天气类型到复杂的气象现象&#xff0c;为描述不同的天气和气候条件提供了丰富的词汇。 Sunny 晴朗 Cloudy 多云 Overcast 阴天 Partly Cloudy 局部多云 Clear 清晰 Foggy 雾 Misty 薄雾 Hazy 朦胧 Rainy 下雨 Showers …

EasyUI动态加载组件

要实现如下的效果&#xff0c;在表格中显示进度条 主要是需要再次初始化组件&#xff0c;借用ChatGPT的意思是&#xff1a; 在许多 JavaScript UI 框架中&#xff0c;包括 EasyUI&#xff0c;在动态地创建或插入新的 DOM 元素后&#xff0c;通常需要手动初始化相关的组件或特性…

视觉设计师的项目评审复盘攻略:如何提升设计质量与效率

视觉设计师的角色是至关重要的&#xff0c;以确保设计项目满足预期的质量和结果。作为一名视觉设计师&#xff0c;有必要进行定期的项目审查&#xff0c;以确保项目在正轨上进行&#xff0c;并尽早解决任何问题。在本文中我们将讨论可视化设计人员如何做好项目评审&#xff0c;…

【Anaconda】conda创建、删除、查看虚拟环境,安装pytorch

1.删除环境 首先退出现有的环境 conda deactivate然后查看要删除的环境名称与路径 conda env list接下来就可以删除环境了 有两种方法 方法1&#xff1a; conda env remove -p 要删除的虚拟环境路径对我来说就是&#xff1a; conda env remove -p D:\Anaconda3\envs\MVDet…

家庭动态网络怎么在公网访问主机数据?--DDNS配置(动态域名解析配置)

前言 Dynamic DNS是一个DNS服务。当您的设备IP地址被互联网服务提供商动态变更时,它提供选项来自动变更一个或多个DNS记录的IP地址。 此服务在技术术语上也被称作DDNS或是Dyn DNS 如果您没有一个静态IP,那么每次您重新连接到互联网是IP都会改变。为了避免每次IP变化时手动更…

BulingBuling - 《研究巴菲特》 [ Buffettology ]

研究巴菲特 使沃伦-巴菲特成为世界上最著名的投资者的那些以前未曾解释过的技术 作者&#xff1a;玛丽-巴菲特 Buffettology The Previously Unexplained Techniques That Have Made Warren Buffett The Worlds Most Famous Investor By Mary Buffett 内容提要 《Buffetto…

mysql数据库 mvcc

在看MVCC之前我们先补充些基础内容&#xff0c;首先来看下事务的ACID和数据的总体运行流程 数据库整体的使用流程: ACID流程图 mysql核心日志: 在MySQL数据库中有三个非常重要的日志binlog,undolog,redolog. mvcc概念介绍&#xff1a; MVCC&#xff08;Multi-Version Concurr…

高效宣讲管理:Java+SpringBoot实战

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

Ubuntu Desktop - Details (设备详情)

Ubuntu Desktop - Details [设备详情] 1. OverviewReferences 1. Overview System Settings -> Details -> Overview ​ References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.net/

Code Composer Studio (CCS) - 文件比较

Code Composer Studio [CCS] - 文件比较 References 鼠标单击选中一个文件&#xff0c;再同时按住 Ctrl 鼠标左键来选中第二个文件&#xff0c;在其中一个文件上鼠标右击选择 Compare With -> Each Other. References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.n…

[VulnHub靶机渗透] Fowsniff

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【python】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏…

使用cockpit安装kvm虚拟机

下载管理虚拟机的插件 如果安装完成之后&#xff0c;出现报错&#xff0c;则刷新。如下图所示 添加虚拟网桥 进入添加网桥之后&#xff0c;名称自己修改&#xff0c;端口设置为自己的网卡名称。 之后返回xshell之后再次查看ip地址就会出现 添加镜像到物理机的根目录下 将系统…

智胜未来,新时代IT技术人风口攻略-第四版(弃稿)

文章目录 前言鸿蒙生态科普调研人员画像高校助力鸿蒙高校鸿蒙课程开设占比教研力量并非唯一原因 企业布局规划全盘接纳仍需一段时间企业对鸿蒙的一些诉求 机构入场红利机构鸿蒙课程开设占比机构对鸿蒙的一些诉求 鸿蒙实际体验高校用户群体高度认同与影响体验企业用户群体未来可…

深入解析Mybatis-Plus框架:简化Java持久层开发(二)

&#x1f340; 前言 博客地址&#xff1a; CSDN&#xff1a;https://blog.csdn.net/powerbiubiu &#x1f44b; 简介 本章节开始从实际的应用场景&#xff0c;来讲解Mybatis-Plus常用的一些操作&#xff0c;根据业务场景来进行增删改查的功能&#xff0c;首先先搭建一个项目…

2024年上班族最适合做的副业:抖音小店,门槛真低!

大家好&#xff0c;我是电商糖果 搞钱难&#xff0c;做副业搞钱更难&#xff01; 作为普通上班族&#xff0c;一个人的薪资很难养活一个家庭&#xff0c;于是越来越多人开始琢磨副业。 而2024年最适合上班族的副业&#xff0c;真的是非抖音小店莫属了。 这两年抖音购物已经…

java.lang.NoClassDefFoundError: org/springframework/core/GenericTypeResolver

前言 小编我将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识&#xff0c;有兴趣的小伙伴可以关注一下&#xff01; 也许一个人独行&#xff0c;可以走的很快&#xff0c;但是一群人结伴而行&#xff0c;才能走的更远&#xff01;让我们在成长的道路上互相学习&…

Java实现新能源电池回收系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用户档案模块2.2 电池品类模块2.3 回收机构模块2.4 电池订单模块2.5 客服咨询模块 三、系统设计3.1 用例设计3.2 业务流程设计3.3 E-R 图设计 四、系统展示五、核心代码5.1 增改电池类型5.2 查询电池品类5.3 查询电池回…

485. Max Consecutive Ones(最大连续 1 的个数)

问题描述 给定一个二进制数组 nums &#xff0c; 计算其中最大连续 1 的个数。 问题分析 因为nums中只有1与0两种字符&#xff0c;我们可以设计一个统计变量来统计某一段中1出现的次数&#xff0c;因为当1后面跟着一个0时意味着这一段1结束&#xff0c;由此可以实现统计1的数…