JUC(java.util.concurrent) 的常见类

Callable 接口

Callable 的用法

        Callable 是一个 interface(类似之前的 Runnable,用来描述一个任务,但是没有返回值)也是描述一个任务的,有返回值。方便程序猿借助多线程的方式计算结果.

例如:创建线程计算 1 + 2 + 3 + ... + 1000, 不使用 Callable 

        创建一个类 Result , 包含一个 sum 表示最终结果, lock 表示线程同步使用的锁对象. main 方法中先创建 Result 实例, 然后创建一个线程 t. 在线程内部计算 1 + 2 + 3 + ... + 1000. 主线程同时使用 wait 等待线程 t 计算结束. (注意, 如果执行到 wait 之前, 线程 t 已经计算完了, 就不必等待了). 当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果.

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);
  }
}

使用 callable 

        创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型. 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果. 把 callable 实例使用 FutureTask 包装一下. 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行FutureTask 内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中. 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.

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

public class ThreadDemo29 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 使用 Callable 来计算 1 + 2 + 3 + ... + 1000
        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<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();

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

理解 Callable

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

Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.

FutureTask 就可以负责这个等待结果出来的工作.

理解 FutureTask

        想象去吃麻辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你一张 "小票" . 这个小票就是

FutureTask. 后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没.

相关面试题

介绍下 Callable 是什么

Callable 是一个 interface . 相当于把线程封装了一个 "返回值". 方便程序猿借助多线程的方式计算结果.

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

Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.

FutureTask 就可以负责这个等待结果出来的工作.

ReentrantLock

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.

ReentrantLock 的用法:

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

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

unlock():解锁

ReentrantLock 和 synchronized 的区别:

        synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现)。

        synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock。

        synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间还是获取不到就放弃。

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

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

import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo30 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock(true); //输入 true 就会变成公平锁
        //reentrantLock 返回值是是否加上了锁
        boolean result = reentrantLock.tryLock(); //括号内可以填参数,表示最大等待时间,如果超过了就放弃锁
        try {
            
        } finally {
            if (result) {
                reentrantLock.unlock();
            }
        }

    }
}

如何选择使用哪个锁?

锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.

锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.

如果需要使用公平锁, 使用 ReentrantLock.

原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个

AtomicBoolean

AtomicInteger

AtomicIntegerArray

AtomicLong

AtomicReference

AtomicStampedReference

以 AtomicInteger 举例,常见方法有

 

线程池

        虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效. 线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 "池子"中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.

ExecutorService Executors

代码示例:

ExecutorService 表示一个线程池实例.

Executors 是一个工厂类, 能够创建出几种不同风格的线程池.

ExecutorService 的 submit 方法能够向线程池中提交若干个任务.

ExecutorService pool = Executors.newFixedThreadPool(10); 
pool.submit(new Runnable() {
    @Override
    public void run() { 
        System.out.println("hello");
    }
});

Executors 创建线程池的几种方式

        newFixedThreadPool: 创建固定线程数的线程池

        newCachedThreadPool: 创建线程数目动态增长的线程池.

        newSingleThreadExecutor: 创建只包含单个线程的线程池.

        newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

Executors 本质上是 ThreadPoolExecutor 类的封装.

ThreadPoolExecutor

ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定.

ThreadPoolExecutor 的构造方法

理解 ThreadPoolExecutor 构造方法的参数

把创建一个线程池想象成开个公司. 每个员工相当于一个线程.

        corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)

        maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).

        keepAliveTime: 临时工允许的空闲时间.

        unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.

        workQueue: 传递任务的阻塞队列

        threadFactory: 创建线程的工厂, 参与具体的创建线程工作.

        RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.

        AbortPolicy(): 超过负荷, 直接抛出异常.

        CallerRunsPolicy(): 调用者负责处理

        DiscardOldestPolicy(): 丢弃队列中最老的任务.

        DiscardPolicy(): 丢弃新来的任务.

线程池的工作流程:

信号量 Semaphore

信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器.

        可以把信号量想象成是图书馆中某一本书的在馆数: 当前在馆 100 本. 表示有 100 个可用资源。当有书接进去的时候, 就相当于申请一个可用资源, 可用在馆数就 -1 (这个称为信号量的 P 操作) 当有人还书的时候, 就相当于释放一个可用资源, 可用在馆数就 +1 (这个称为信号量的 V 操作)如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

        锁,可以视为是计数器为 1 的信号量,只有 0 和 1 这两种取值(二元信号量)。

Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.

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

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

 具体的使用:

import java.util.concurrent.Semaphore;

public class ThreadDemo31 {
    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 操作");

        semaphore.release();
    }
}

CountDownLatch

        同时等待 N 个任务执行结束. 好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能比赛结束。

为了做到等待所有都到达终点,主要是采用两种方法:

1. await(a → all),主线程调用这个方法

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

        CountDownLatch 在构造的时候,会指定一个计数(选手的个数)。就像上述十位选手进行比赛,初始情况下会调用 await 然后就会阻塞,每个选手冲过终点都会调用 countDown 方法,前 9 次调用 countDown,await 没有任何影响,第十次调用的时候,await 就会被唤醒,然后返回解除阻塞。此时就认为整个比赛都结束了。

相关面试题

1) 线程同步的方式有哪些?

synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.

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

以 juc 的 ReentrantLock 为例,

        synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.

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

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

3) AtomicInteger 的实现原理是什么?

基于 CAS 机制. 伪代码如下:

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

4) 信号量听说过么?之前都用在过哪些场景下?

        信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器.

        使用信号量可以实现 "共享锁", 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待,直到前面的线程执行了 V 操作.

  • 41
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的JUCjava.util.concurrent)包提供了一些并发编程中常用的,这些可以帮助我们更方便地实现多线程编程。以下是一些常用的JUC及其解析: 1. CountDownLatch(倒计时器) CountDownLatch是一个计数器,它允许一个或多个线程等待一组事件发生后再继续执行。它最基本的方法是await()和countDown()。await()方法会阻塞当前线程,直到计数器的值为0;countDown()方法会将计数器的值减1。 2. CyclicBarrier(循环屏障) CyclicBarrier是一个同步工具,它允许一组线程等待彼此达到一个公共屏障点。当所有线程都到达这个屏障点时,它们才能继续执行。CyclicBarrier可以被重复使用,当所有线程都执行完后,它会自动重置。 3. Semaphore(信号量) Semaphore是一种计数器,它维护了一组许可证。当调用acquire()方法时,线程会阻塞,直到许可证可用;而当调用release()方法时,许可证的数量会增加。Semaphore可以用于限制同时访问某些资源的线程数量。 4. ReentrantLock(重入锁) ReentrantLock是一个可重入的互斥锁。它和synchronized关键字似,但是提供了更多的灵活性和功能。ReentrantLock中最常用的方法是lock()和unlock(),它们分别用于获取锁和释放锁。 5. ConcurrentHashMap(并发哈希表) ConcurrentHashMap是一个线程安全的哈希表实现。它和HashMap似,但是支持并发访问。ConcurrentHashMap中的所有方法都是线程安全的,而且它的性能比Hashtable和同步的HashMap要好。 6. Executors(线程池) Executors是一个工厂,用于创建各种型的线程池。它提供了一些静态方法,例如newFixedThreadPool()、newCachedThreadPool()、newSingleThreadExecutor()等,可以方便地创建各种型的线程池。 7. Future(异步计算) Future是一个接口,它表示一个异步计算的结果。Future可以通过get()方法获取计算结果,或者通过cancel()方法取消计算。Future还可以用于实现一些高级的并发操作,例如等待一组异步计算全部完成后再继续执行。 总的来说,JUC包提供了很多有用的和工具,可以帮助我们更方便地实现多线程编程,提高程序的并发性能和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值