当前位置: 首页 > news >正文

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

 

目录

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<V>接口,RunnableFuture<V>接口继承了Runnable, Future<V>

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)这样的功能

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

相关文章:

  • 如何给英文网站做外链/百度推广售后客服电话
  • wordpress 4.8 漏洞/深圳优化公司样高粱seo
  • 鞍山网上制作网站/北京优化靠谱的公司
  • 韩城建设局网站/什么是网络营销渠道
  • 二手网站建设方案/教育培训网
  • 南昌网站开发公司哪家公司好/软文发稿平台有哪些
  • SpringBoot数据访问Redis
  • 【数据结构】手撕八大排序算法
  • 生物信息数据存储、管理规范
  • RNN从理论到实战【实战篇】
  • 【第25天】SQL进阶-查询优化- performance_schema系列实战二:锁问题排查(全局读锁)(SQL 小虚竹)
  • C# 中的闭包一个小问题
  • 《Buildozer打包实战指南》第二节 安装Kivy和Buildozer
  • 达梦数据库导入dmp文件
  • linux基功系列之man帮助命令实战
  • Transformer模型详解相关了解
  • Eclipse 连接 SQL Server 数据库教程
  • 实时更新的github hosts地址