锋盈数科-知识库 Logo
首页
软件开发
计算机基础
Hello Halo
新手必读
关于本知识库
登录 →
锋盈数科-知识库 Logo
首页 软件开发 计算机基础 Hello Halo 新手必读 关于本知识库
登录
  1. 首页
  2. 软件开发
  3. Callable接口_JUC的常见类_多线程环境使用ArrayList

Callable接口_JUC的常见类_多线程环境使用ArrayList

0
  • 软件开发
  • 发布于 2024-09-19
  • 0 次阅读
黄健
黄健


目录

1.Callable接口

相关面试题

2.ReentrantLock

相关面试题

3.信号量Semaphore

4.CountDownLatch

5.多线程环境使用ArrayList

热加载



1.Callable接口

Callable是一个接口,把线程封装了一个"返回值”,方便程序员借助多线程的方式计算结果.

类似于Runnable,是用来描述一个任务,区别就是Runnable描述的任务没有返回值,而Callable描述的任务有返回值

看这个例子

创建线程计算 1 + 2 + 3 + … + 1000, 不使用 Callable 版本

public class Test {
    static class Result {
        public int sum = 0;
        public Object lock = new Object();
    }
    public static void main(String[] args) throws InterruptedException {
        Result result = new Result();
        Thread t = new Thread() {
            @Override
            public void run() {
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                synchronized (result.lock) {
                    result.sum = sum;
                    result.lock.notify();
                }
            }
        };
        t.start();
        synchronized (result.lock) {
            while (result.sum == 0) {
                result.lock.wait();
            }
            System.out.println(result.sum);
        }
    }

}

使用Runnable来描述任务,没有返回值,这里使用了一个辅助类result,还需要使用一些加锁和wait,notify操作,比较复杂易出错

我们使用Callable来解决这个问题

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 = 1; i <= 1000 ; i++) {
                    sum+=i;
                }
                return sum;
            }
        };
创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结
果. 

        Integer result = futureTask.get();
        System.out.println(result);
    }

call方法就相当于Runnable的run方法,只不过call方法会返回一个泛型返回值,run返回void

创建好任务后,需要一个线程来执行,但是这里不能把callable直接传入Thread的构造方法中,需要给它套上一个辅助类

FutureTask实现了RunnableFuture接口,RunnableFuture接口继承了Runnable, Future

public class FutureTask<V> implements RunnableFuture<V>{
        ...
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

FutureTask既是Runnable对象,也是Future对象。Future是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果、设置结果操作,get方法会阻塞,直到任务返回结果

get方法就是获取结果.get会发生阻塞,直到callable执行完毕,get才阻塞完成,获取到结果

Callable也是创建线程的一种方式

相关面试题

介绍下 Callable 是什么

Callable是一个接口,相当于把线程封装了一个"返回值”,方便程序员借助多线程方式计算结果

Callable和Runnable相对,都描述一个任务,Callable描述的是带有返回值的任务,Runnable描述的是不带返回值的任务

Callable通常搭配FutureTask使用,FutureTask用来保存Callable的返回结果,因为Callable往往是在另一个线程中执行的,不知道具体执行完的时间,FutureTask就是负责等待结果出来并保存的

JUC的常见类:

2.ReentrantLock

ReentrantLock是标准库提供的另一种锁,是可重入锁

synchronized是基于代码块的方式加锁解锁的

ReentrantLock比较传统,使用了lock和unlock方式加锁解锁
ReentrantLock 的用法:

lock(): 加锁, 如果获取不到锁就死等.

trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.

unlock(): 解锁

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

        reentrantLock.unlock();
    }

在lock和unlock之间就是加锁的部分,但是这样写有可能unlock执行不到,加锁的代码块中间如果存在return或者异常都可能导致unlock不能顺利进行

或者在return之前加上unlock,但是如果这种条件语句很多,那么就会很麻烦,此时用finally来解决

上述是ReentrantLock的劣势,但是它也是有优势的

1.ReentrantLock提供了公平锁版本的实现

2.synchronized提供的加锁操作是"死等”,只要获取不到锁就一直等待,ReentrantLock提供了更灵活的等待方式:tryLock

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

有参数版本:指定了超时时间,加不上锁就等待,如果超时了还没获取到锁就放弃

3.ReentrantLock提供了一个更强大,更方便的等待通知机制.

synchronized搭配的是waitnotify,notify的时候是随机唤醒一个wait的线程

ReentrantLock搭配一个Condition类,进行唤醒的时候可以唤醒指定的线程

虽然ReentrantLock有一定的优势,但是实际开发常用的是synchronized

相关面试题

为什么有了 synchronized 还需要 juc 下的 lock?

synchronized使用时不需要手动释放锁,ReentrantLock 使用时需要手动释放. 使用起来更 灵活

synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式

synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的 线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程

3.信号量Semaphore

操作系统中的信号量和此处的信号量是相同的,只不过此处的信号量是Java把操作系统原生的信号量封装了一下

信号量本质是一个"计数器”,用于描述可用资源的个数

就相当于停车场的入口的显示牌,有多少个车位可用!如果车位满了,那么牌子就是显示可用为0.那么再想要停车,就只能换地方,或者等待.

围绕这个计数器有两个操作:

P操作:申请一个可用资源,计数器就-1

V操作:释放一个可用资源,计数器就+1

P操作如果要是计数器为0了,继续P操作,就会阻塞等待

P操作使用acquire申请

V操作使用release释放

考虑一个计数初始值为1的信号量,针对这个值,只能有1和0两个取值,不会是负值.

如果执行P操作,1->0

如果执行V操作,0->1

如果进行一次P操作了,再进行一次P操作,就会阻塞等待

那么根据这个特性我们很容易联想到锁(锁可以视为计数器为1的信号量,有两个取值,称为二元信号量)

锁是信号量的一种特殊情况,信号量是锁的一般表达

实际开发中信号量也会用到,比如说,图书馆中有某本书有20本,那么就可以用初始值为20的信号量.借书就P操作,还书就V操作.如果书为0了,再P操作就会阻塞等待!

public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        semaphore.acquire();
        System.out.println("第一次P操作");

        semaphore.acquire();
        System.out.println("第二次P操作");

        semaphore.acquire();
        System.out.println("第三次P操作");

        semaphore.acquire();
        System.out.println("第四次P操作");
    }

申请了3个可用资源,当申请第四个就会阻塞等待,等到别的线程调用release释放可用资源,才能停止阻塞

无参版本是申请一个资源,带参数版本可以指定申请的资源数

代码中也可以使用Semaphore来实现类似于锁的功能,保证线程安全

4.CountDownLatch

同时等待 N 个任务执行结束

类似于跑步比赛,多个选手都就位,哨声响才同时出发,所有选手都通过终点,才公布成绩

跑步比赛开始时间都是相同的,但是结束时间是不明确的,为了衡量这个时间引入了CountDownLatch,主要提供了两个方法

1.await 方法,主线程来调用该方法,很多"a"为前缀的术语都表示异步.

同步和异步是相对的,同步:发送请求方自己主动等待响应结果

异步:发送请求方请求完就不管了,等有结果了,对方主动将结果推送出来

2.countDown表示冲过了终点线.

CountDown在构造的时候,指定一个计数器(选手的个数).例如,指定了四个选手进行比赛,初始情况下,调用await,就会阻塞,每个选手冲过终点后都会调用countDown方法,前三次调用countDown,await没有任何影响,第四次调用countDown,await就会被唤醒,返回,解除阻塞,此时认为整个比赛结束!

在开发中CountDownLatch也有很多应用场景,比如下载一个大的文件.使用多线程下载,把一个大的文件切分成多个小块儿的文件,安排多个线程分别下载,多线程下载,不是充分利用了多核CPU,而是充分利用了带宽(下载是IO操作,与CPU关系不大),此处就可以用CountDownLatch来区分是不是整体都下载完了

5.多线程环境使用ArrayList

Java标准库中的大部分集合都是"线程不安全"的,多个线程使用同一个集合类对象,很可能会出现问题

Vector, Stack, HashTable, 这几个集合是线程安全的,关键方法带有synchronized

多线程环境使用ArrayList:

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

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

3.CopyOnWriteArrayList

简称为"COW”,即"写时拷贝”,如果针对这个ArrayList进行读操作,不做任何额外的工作,如果进行写操作,则拷贝一份新的ArrayList,针对新的进行修改,修改过程中如果有读操作,就继续读旧的这份数据,当修改完毕了,使用新的替换旧的(本质上是一个引用之间的赋值,是原子的)

这种方案的优势很明显,就是不用加锁了,缺点也很明显,就是这个Array List不能太大了,只适用于这种数组比较小的情况下

热加载

我们知道服务器程序的配置与维护是需要修改配置文件的,修改配置后就需要重启才能生效,但是重启操作成本比较高,重启过程中服务就中断了,用户发送的请求就没有响应了,产生的后果是很不好的,因此,很多服务器提供了"热加载”(reload)这样的功能

“热加载"功能可以不重启服务器,实现服务器程序的配置的更新,热加载的实现就是用写时拷贝的思想,新的配置放到新的对象中,加载过程里,请求仍然基于旧的配置进行工作,当新的对象加载完毕,使用新的对象代替旧的对象(替换完成之后,旧的对象就可以释放了)

原文链接: https://blog.csdn.net/chenchenchencl/article/details/128682227

标签: #软件开发 1171
相关文章

万字:支付“核心系统”详解 2024-11-02 15:33

专栏作者:隐墨星辰 \| 主编:陈天宇宙 这篇文章也尝试化繁为简,探寻支付系统的本质,讲清楚在线支付系统最核心的一些概念和设计理念。 虽然支付行业已经过了风头最劲的时光,但跨境支付仍然在蓬勃发展,每年依然有很多新人进入这个行业,这篇文章尝试为这些刚入行的新人提供一点帮助。 文章只介绍一些支付行业十几

资深支付架构师视角:实战从问题定义到代码落地的完整套路 2024-11-02 15:33

前言 今天从一个实际案例入手,介绍站在架构师的角度,如何识别并定义问题,提炼需求,技术方案选型,再到详细设计,最后利用AI的能力协助写出核心的代码,验证与调优。 解决问题存在一定的模式,也可以称之为框架,总结出自己的思考和解题框架,以后再碰到同类型的问题就可以如庖丁解牛一样容易。 很多年前,我写代码

Spring 实现 3 种异步接口 2024-10-18 09:07

大家好,我是苏三~ 如何处理比较耗时的接口? 这题我熟,直接上异步接口,使用 Callable、WebAsyncTask 和 DeferredResult、CompletableFuture等均可实现。 但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地

重学SpringBoot3-集成Redis(五)之布隆过滤器 2024-10-08 11:24

更多SpringBoot3内容请关注我的专栏:《SpringBoot3》 期待您的点赞👍收藏⭐评论✍ 重学SpringBoot3-集成Redis(五)之布隆过滤器 1. 什么是布隆过滤器? * 基本概念 适用场景 2. 使用 Redis 实现布隆过滤器 * 项目依赖 Redis 配置

设计模式第16讲——迭代器模式(Iterator) 2024-10-08 11:24

一、什么是迭代器模式 迭代器模式是一种行为型设计模式,它提供了一种统一的方式来访问集合对象中的元素,而不是暴露集合内部的表示方式。简单地说,就是将遍历集合的责任封装到一个单独的对象中,我们可以按照特定的方式访问集合中的元素。 二、角色组成 抽象迭代器(Iterator):定义了遍历聚合对象所需的方法

vue2路由和vue3路由区别及原理 2024-10-08 11:24

一、Vue2 与 Vue3 路由的区别 1. 创建路由实例方式的不同 Vue 2 中,通过 Vue.use() 注册路由插件,并通过 new VueRouter() 来创建路由实例。 import Vue from 'vue';import VueRouter from 'vue-router';i

目录

IT 外包服务商

  • 意见投递
  • zyf6619

软件开发应用

主菜单

  • 首页
  • 软件开发
  • 计算机基础
  • Hello Halo
  • 新手必读
  • 关于本知识库
Copyright © 2024 your company All Rights Reserved. Powered by Halo.