Java EE初阶启程记13---JUC(java.util.concurrent) 的常见类

🔥个人主页: 寻星探路

🎬作者简介:Java研发方向学习者

📖个人专栏:、《

⭐️人生格言:没有人生来就会编程,但我生来倔强!!!



目录

[一、Callable 接口](#一、Callable 接口)

二、ReentrantLock

1、原子类

2、线程池

[2.1ExecutorService 和 Executors](#2.1ExecutorService 和 Executors)

2.2ThreadPoolExecutor

2.3线程池的工作流程

3、信号量Semaphore

4、CountDownLatch

5、相关面试题


一、Callable 接口

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

代码示例:创建线程计算1+2+3+...+1000,不使用Callable版本

• 创建一个类Result,包含一个sum表示最终结果,lock表示线程同步使用的锁对象。

• main方法中先创建Result实例,然后创建一个线程 t ,在线程内部计算1+2+3+...+1000。

• 主线程同时使用wait等待线程 t 计算结束。(注意,如果执行到wait之前,线程 t 已经计算完了,就不必等待了)。

• 当线程 t 计算完毕后,通过notify唤醒主线程,主线程再打印结果。

java 复制代码
 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);
     }
 }

可以看到,上述代码需要一个辅助类Result,还需要使用一系列的加锁和waitnotify操作,代码复杂,容易出错。

代码示例:创建线程计算1+2+3+...+1000,使用Callable版本

• 创建一个匿名内部类,实现Callable接口,Callable带有泛型参数,泛型参数表示返回值的类型。

• 重写Callable的call方法,完成累加的过程,直接通过返回值返回计算结果。

• 把callable实例使用FutureTask包装一下。

• 创建线程,线程的构造方法传入FutureTask,此时新线程就会执行FutureTask内部的Callable的 call 方法,完成计算,计算结果就放到了FutureTask对象中。

• 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕,并获取到FutureTask中的结果。

java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
    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<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        int result = futureTask.get();
        System.out.println(result);
    }
}

可以看到,使用Callable和FutureTask之后,代码简化了很多,也不必手动写线程同步代码了。

理解Callable

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

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

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

理解FutureTask

想象去吃麻辣烫,当餐点好后,后厨就开始做了。同时前台会给你一张"小票",这个小票就是 FutureTask,后⾯我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没。

二、ReentrantLock

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

ReentrantLock 也是可重入锁,Reentrant这个单词的原意就是"可重入"。

ReentrantLock 的用法:

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

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

• unlock():解锁。

java 复制代码
ReentrantLock lock = new ReentrantLock();
----------------------------------------

lock.lock();   
try {    
    // working    
} finally {    
    lock.unlock()    
}  

ReentrantLock 和 synchronized 的区别:

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

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

• synchronized在申请锁失败时,会死等,ReentrantLock可以通过trylock的方式等待一段时间就放 弃。

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

java 复制代码
// ReentrantLock 的构造⽅法 
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

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

如何选择使用哪个锁?

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

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

• 如果需要使用公平锁,使用ReentrantLock。

1、原子类

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

• AtomicBoolean

• AtomicInteger

• AtomicIntegerArray

• AtomicLong

• AtomicReference

• AtomicStampedReference

以AtomicInteger 举例,常见方法有

java 复制代码
addAndGet(int delta);   i += delta;
decrementAndGet();                --i;
getAndDecrement();                i--;
incrementAndGet();                ++i;
getAndIncrement();                i++;

2、线程池

虽然创建销毁线程比创建销毁进程更轻量,但是在频繁创建销毁线程的时候还是会比较低效。

线程池就是为了解决这个问题,如果某个线程不再使用了,并不是真正把线程释放,而是放到一个"池子"中,下次如果需要用到线程就直接从池子中取,不必通过系统来创建了。

2.1ExecutorService 和 Executors

代码示例:

• ExecutorService 表示一个线程池实例。

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

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

java 复制代码
 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类的封装。

2.2ThreadPoolExecutor

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

ThreadPoolExecutor 的构造方法

**理解ThreadPoolExecutor构造方法的参数**

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

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

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

• keepAliveTime:临时工允许的空闲时间。

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

• workQueue:传递任务的阻塞队列。

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

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

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

◦ CallerRunsPolicy():调用者负责处理。

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

◦ DiscardPolicy():丢弃新来的任务。

代码示例:

java 复制代码
 ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
                                                 new SynchronousQueue<Runnable>(),
                                                 Executors.defaultThreadFactory(),
                                                 new ThreadPoolExecutor.AbortPolicy
 for(int i=0;i<3;i++) {
     pool.submit(new Runnable() {
         @Override
         void run() {
             System.out.println("hello");
         }
     });
 }

2.3线程池的工作流程

3、信号量Semaphore

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

理解信号量

可以把信号量想象成是停车场的展示牌:当前有车位100个,表示有100个可用资源。

当有车开进去的时候,就相当于申请一个可用资源,可用车位就-1(这个称为信号量的P操作)

当有车开出来的时候,就相当于释放一个可用资源,可用车位就+1(这个称为信号量的V操作)

如果计数器的值已经为0了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源。

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

代码示例

• 创建Semaphore示例,初始化为4,表示有4个可用资源。

• acquire方法表示申请资源(P操作),release方法表示释放资源(V操作)。

• 创建20个线程,每个线程都尝试申请资源,sleep1秒之后,释放资源,观察程序的执行效果。

java 复制代码
 Semaphore semaphore = new Semaphore(4);

 Runnable runnable = new Runnable() {
   @Override
   public void run() {
        try {
            System.out.println("申请资源");
            semaphore.acquire();
            System.out.println("我获取到资源了");
            Thread.sleep(1000);
            System.out.println("我释放资源了");
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 };

 for (int i = 0; i < 20; i++) {
    Thread t = new Thread(runnable);
    t.start();
 }

4、CountDownLatch

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

好像跑步比赛,10个选⼿依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。

• 构造CountDownLatch实例,初始化10表示有10个任务需要完成。

• 每个任务执行完毕,都调用 latch.countDown() 。在CountDownLatch内部的计数器同时自减。

• 主线程中使用 latch.await();阻塞等待所有任务执行完毕,相当于计数器为0了。

java 复制代码
 public class Demo {
    public static void main(String[] args) throws Exception {
        CountDownLatch latch = new CountDownLatch(10);
        Runnable r = new Runable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(Math.random() * 10000);
                    latch.countDown();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(r).start();
         }
             // 必须等到 10 ⼈全部回来 
         latch.await();
         System.out.println("⽐赛结束");
     }
 }

5、相关面试题

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机制,伪代码如下:

java 复制代码
 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操作。

相关推荐
编程火箭车1 天前
【Java SE 基础学习打卡】02 计算机硬件与软件
java·电脑选购·计算机基础·编程入门·计算机硬件·软件系统·编程学习路线
Felix_XXXXL1 天前
IDEA + Spring Boot 的三种热加载方案
java·后端
我命由我123451 天前
IDEA - IDEA 快速回到页面首尾、页面快速滑动、快速定位到指定行
java·运维·ide·后端·java-ee·intellij-idea·intellij idea
Mickyจุ๊บ1 天前
IDEA 插件推荐
java·ide·intellij-idea
千里镜宵烛1 天前
深入 Lua 环境机制:全局变量的 “容器” 与 “隔离术”
开发语言·junit·lua
命运之光1 天前
【快速解决】idea运行javafx错误: 缺少 JavaFX 运行时组件, 需要使用该组件来运行此应用程序
java·ide·intellij-idea
学到头秃的suhian1 天前
Maven
java·maven
QX_hao1 天前
【Go】--反射(reflect)的使用
开发语言·后端·golang
小坏讲微服务1 天前
Docker-compose 搭建Maven私服部署
java·spring boot·后端·docker·微服务·容器·maven